def build_document(self): series_list = [self.strategy_series, self.benchmark_series] # First Page self._add_header() self._add_perf_chart(series_list) self._add_relative_performance_chart(self.strategy_series, self.benchmark_series) self._add_statistics_table(series_list) # Second Page self.document.add_element(NewPageElement()) self._add_header() self.document.add_element(ParagraphElement("\n")) self._add_returns_statistics_charts() self._add_ret_distribution_and_similarity() self._add_rolling_table() # Third Page self.document.add_element(NewPageElement()) self._add_header() self._add_cone_and_quantiles() self._add_underwater_and_skewness() self._add_rolling_return_chart(series_list) self.document.add_element(ParagraphElement("\n")) self._add_rolling_vol_chart(series_list) self.document.add_element(ParagraphElement("\n")) self._add_rolling_alpha_and_beta(series_list)
def build_document(self): series_list = [self.strategy_series, self.benchmark_series] # First Page self._add_header() self._add_perf_chart(series_list) self._add_relative_performance_chart(self.strategy_series, self.benchmark_series) self._add_statistics_table(series_list) # Next Page self.document.add_element(NewPageElement()) self._add_header() self.document.add_element(ParagraphElement("\n")) self._add_returns_statistics_charts(self.strategy_series) self._add_returns_statistics_charts(self.benchmark_series) self.document.add_element(ParagraphElement("\n")) self.document.add_element(ParagraphElement("\n")) self._add_ret_distribution_and_similarity() # Next Page self.document.add_element(NewPageElement()) self._add_header() self.document.add_element(ParagraphElement("\n")) self.document.add_element(ParagraphElement("\n")) self._add_rolling_return_chart(series_list) self.document.add_element(ParagraphElement("\n")) self.document.add_element(ParagraphElement("\n")) self._add_rolling_vol_chart(series_list)
def _add_performance_statistics(self): """ For each ticker computes its overall performance (PnL of short positions, PnL of long positions, total PnL). It generates a table containing final PnL values for each of the ticker nad optionally plots the performance throughout the backtest. """ closed_positions = self.backtest_result.portfolio.closed_positions() closed_positions_pnl = QFDataFrame.from_records( data=[(self._ticker_name(p.contract()), p.end_time, p.direction(), p.total_pnl) for p in closed_positions], columns=["Tickers name", "Time", "Direction", "Realised PnL"] ) closed_positions_pnl = closed_positions_pnl.sort_values(by="Time") # Get all open positions history open_positions_history = self.backtest_result.portfolio.positions_history() open_positions_history = open_positions_history.reset_index().melt( id_vars='index', value_vars=open_positions_history.columns, var_name='Contract', value_name='Position summary') open_positions_pnl = QFDataFrame(data={ "Tickers name": open_positions_history["Contract"].apply(lambda contract: self._ticker_name(contract)), "Time": open_positions_history["index"], "Direction": open_positions_history["Position summary"].apply( lambda p: p.direction if isinstance(p, BacktestPositionSummary) else 0), "Total PnL of open position": open_positions_history["Position summary"].apply( lambda p: p.total_pnl if isinstance(p, BacktestPositionSummary) else 0) }) all_positions_pnl = pd.concat([closed_positions_pnl, open_positions_pnl], sort=False) performance_dicts_series = all_positions_pnl.groupby(by=["Tickers name"]).apply( self._performance_series_for_ticker) performance_df = QFDataFrame(performance_dicts_series.tolist(), index=performance_dicts_series.index) self.document.add_element(NewPageElement()) self.document.add_element(HeadingElement(level=2, text="Performance of each asset")) final_performance = performance_df. \ applymap(lambda pnl_series: pnl_series.iloc[-1] if not pnl_series.empty else 0.0). \ sort_values(by="Overall performance", ascending=False). \ applymap(lambda p: '{:,.2f}'.format(p)). \ reset_index() table = DFTable(final_performance, css_classes=['table', 'left-align']) table.add_columns_classes(["Tickers name"], 'wide-column') self.document.add_element(table) # Add performance plots if self.generate_pnl_chart_per_ticker: self.document.add_element(NewPageElement()) self.document.add_element( HeadingElement(level=2, text="Performance of each asset during the whole backtest")) for ticker_name, performance in performance_df.iterrows(): self._plot_ticker_performance(ticker_name, performance)
def build_document(self, backtest_summary: BacktestSummary, out_of_sample_start_date: Optional[datetime] = None): self.backtest_summary = backtest_summary self.backtest_evaluator = BacktestSummaryEvaluator(backtest_summary) self.document = Document(backtest_summary.backtest_name) self.out_of_sample_start_date = out_of_sample_start_date if out_of_sample_start_date is not None else \ (backtest_summary.start_date + (backtest_summary.end_date - backtest_summary.start_date) / 2) self._add_header() self._add_backtest_description() tickers_groups_for_stats_purposes = list(self.backtest_summary.tickers) # In case of > 1 ticker in the backtest summary, include also stats for all tickers if possible if len(self.backtest_summary.tickers) > 1: tickers_groups_for_stats_purposes = [ self.backtest_summary.tickers ] + tickers_groups_for_stats_purposes if backtest_summary.num_of_model_params not in [1, 2]: raise ValueError( "Incorrect number of parameters. Supported: 1 and 2") for tickers in tickers_groups_for_stats_purposes: tickers, _ = convert_to_list(tickers, Ticker) self.document.add_element(NewPageElement()) if backtest_summary.num_of_model_params == 1: self._add_line_plots(tickers) else: self._add_heat_maps(tickers)
def build_document(self): series_list = [self.strategy_series, self.benchmark_series] self._add_header() self.document.add_element(ParagraphElement("\n")) self._add_perf_chart(series_list) self.document.add_element(ParagraphElement("\n")) self._add_returns_statistics_charts() self.document.add_element(ParagraphElement("\n")) self._add_ret_distribution_and_similarity() self.document.add_element(ParagraphElement("\n")) self._add_rolling_table() # Next Page self.document.add_element(NewPageElement()) self.document.add_element(ParagraphElement("\n")) self._add_cone_and_quantiles() self._add_underwater_and_skewness() self._add_statistics_table(series_list)
def _add_pnl_and_performance_contribution_tables( self, ticker_to_pnl: Dict[Ticker, PricesSeries]): # For each ticker compute the PnL for each period (each year, month etc) pnl_df = QFDataFrame.from_dict(ticker_to_pnl) agg_performance = pnl_df.groupby(pd.Grouper(key=pnl_df.index.name, freq=self._frequency.to_pandas_freq())) \ .apply(lambda s: s.iloc[-1] - s.iloc[0]) # Format the column labels, so that they point exactly to the considered time frame column_labels_format = { Frequency.YEARLY: "%Y", Frequency.MONTHLY: "%b %Y", } columns_format = column_labels_format[self._frequency] performance_df = agg_performance.rename( index=lambda timestamp: timestamp.strftime(columns_format)) # Transpose the original data frame, so that performance for each period is presented in a separate column performance_df = performance_df.transpose() performance_df.index = performance_df.index.set_names("Asset") performance_df = performance_df.reset_index() performance_df["Asset"] = performance_df["Asset"].apply( lambda t: t.name) performance_tables = self._create_performance_tables( performance_df.copy()) performance_contribution_tables = self._create_performance_contribution_tables( performance_df.copy()) # Add the text and all figures into the document self.document.add_element( HeadingElement(level=2, text="Profit and Loss")) self.document.add_element( ParagraphElement( "The following tables provide the details on the Total profit and " "loss for each asset (notional in currency units).")) self.document.add_element(ParagraphElement("\n")) for table in performance_tables: self.document.add_element( HeadingElement(level=3, text="Performance between: {} - {}".format( table.model.data.columns[1], table.model.data.columns[-1]))) self.document.add_element(table) self.document.add_element(ParagraphElement("\n")) self.document.add_element(NewPageElement()) # Add performance contribution table self.document.add_element( HeadingElement(level=2, text="Performance contribution")) for table in performance_contribution_tables: self.document.add_element( HeadingElement( level=3, text="Performance contribution between {} - {}".format( table.model.data.columns[1], table.model.data.columns[-1]))) self.document.add_element(table)
def build_document(self): self._add_header() for ticker in self.tickers: self.create_tickers_analysis(ticker) self.document.add_element(NewPageElement()) self.add_models_implementation()
def _add_backtest_description(self): """ Adds verbal description of the backtest to the document. The description will be placed on a single page. """ param_names = self._get_param_names() self.document.add_element( HeadingElement( 1, "Model: {}".format(self.backtest_summary.backtest_name))) self.document.add_element(ParagraphElement("\n")) self.document.add_element( HeadingElement(2, "Tickers tested in this study: ")) ticker_str = "\n".join([ ticker.name if isinstance(ticker, FutureTicker) else ticker.as_string() for ticker in self.backtest_summary.tickers ]) self.document.add_element(ParagraphElement(ticker_str)) self.document.add_element(ParagraphElement("\n")) self.document.add_element(HeadingElement(2, "Dates of the backtest")) self.document.add_element( ParagraphElement("Backtest start date: {}".format( date_to_str(self.backtest_summary.start_date)))) self.document.add_element( ParagraphElement("Backtest end date: {}".format( date_to_str(self.backtest_summary.end_date)))) self.document.add_element(ParagraphElement("\n")) self.document.add_element(HeadingElement(2, "Parameters Tested")) for param_index, param_list in enumerate( self.backtest_summary.parameters_tested): param_list_str = ", ".join(map(str, param_list)) self.document.add_element( ParagraphElement("{} = [{}]".format(param_names[param_index], param_list_str))) self.document.add_element(NewPageElement()) self.document.add_element( HeadingElement(2, "Alpha model implementation")) self.document.add_element(ParagraphElement("\n")) model_type = self.backtest_summary.alpha_model_type with open(inspect.getfile(model_type)) as f: class_implementation = f.read() # Remove the imports section class_implementation = "<pre>class {}".format(model_type.__name__) + \ class_implementation.split("class {}".format(model_type.__name__))[1] + "</pre>" self.document.add_element(CustomElement(class_implementation))
def _add_avg_time_in_the_market_per_ticker(self): """ Compute the total time in the market per ticker (separately for long and short positions) in minutes and divide it by the total duration of the backtest in minutes. """ self.document.add_element(NewPageElement()) self.document.add_element(HeadingElement(level=2, text="Average time in the market per asset")) start_time = self.backtest_result.start_date end_time = self.backtest_result.portfolio.timer.now() backtest_duration = pd.Timedelta(end_time - start_time) / pd.Timedelta(minutes=1) # backtest duration in min closed_positions_time = [ (self._ticker_name(position.contract()), position.start_time, position.end_time, position.direction()) for position in self.backtest_result.portfolio.closed_positions() ] open_positions_time = [ (self._ticker_name(c), position.start_time, end_time, position.direction()) for c, position in self.backtest_result.portfolio.open_positions_dict.items() ] positions = QFDataFrame(data=closed_positions_time + open_positions_time, columns=["Tickers name", "Start time", "End time", "Position direction"]) def compute_duration(grouped_rows): return pd.DatetimeIndex([]).union_many( [pd.date_range(row["Start time"], row["End time"], freq='T', closed='left') for _, row in grouped_rows.iterrows()]).size positions = positions.groupby(by=["Tickers name", "Position direction"]).apply(compute_duration) \ .rename("Duration (minutes)").reset_index() positions["Duration"] = positions["Duration (minutes)"] / backtest_duration positions = positions.pivot_table(index="Tickers name", columns="Position direction", values="Duration").reset_index() positions = positions.rename(columns={-1: "Short", 1: "Long"}) # Add default 0 column in case if only short / long positions occurred in the backtest for column in ["Short", "Long"]: if column not in positions.columns: positions[column] = 0.0 positions["Out"] = 1.0 - positions["Long"] - positions["Short"] positions[["Long", "Short", "Out"]] = positions[["Long", "Short", "Out"]].applymap(lambda x: '{:.2%}'.format(x)) positions = positions.fillna(0.0) table = DFTable(positions, css_classes=['table', 'left-align']) table.add_columns_classes(["Tickers name"], 'wide-column') self.document.add_element(table)
def _add_performance_statistics(self, ticker_to_pnl_series: Dict[Ticker, PricesSeries]): """ Generate performance and drawdown plots, which provide the comparison between the strategy performance and Buy and Hold performance for each of the assets. """ self.document.add_element(NewPageElement()) self.document.add_element( HeadingElement( level=2, text="Performance and Drawdowns - Strategy vs Buy and Hold")) self.document.add_element(ParagraphElement("\n")) for ticker in self.tickers: grid = self._get_new_grid() buy_and_hold_returns = self._generate_buy_and_hold_returns(ticker) strategy_exposure_series = ticker_to_pnl_series[ ticker].to_simple_returns().fillna(0.0) strategy_exposure_series = strategy_exposure_series.where( strategy_exposure_series == 0.0).fillna(1.0) strategy_returns = buy_and_hold_returns * strategy_exposure_series strategy_returns = strategy_returns.dropna() strategy_returns.name = "Strategy" if len(strategy_returns) > 0: perf_chart = self._get_perf_chart( [buy_and_hold_returns, strategy_returns], False, "Performance - {}".format(ticker.name)) underwater_chart = self._get_underwater_chart( strategy_returns.to_prices(), title="Drawdown - {}".format(ticker.name), benchmark_series=buy_and_hold_returns.to_prices(), rotate_x_axis=True) grid.add_chart(perf_chart) grid.add_chart(underwater_chart) self.document.add_element(grid) else: self._logger.warning( "No data is available for {}. No plots will be generated.". format(ticker.name))
def build_document(self): """Creates a document with charts""" self._add_header() end_date = pd.concat(self.factors_series, axis=1).index.max() start_date = end_date - RelativeDelta(years=1) all_series_one_year = [self.benchmark_series.loc[start_date:]] + \ [series.loc[start_date:] for series in self.factors_series] self._add_perf_chart_for_factor(series_list=all_series_one_year, title="Factors - 1 Year") all_series = [self.benchmark_series] + self.factors_series self._add_perf_chart_for_factor(series_list=all_series, title="Factors - Full History", force_log_scale=True) for series in self.factors_series: self.document.add_element(NewPageElement()) self._add_header() self._add_perf_chart_for_factor(series_list=[ self.benchmark_series.loc[start_date:], series.loc[start_date:] ], title="{} - 1 Year".format( series.name)) self._add_relative_performance_chart( series.loc[start_date:], self.benchmark_series.loc[start_date:], chart_title="Relative Performance", legend_subtitle="Factor - Benchmark") self._add_perf_chart_for_factor( series_list=[self.benchmark_series, series], title="{} - Full History".format(series.name), force_log_scale=True) self.document.add_element(ParagraphElement("\n")) self._add_relative_performance_chart( series, self.benchmark_series, chart_title="Relative Performance", legend_subtitle="Factor - Benchmark")
def _add_page(self, ticker: Ticker): self._add_header() self.document.add_element(ParagraphElement("\n")) self.document.add_element(HeadingElement(2, ticker.as_string())) self.document.add_element(ParagraphElement("\n")) price_df = self.price_provider.get_price(ticker, PriceField.ohlcv(), self.start_date, self.end_date) self._insert_table_with_overall_measures(price_df, ticker) self.document.add_element(ParagraphElement("\n")) self._add_price_chart(price_df) self.document.add_element(ParagraphElement("\n")) self._add_trend_strength_chart(price_df) self.document.add_element(ParagraphElement("\n")) self._add_up_and_down_trend_strength(price_df) self.document.add_element(NewPageElement()) # add page break
def _add_simulation_results(self): """ Generate a data frame consisting of a certain number of "scenarios" (each scenario denotes one single equity curve). """ self.document.add_element(NewPageElement()) self.document.add_element(HeadingElement(level=1, text="Monte Carlo simulations\n")) self.document.add_element(HeadingElement(level=2, text="Average number of trades per year: {}\n".format( int(self._average_number_of_trades_per_year())))) if self.initial_risk is not None: self.document.add_element(HeadingElement(level=2, text="Initial risk: {:.2%}".format(self.initial_risk))) scenarios_df, total_returns = self._get_scenarios() # Plot all the possible paths on a chart all_paths_chart = self._get_simulation_plot(scenarios_df) self.document.add_element(ChartElement(all_paths_chart, figsize=self.full_image_size, dpi=self.dpi)) # Plot the distribution plot distribution_plot = self._get_distribution_plot( total_returns, title="Monte Carlo Simulations Distribution (one year % return)", bins=200, crop=True) # Format the x-axis so that its labels are shown as a percentage in case of percentage returns axes_formatter_decorator = AxesFormatterDecorator(x_major=PercentageFormatter(), key="axes_formatter") distribution_plot.add_decorator(axes_formatter_decorator) self.document.add_element(ChartElement(distribution_plot, figsize=self.full_image_size, dpi=self.dpi)) simulations_summary_table = self._get_monte_carlos_simulator_outputs(scenarios_df, total_returns) self.document.add_element(simulations_summary_table) # Extract the results of each of the scenarios and summarize the data in the tables dist_summary_tables = self._get_distribution_summary_table(total_returns) self.document.add_element(dist_summary_tables) # Add the "Chances of dropping below" and "Simulations summary" tables ruin_chances_table = self._get_chances_of_dropping_below_table(scenarios_df) self.document.add_element(ruin_chances_table)
def add_backtest_description(document: Document, backtest_result: BacktestSummary, param_names: List[str]): """ Adds verbal description of the backtest to the document. The description will be placed on a single page. """ document.add_element(ParagraphElement("\n")) document.add_element( HeadingElement(1, "Model: {}".format(backtest_result.backtest_name))) document.add_element(ParagraphElement("\n")) document.add_element(HeadingElement(2, "Tickers tested in this study: ")) ticker_str = "\n".join( [ticker.as_string() for ticker in backtest_result.tickers]) document.add_element(ParagraphElement(ticker_str)) document.add_element(ParagraphElement("\n")) document.add_element(HeadingElement(2, "Dates of the backtest")) document.add_element( ParagraphElement("Backtest start date: {}".format( date_to_str(backtest_result.start_date)))) document.add_element( ParagraphElement("Backtest end date: {}".format( date_to_str(backtest_result.end_date)))) document.add_element(ParagraphElement("\n")) document.add_element(HeadingElement(2, "Parameters Tested")) for param_index, param_list in enumerate( backtest_result.parameters_tested): param_list_str = ", ".join(map(str, param_list)) document.add_element( ParagraphElement("{} = [{}]".format(param_names[param_index], param_list_str))) document.add_element(NewPageElement())