def build_document(self, tickers: Sequence[Ticker], start_date: datetime, end_date: datetime, use_next_open_instead_of_close=False, title="Trend Strength"): """ tickers tickers of all tested instruments start_date start date of the study end_date end date of the study use_next_open_instead_of_close if True, the daily trend will be calculated from Open to Open of the next day, if False, the daily trend will be calculated from Open to Close of the same day title title of the document to be created """ self.use_next_open_instead_of_close = use_next_open_instead_of_close suffix = "O-O" if use_next_open_instead_of_close else "O-C" self.title = "{} {}".format(title, suffix) self.tickers = tickers self.document = Document(title) self.start_date = start_date self.end_date = end_date self.ticker_to_trend_dict = {} for ticker in self.tickers: try: self._add_page(ticker) print("Finished evaluating trend strength of: {}".format(ticker.as_string())) except Exception: print("Error while processing {}".format(ticker.as_string())) self._add_summary()
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 __init__(self, settings: Settings, pdf_exporter: PDFExporter, trades_df: QFDataFrame, start_date: datetime, end_date: datetime, nr_of_assets_traded: int = 1, title: str = "Trades"): """ trades_df indexed by consecutive numbers starting at 0. columns are indexed using TradeField values nr_of_assets_traded the model can be used to trade on many instruments at the same time. All aggregated trades will be in trades_df nr_of_instruments_traded informs on how many instruments at the same time the model was traded. title title of the document, will be a part of the filename. Do not use special characters """ self.trades_df = trades_df.sort_values([TradeField.EndDate, TradeField.StartDate]).reset_index(drop=True) self.start_date = start_date self.end_date = end_date self.nr_of_assets_traded = nr_of_assets_traded self.returns_of_trades = SimpleReturnsSeries(self.trades_df[TradeField.Return]) self.returns_of_trades.name = "Returns of Trades" self.title = title self.document = Document(title) # position is linked to the position of axis in tearsheet.mplstyle self.half_image_size = (4, 2.2) self.dpi = 400 self.settings = settings self.pdf_exporter = pdf_exporter
def create_document(self): self.document = Document(self.backtest_summary.backtest_name) self._add_header() add_backtest_description(self.document, self.backtest_summary) selected_tickers, rejected_tickers = self._evaluate_tickers() self.document.add_element(HeadingElement(2, "Selected Tickers")) self.document.add_element(ParagraphElement("\n")) self._add_table(selected_tickers) self.document.add_element(HeadingElement(2, "Rejected Tickers")) self.document.add_element(ParagraphElement("\n")) self._add_table(rejected_tickers)
def __init__(self, settings: Settings, pdf_exporter: PDFExporter, title: str = "Document Title"): self.title = title self.document = Document(title) self.full_image_size = (8, 2.4) # position is linked to the position of axis in tearsheet.mplstyle self.full_image_axis_position = (0.07, 0.1, 0.915, 0.80 ) # (left, bottom, width, height) self.half_image_size = (4, 2.1) self.dpi = 400 self.settings = settings self.pdf_exporter = pdf_exporter
def _merge_documents(documents: Sequence[Document], filename: str) -> Document: """ Merges documents into a single document. All elements inside the documents are placed in the returned document. Parameters ---------- documents a list of documents to merge filename The filename that should be applied to the resulting document. """ result = Document(filename) for document in documents: for element in document.elements: result.add_element(element) return result
def build_document(self, backtest_result: BacktestSummary): self.backtest_result = backtest_result self.backtest_evaluator = BacktestSummaryEvaluator(backtest_result) self.document = Document(backtest_result.backtest_name) self._add_header() param_names = self._get_param_names() add_backtest_description(self.document, self.backtest_result, param_names) if backtest_result.num_of_model_params == 1: chart_adding_function = self._add_line_chart_grid elif backtest_result.num_of_model_params == 2: parameters_list = self.backtest_evaluator.params_backtest_summary_elem_dict.keys() def fun(tickers): return self._add_heat_map_grid(tickers, parameters_list) chart_adding_function = fun elif backtest_result.num_of_model_params == 3: chart_adding_function = self._add_multiple_heat_maps else: raise ValueError("Incorrect number of parameters. Supported: 1, 2 and 3") self._create_charts(chart_adding_function)
def setUp(self): self.document = Document("")
class AbstractDocument(metaclass=ABCMeta): """ Base class for most PDF documents with charts and tables. Parameters ----------- settings: Settings settings containing all necessary information pdf_exporter: PDFExporter used to create PDF document title: str title of the document """ def __init__(self, settings: Settings, pdf_exporter: PDFExporter, title: str = "Document Title"): self.title = title self.document = Document(title) self.full_image_size = (8, 2.4) # position is linked to the position of axis in tearsheet.mplstyle self.full_image_axis_position = (0.07, 0.1, 0.915, 0.80 ) # (left, bottom, width, height) self.half_image_size = (4, 2.1) self.dpi = 400 self.settings = settings self.pdf_exporter = pdf_exporter @abstractmethod def build_document(self): # main function that composes the document pass @abstractmethod def save(self, report_dir: str = ""): # function that saves the document on the disk pass def _get_new_grid(self) -> GridElement: return GridElement(mode=PlottingMode.PDF, figsize=self.half_image_size, dpi=self.dpi) def _add_header(self): logo_path = join(get_starting_dir_abs_path(), self.settings.logo_path) company_name = self.settings.company_name self.document.add_element( PageHeaderElement(logo_path, company_name, self.title)) def _get_underwater_chart(self, series: QFSeries): underwater_chart = UnderwaterChart(series, rotate_x_axis=True) underwater_chart.add_decorator(TopDrawdownDecorator(series, 5)) underwater_chart.add_decorator(AxesLabelDecorator(y_label="Drawdown")) underwater_chart.add_decorator(TitleDecorator("Drawdown")) return underwater_chart def _get_large_perf_chart(self, series_list): return self.__get_perf_chart(series_list, True) def _get_small_perf_chart(self, series_list): return self.__get_perf_chart(series_list, False) def __get_perf_chart(self, series_list, is_large_chart): strategy = series_list[0].to_prices( 1) # the main strategy should be the first series log_scale = True if strategy[ -1] > 10 else False # use log scale for returns above 1 000 % if is_large_chart: chart = LineChart(start_x=strategy.index[0], end_x=strategy.index[-1], log_scale=log_scale) position_decorator = AxesPositionDecorator( *self.full_image_axis_position) chart.add_decorator(position_decorator) else: chart = LineChart(log_scale=log_scale, rotate_x_axis=True) line_decorator = HorizontalLineDecorator(1, key="h_line", linewidth=1) chart.add_decorator(line_decorator) legend = LegendDecorator() for series in series_list: strategy_tms = series.to_prices(1) series_elem = DataElementDecorator(strategy_tms) chart.add_decorator(series_elem) legend.add_entry(series_elem, strategy_tms.name) chart.add_decorator(legend) title_decorator = TitleDecorator("Strategy Performance", key="title") chart.add_decorator(title_decorator) return chart def _get_leverage_chart(self, leverage: QFSeries, rotate_x_axis: bool = False): return self._get_line_chart(leverage, "Leverage over time", rotate_x_axis) def _get_line_chart(self, series: QFSeries, title: str, rotate_x_axis: bool = False): chart = LineChart(rotate_x_axis=rotate_x_axis) series_elem = DataElementDecorator(series) chart.add_decorator(series_elem) title_decorator = TitleDecorator(title, key="title") chart.add_decorator(title_decorator) return chart def _get_rolling_ret_and_vol_chart(self, timeseries): freq = timeseries.get_frequency() rolling_window_len = int(freq.value / 2) # 6M rolling step = round(freq.value / 6) # 2M shift tms = timeseries.to_prices(1) chart = LineChart(start_x=tms.index[0], end_x=tms.index[-1]) line_decorator = HorizontalLineDecorator(0, key="h_line", linewidth=1) chart.add_decorator(line_decorator) legend = LegendDecorator() def tot_return(window): return PricesSeries(window).total_cumulative_return() def volatility(window): return get_volatility(PricesSeries(window), freq) functions = [tot_return, volatility] names = ['Rolling Return', 'Rolling Volatility'] for func, name in zip(functions, names): rolling = tms.rolling_window(rolling_window_len, func, step=step) rolling_element = DataElementDecorator(rolling) chart.add_decorator(rolling_element) legend.add_entry(rolling_element, name) chart.add_decorator(legend) chart.add_decorator( AxesFormatterDecorator(y_major=PercentageFormatter(".0f"))) position_decorator = AxesPositionDecorator( *self.full_image_axis_position) chart.add_decorator(position_decorator) title_str = "Rolling Stats [{} samples]".format(rolling_window_len) title_decorator = TitleDecorator(title_str, key="title") chart.add_decorator(title_decorator) return chart def _get_rolling_chart(self, timeseries_list, rolling_function, function_name): days_rolling = int(BUSINESS_DAYS_PER_YEAR / 2) # 6M rolling step = round(days_rolling / 5) legend = LegendDecorator() chart = None for i, tms in enumerate(timeseries_list): if i == 0: chart = LineChart(start_x=tms.index[0], end_x=tms.index[-1]) line_decorator = HorizontalLineDecorator(0, key="h_line", linewidth=1) chart.add_decorator(line_decorator) tms = tms.to_prices(1) rolling = tms.rolling_window(days_rolling, rolling_function, step=step) rolling_element = DataElementDecorator(rolling) chart.add_decorator(rolling_element) legend.add_entry(rolling_element, tms.name) chart.add_decorator(legend) chart.add_decorator( AxesFormatterDecorator(y_major=PercentageFormatter(".0f"))) position_decorator = AxesPositionDecorator( *self.full_image_axis_position) chart.add_decorator(position_decorator) title_str = "{} - Rolling Stats [{} days]".format( function_name, days_rolling) title_decorator = TitleDecorator(title_str, key="title") chart.add_decorator(title_decorator) return chart def _add_statistics_table(self, ta_list: List[TimeseriesAnalysis]): table = Table(css_class="table stats-table") for ta in ta_list: ta.populate_table(table) self.document.add_element(table)
class ModelParamsEvaluationDocument: def __init__(self, settings: Settings, pdf_exporter: PDFExporter): self.backtest_summary = None self.backtest_evaluator = None # type: BacktestSummaryEvaluator self.document = None self.out_of_sample_start_date = None # type: Optional[datetime] # position is linked to the position of axis in tearsheet.mplstyle self.image_size = (7, 6) self.full_image_size = (8, 2.4) self.image_axis_position = (0.08, 0.08, 0.92, 0.85) self.dpi = 400 self.settings = settings self.pdf_exporter = pdf_exporter 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 _add_header(self): logo_path = join(get_starting_dir_abs_path(), self.settings.logo_path) company_name = self.settings.company_name self.document.add_element( PageHeaderElement(logo_path, company_name, self.backtest_summary.backtest_name)) 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 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_line_plots(self, tickers: Sequence[Ticker]): parameters_list = sorted( self.backtest_evaluator.params_backtest_summary_elem_dict.keys()) title_to_plot = defaultdict(lambda: LineChart()) title_to_legend = defaultdict( lambda: LegendDecorator(key="legend_decorator")) for start_time, end_time in [ (self.backtest_summary.start_date, self.out_of_sample_start_date), (self.out_of_sample_start_date, self.backtest_summary.end_date) ]: results = [] for param_tuple in parameters_list: trades_eval_result = self.backtest_evaluator.evaluate_params_for_tickers( param_tuple, tickers, start_time, end_time) results.append(trades_eval_result) sqn_avg_nr_trades = DataElementDecorator( [x.sqn_per_avg_nr_trades for x in results]) avg_nr_of_trades = DataElementDecorator( [x.avg_nr_of_trades_1Y for x in results]) annualised_return = DataElementDecorator( [x.annualised_return for x in results]) adjusted_start_time = min([x.start_date for x in results]) adjusted_end_time = max([x.end_date for x in results]) if adjusted_start_time >= adjusted_end_time: adjusted_end_time = adjusted_start_time if adjusted_start_time <= self.backtest_summary.end_date \ else end_time adjusted_start_time = start_time title = "{} - {} ".format(adjusted_start_time.strftime("%Y-%m-%d"), adjusted_end_time.strftime("%Y-%m-%d")) title_to_plot["SQN (Arithmetic return) per year"].add_decorator( sqn_avg_nr_trades) title_to_legend["SQN (Arithmetic return) per year"].add_entry( sqn_avg_nr_trades, title) title_to_plot["Avg # trades 1Y"].add_decorator(avg_nr_of_trades) title_to_legend["Avg # trades 1Y"].add_entry( sqn_avg_nr_trades, title) if len(tickers) == 1: title_to_plot["Annualised return"].add_decorator( annualised_return) title_to_legend["Annualised return"].add_entry( annualised_return, title) tickers_used = "Many tickers" if len(tickers) > 1 else ( tickers[0].name) for description, line_chart in title_to_plot.items(): self.document.add_element( HeadingElement(3, "{} - {}".format(description, tickers_used))) line_chart.add_decorator( AxesLabelDecorator(x_label=self._get_param_names()[0], y_label=title)) position_decorator = AxesPositionDecorator( *self.image_axis_position) line_chart.add_decorator(position_decorator) legend = title_to_legend[description] line_chart.add_decorator(legend) self.document.add_element( ChartElement(line_chart, figsize=self.full_image_size)) def _add_heat_maps(self, tickers: Sequence[Ticker]): parameters_list = sorted( self.backtest_evaluator.params_backtest_summary_elem_dict.keys()) # Group plots by type, so that they appear in the given logical order title_to_grid = defaultdict(lambda: GridElement( mode=PlottingMode.PDF, figsize=self.image_size)) for start_time, end_time in [ (self.backtest_summary.start_date, self.out_of_sample_start_date), (self.out_of_sample_start_date, self.backtest_summary.end_date) ]: results = QFDataFrame() for param_tuple in parameters_list: trades_eval_result = self.backtest_evaluator.evaluate_params_for_tickers( param_tuple, tickers, start_time, end_time) row, column = param_tuple results.loc[row, column] = trades_eval_result results.sort_index(axis=0, inplace=True, ascending=False) results.sort_index(axis=1, inplace=True) results.fillna(TradesEvaluationResult(), inplace=True) sqn_avg_nr_trades = results.applymap( lambda x: x.sqn_per_avg_nr_trades).fillna(0) avg_nr_of_trades = results.applymap( lambda x: x.avg_nr_of_trades_1Y).fillna(0) annualised_return = results.applymap( lambda x: x.annualised_return).fillna(0) adjusted_start_time = results.applymap( lambda x: x.start_date).min().min() adjusted_end_time = results.applymap( lambda x: x.end_date).max().max() if adjusted_start_time >= adjusted_end_time: adjusted_end_time = adjusted_start_time if adjusted_start_time <= self.backtest_summary.end_date \ else end_time adjusted_start_time = start_time title = "{} - {} ".format(adjusted_start_time.strftime("%Y-%m-%d"), adjusted_end_time.strftime("%Y-%m-%d")) title_to_grid["SQN (Arithmetic return) per year"].add_chart( self._create_single_heat_map(title, sqn_avg_nr_trades, 0, 0.5)) title_to_grid["Avg # trades 1Y"].add_chart( self._create_single_heat_map(title, avg_nr_of_trades, 2, 15)) if len(tickers) == 1: title_to_grid["Annualised return"].add_chart( self._create_single_heat_map(title, annualised_return, 0.0, 0.3)) tickers_used = "Many tickers" if len(tickers) > 1 else ( tickers[0].name) for description, grid in title_to_grid.items(): self.document.add_element( HeadingElement(3, "{} - {}".format(description, tickers_used))) self.document.add_element(grid) def _create_single_heat_map(self, title, result_df, min_v, max_v): chart = HeatMapChart(data=result_df, color_map=plt.get_cmap("coolwarm"), min_value=min_v, max_value=max_v) chart.add_decorator( AxisTickLabelsDecorator(labels=list(result_df.columns), axis=Axis.X)) chart.add_decorator( AxisTickLabelsDecorator(labels=list(reversed(result_df.index)), axis=Axis.Y)) chart.add_decorator(ValuesAnnotations()) param_names = self._get_param_names() chart.add_decorator( AxesLabelDecorator(x_label=param_names[1], y_label=param_names[0])) chart.add_decorator(TitleDecorator(title)) position_decorator = AxesPositionDecorator(*self.image_axis_position) chart.add_decorator(position_decorator) return chart def _get_param_names(self): model_parameters_names = [ tuple(el.model_parameters_names) for el in self.backtest_summary.elements_list ] assert len( set(model_parameters_names) ) == 1, "All parameters should have exactly the same parameters tuples" return model_parameters_names[0] def save(self, title: Optional[str] = None): if self.document is not None: output_sub_dir = "param_estimation" # Set the style for the report plt.style.use(['tearsheet']) if title is None: title = self.backtest_summary.backtest_name filename = "%Y_%m_%d-%H%M {}.pdf".format(title) filename = datetime.now().strftime(filename) self.pdf_exporter.generate([self.document], output_sub_dir, filename) else: raise AssertionError( "The documnent is not initialized. Build the document first")
class ModelParamsEvaluator(object): def __init__(self, settings: Settings, pdf_exporter: PDFExporter): self.backtest_result = None self.backtest_evaluator = None # type: BacktestSummaryEvaluator self.document = None # position is linked to the position of axis in tearsheet.mplstyle self.image_size = (7, 6) self.image_axis_position = (0.08, 0.08, 0.92, 0.85) self.dpi = 400 self.settings = settings self.pdf_exporter = pdf_exporter def build_document(self, backtest_result: BacktestSummary): self.backtest_result = backtest_result self.backtest_evaluator = BacktestSummaryEvaluator(backtest_result) self.document = Document(backtest_result.backtest_name) self._add_header() param_names = self._get_param_names() add_backtest_description(self.document, self.backtest_result, param_names) if backtest_result.num_of_model_params == 1: chart_adding_function = self._add_line_chart_grid elif backtest_result.num_of_model_params == 2: parameters_list = self.backtest_evaluator.params_backtest_summary_elem_dict.keys() def fun(tickers): return self._add_heat_map_grid(tickers, parameters_list) chart_adding_function = fun elif backtest_result.num_of_model_params == 3: chart_adding_function = self._add_multiple_heat_maps else: raise ValueError("Incorrect number of parameters. Supported: 1, 2 and 3") self._create_charts(chart_adding_function) def _create_charts(self, chart_adding_function: Callable[[Any], None]): # create a chart for all trades of all tickers traded chart_adding_function(tickers=self.backtest_result.tickers) for ticker in self.backtest_result.tickers: # create a chart for single ticker chart_adding_function(tickers=[ticker]) def _add_header(self): logo_path = join(get_starting_dir_abs_path(), self.settings.logo_path) company_name = self.settings.company_name self.document.add_element(PageHeaderElement(logo_path, company_name, self.backtest_result.backtest_name)) def _add_line_chart_grid(self, tickers: Sequence[Ticker]): grid = GridElement(mode=PlottingMode.PDF, figsize=self.image_size) params = sorted(self.backtest_evaluator.params_backtest_summary_elem_dict.keys()) # this will sort the tuples params_as_values = [param_tuple[0] for param_tuple in params] results = [] for param_tuple in params: trades_eval_result = self.backtest_evaluator.evaluate_params_for_tickers(param_tuple, tickers) results.append(trades_eval_result) sqn_avg_trades = [elem.sqn_per_avg_nr_trades for elem in results] sqn_per100trades = [elem.sqn_per100trades for elem in results] avg_nr_of_trades = [elem.avg_nr_of_trades_1Y for elem in results] annualised_return = [elem.annualised_return for elem in results] drawdown = [elem.drawdown for elem in results] grid.add_chart(self._crete_single_line_chart("SQN / AVG #trades", params_as_values, sqn_avg_trades, tickers)) grid.add_chart(self._crete_single_line_chart("SQN / 100 trades", params_as_values, sqn_per100trades, tickers)) grid.add_chart(self._crete_single_line_chart("Avg # trades 1Y", params_as_values, avg_nr_of_trades, tickers)) grid.add_chart(self._crete_single_line_chart("An Return", params_as_values, annualised_return, tickers)) grid.add_chart(self._crete_single_line_chart("Drawdown", params_as_values, drawdown, tickers)) self.document.add_element(grid) def _crete_single_line_chart(self, measure_name, parameters, values, tickers): result_series = QFSeries(data=values, index=parameters) line_chart = LineChart() data_element = DataElementDecorator(result_series) line_chart.add_decorator(data_element) line_chart.add_decorator(TitleDecorator(self._get_chart_title(tickers, measure_name))) param_names = self._get_param_names() line_chart.add_decorator(AxesLabelDecorator(x_label=param_names[0], y_label=measure_name)) self._resize_chart(line_chart) return line_chart def _add_heat_map_grid(self, tickers: Sequence[Ticker], parameters_list: Sequence[tuple], third_param=None): grid = GridElement(mode=PlottingMode.PDF, figsize=self.image_size) result_df = QFDataFrame() for param_tuple in parameters_list: row = param_tuple[0] column = param_tuple[1] trades_eval_result = self.backtest_evaluator.evaluate_params_for_tickers(param_tuple, tickers) result_df.loc[row, column] = trades_eval_result result_df.sort_index(axis=0, inplace=True, ascending=False) result_df.sort_index(axis=1, inplace=True) sqn_avg_nr_trades = result_df.applymap(lambda x: x.sqn_per_avg_nr_trades) sqn_per100trades = result_df.applymap(lambda x: x.sqn_per100trades) avg_nr_of_trades = result_df.applymap(lambda x: x.avg_nr_of_trades_1Y) annualised_return = result_df.applymap(lambda x: x.annualised_return) drawdown = result_df.applymap(lambda x: x.drawdown) grid.add_chart(self._create_single_heat_map("SQN / AVG #trades", sqn_avg_nr_trades, tickers, 0, 1, third_param)) grid.add_chart(self._create_single_heat_map("SQN / 100 trades", sqn_per100trades, tickers, 0, 2, third_param)) grid.add_chart(self._create_single_heat_map("Avg # trades 1Y", avg_nr_of_trades, tickers, 3, 30, third_param)) grid.add_chart(self._create_single_heat_map("An Return", annualised_return, tickers, -0.1, 0.2, third_param)) grid.add_chart(self._create_single_heat_map("Drawdown", drawdown, tickers, -0.5, -0.1, third_param)) self.document.add_element(grid) def _create_single_heat_map(self, measure_name, result_df, tickers, min_v, max_v, third_param): chart = HeatMapChart(data=result_df, color_map=plt.get_cmap("coolwarm"), min_value=min_v, max_value=max_v) chart.add_decorator(AxisTickLabelsDecorator(labels=list(result_df.columns), axis=Axis.X)) chart.add_decorator(AxisTickLabelsDecorator(labels=list(reversed(result_df.index)), axis=Axis.Y)) chart.add_decorator(ValuesAnnotations()) param_names = self._get_param_names() chart.add_decorator(AxesLabelDecorator(x_label=param_names[1], y_label=param_names[0])) if third_param is None: title = self._get_chart_title(tickers, measure_name) else: title = "{}, {} = {:0.2f}".format(self._get_chart_title(tickers, measure_name), param_names[2], third_param) chart.add_decorator(TitleDecorator(title)) self._resize_chart(chart) return chart def _resize_chart(self, chart): left, bottom, width, height = self.image_axis_position position_decorator = AxesPositionDecorator(left, bottom, width, height) chart.add_decorator(position_decorator) def _add_multiple_heat_maps(self, tickers: Sequence[Ticker]): parameters_list = self.backtest_evaluator.params_backtest_summary_elem_dict.keys() # sort by third parameter, must have for groupby. sorted_list = sorted(parameters_list, key=lambda x: x[2]) for third_param, group in groupby(sorted_list, lambda x: x[2]): self._add_heat_map_grid(tickers, group, third_param=third_param) def _get_chart_title(self, tickers, measure_name): if len(tickers) > 1: title = "{} - Many Tickers".format(measure_name) elif len(tickers) == 1: title = "{} - {}".format(measure_name, tickers[0].as_string()) else: raise ValueError("No tickers provided") return title def _get_param_names(self): names = [] num_of_params = self.backtest_result.num_of_model_params model_type = self.backtest_result.alpha_model_type args = getfullargspec(model_type.__init__).args if len(args) <= 3: # alpha model type does not include any parameter fields to evaluate for param in range(1, num_of_params + 1): names.append("Parameter #{}".format(param)) else: for param in range(1, num_of_params + 1): names.append(args[param]) assert len(names) == self.backtest_result.num_of_model_params return names def save(self): if self.document is not None: output_sub_dir = "param_estimation" # Set the style for the report plt.style.use(['tearsheet']) filename = "%Y_%m_%d-%H%M {}.pdf".format(self.backtest_result.backtest_name) filename = datetime.now().strftime(filename) self.pdf_exporter.generate([self.document], output_sub_dir, filename) else: raise AssertionError("The documnent is not initialized. Build the document first")
class TickerScreener(object): """ TODO: class not implemented in full Class enables generation of the PDF document containing evaluation of trades on individual tickers """ def __init__(self, backtest_summary: BacktestSummary, settings: Settings, pdf_exporter: PDFExporter): self.backtest_summary = backtest_summary self.settings = settings self.pdf_exporter = pdf_exporter self.document = None self.all_tickers_tested = backtest_summary.tickers self.num_of_model_params = backtest_summary.num_of_model_params def create_document(self): self.document = Document(self.backtest_summary.backtest_name) self._add_header() add_backtest_description(self.document, self.backtest_summary) selected_tickers, rejected_tickers = self._evaluate_tickers() self.document.add_element(HeadingElement(2, "Selected Tickers")) self.document.add_element(ParagraphElement("\n")) self._add_table(selected_tickers) self.document.add_element(HeadingElement(2, "Rejected Tickers")) self.document.add_element(ParagraphElement("\n")) self._add_table(rejected_tickers) def _add_header(self): logo_path = join(get_starting_dir_abs_path(), self.settings.logo_path) company_name = self.settings.company_name self.document.add_element(PageHeaderElement(logo_path, company_name, self.backtest_summary.backtest_name)) def _evaluate_tickers(self): for ticker in self.all_tickers_tested: for backtest_elem in self.backtest_summary.elements_list: raise NotImplementedError() # ticker_eval = TradesEvaluationResult() # ticker_eval.SQN = sqn(ticker_trades_df) # ticker_eval.avg_nr_of_trades_1Y = avg_nr_of_trades_per1y() def _add_table(self, tickers_eval_list): table = Table(column_names=["Ticker", "Max SQN per 100 trades", "Avg #trades per 1Y for Max SQN"], css_class="table stats-table") sorted_tickers_eval_list = sorted(tickers_eval_list, key=lambda x: x.SQN) for ticker_eval in sorted_tickers_eval_list: table.add_row([ticker_eval.ticker.as_string(), ticker_eval.SQN, ticker_eval.avg_nr_of_trades_1Y]) self.document.add_element(table) def _select_trades_of_ticker(self, trades: QFDataFrame, ticker: Ticker): """ Select only the trades generated by the ticker provided If ticker is not provided (None) return all the trades """ if ticker is not None: trades = trades.loc[trades[TradeField.Ticker] == ticker] return trades def _objective_function(self, trades: QFDataFrame): """ Calculates the simple SQN * sqrt(average number of trades per year) """ number_of_instruments_traded = len(self.all_tickers_tested) returns = trades[TradeField.Return] period_length = self.backtest_summary.end_date - self.backtest_summary.start_date period_length_in_years = to_days(period_length) / DAYS_PER_YEAR_AVG avg_number_of_trades_1y = returns.count() / period_length_in_years / number_of_instruments_traded sqn = returns.mean() / returns.std() sqn = sqn * np.sqrt(avg_number_of_trades_1y) return sqn def save(self): if self.document is not None: output_sub_dir = "param_estimation" # Set the style for the report plt.style.use(['tearsheet']) filename = "%Y_%m_%d-%H%M Screening {}.pdf".format(self.backtest_summary.backtest_name) filename = datetime.now().strftime(filename) self.pdf_exporter.generate([self.document], output_sub_dir, filename) else: raise AssertionError("The documnent is not initialized. Build the document first")
class TradeAnalysisSheet(object): """ Creates a PDF containing main statistics of the trades """ def __init__(self, settings: Settings, pdf_exporter: PDFExporter, trades_df: QFDataFrame, start_date: datetime, end_date: datetime, nr_of_assets_traded: int = 1, title: str = "Trades"): """ trades_df indexed by consecutive numbers starting at 0. columns are indexed using TradeField values nr_of_assets_traded the model can be used to trade on many instruments at the same time. All aggregated trades will be in trades_df nr_of_instruments_traded informs on how many instruments at the same time the model was traded. title title of the document, will be a part of the filename. Do not use special characters """ self.trades_df = trades_df.sort_values([TradeField.EndDate, TradeField.StartDate]).reset_index(drop=True) self.start_date = start_date self.end_date = end_date self.nr_of_assets_traded = nr_of_assets_traded self.returns_of_trades = SimpleReturnsSeries(self.trades_df[TradeField.Return]) self.returns_of_trades.name = "Returns of Trades" self.title = title self.document = Document(title) # position is linked to the position of axis in tearsheet.mplstyle self.half_image_size = (4, 2.2) self.dpi = 400 self.settings = settings self.pdf_exporter = pdf_exporter def build_document(self): self._add_header() self.document.add_element(ParagraphElement("\n")) self._add_histogram_and_cumulative() self._add_statistics_table() def _add_header(self): logo_path = join(get_starting_dir_abs_path(), self.settings.logo_path) company_name = self.settings.company_name self.document.add_element(PageHeaderElement(logo_path, company_name, self.title)) def _add_histogram_and_cumulative(self): grid = GridElement(mode=PlottingMode.PDF, figsize=self.half_image_size, dpi=self.dpi) perf_chart = self._get_perf_chart() grid.add_chart(perf_chart) histogram_chart = self._get_histogram_chart() grid.add_chart(histogram_chart) self.document.add_element(grid) def _get_perf_chart(self): strategy_tms = self.returns_of_trades.to_prices(1) chart = LineChart() line_decorator = HorizontalLineDecorator(1, key="h_line", linewidth=1) chart.add_decorator(line_decorator) series_elem = DataElementDecorator(strategy_tms) chart.add_decorator(series_elem) title_decorator = TitleDecorator("Alpha Model Performance", key="title") chart.add_decorator(title_decorator) return chart def _get_histogram_chart(self): colors = Chart.get_axes_colors() chart = HistogramChart(self.returns_of_trades * 100) # expressed in % # Format the x-axis so that its labels are shown as a percentage. x_axis_formatter = FormatStrFormatter("%0.0f%%") axes_formatter_decorator = AxesFormatterDecorator(x_major=x_axis_formatter, key="axes_formatter") chart.add_decorator(axes_formatter_decorator) # Only show whole numbers on the y-axis. y_axis_locator = MaxNLocator(integer=True) axes_locator_decorator = AxesLocatorDecorator(y_major=y_axis_locator, key="axes_locator") chart.add_decorator(axes_locator_decorator) # Add an average line. avg_line = VerticalLineDecorator(self.returns_of_trades.values.mean(), color=colors[1], key="average_line_decorator", linestyle="--", alpha=0.8) chart.add_decorator(avg_line) # Add a legend. legend = LegendDecorator(key="legend_decorator") legend.add_entry(avg_line, "Mean") chart.add_decorator(legend) # Add a title. title = TitleDecorator("Distribution of Trades", key="title_decorator") chart.add_decorator(title) chart.add_decorator(AxesLabelDecorator("Return", "Occurrences")) return chart def _add_statistics_table(self): table = Table(column_names=["Measure", "Value"], css_class="table stats-table") number_of_trades = self.returns_of_trades.count() table.add_row(["Number of trades", number_of_trades]) period_length = self.end_date - self.start_date period_length_in_years = to_days(period_length) / DAYS_PER_YEAR_AVG avg_number_of_trades = number_of_trades / period_length_in_years / self.nr_of_assets_traded table.add_row(["Avg number of trades per year per asset", avg_number_of_trades]) positive_trades = self.returns_of_trades[self.returns_of_trades > 0] negative_trades = self.returns_of_trades[self.returns_of_trades < 0] percentage_of_positive = positive_trades.count() / number_of_trades percentage_of_negative = negative_trades.count() / number_of_trades table.add_row(["% of positive trades", percentage_of_positive * 100]) table.add_row(["% of negative trades", percentage_of_negative * 100]) avg_positive = positive_trades.mean() avg_negative = negative_trades.mean() table.add_row(["Avg positive trade [%]", avg_positive * 100]) table.add_row(["Avg negative trade [%]", avg_negative * 100]) best_return = max(self.returns_of_trades) worst_return = min(self.returns_of_trades) table.add_row(["Best trade [%]", best_return * 100]) table.add_row(["Worst trade [%]", worst_return * 100]) max_dd = max_drawdown(self.returns_of_trades) table.add_row(["Max drawdown [%]", max_dd * 100]) prices_tms = self.returns_of_trades.to_prices() total_return = prices_tms.iloc[-1] / prices_tms.iloc[0] - 1 table.add_row(["Total return [%]", total_return * 100]) annualised_ret = annualise_total_return(total_return, period_length_in_years, SimpleReturnsSeries) table.add_row(["Annualised return [%]", annualised_ret * 100]) avg_return = self.returns_of_trades.mean() table.add_row(["Avg return of trade [%]", avg_return * 100]) std_of_returns = self.returns_of_trades.std() table.add_row(["Std of return of trades [%]", std_of_returns * 100]) # System Quality Number sqn = avg_return / std_of_returns table.add_row(["SQN", sqn]) table.add_row(["SQN for 100 trades", sqn * 10]) # SQN * sqrt(100) table.add_row(["SQN * Sqrt(avg number of trades per year)", sqn * sqrt(avg_number_of_trades)]) self.document.add_element(table) def save(self): output_sub_dir = "trades_analysis" # Set the style for the report plt.style.use(['tearsheet']) filename = "%Y_%m_%d-%H%M {}.pdf".format(self.title) filename = datetime.now().strftime(filename) self.pdf_exporter.generate([self.document], output_sub_dir, filename)
class AbstractDocument(metaclass=ABCMeta): """ Base class for Most PDF document with charts and tables. """ def __init__(self, settings: Settings, pdf_exporter: PDFExporter, title: str = "Document Title"): self.title = title self.document = Document(title) self.full_image_size = (8, 2.4) # position is linked to the position of axis in tearsheet.mplstyle self.full_image_axis_position = (0.07, 0.1, 0.915, 0.80 ) # (left, bottom, width, height) self.half_image_size = (4, 2.1) self.dpi = 400 self.settings = settings self.pdf_exporter = pdf_exporter @abstractmethod def build_document(self): # main function that composes the document pass @abstractmethod def save(self, report_dir: str = ""): # function that saves the document on the disk pass def _get_new_grid(self) -> GridElement: return GridElement(mode=PlottingMode.PDF, figsize=self.half_image_size, dpi=self.dpi) def _add_header(self): logo_path = join(get_starting_dir_abs_path(), self.settings.logo_path) company_name = self.settings.company_name self.document.add_element( PageHeaderElement(logo_path, company_name, self.title)) def _get_underwater_chart(self, series: QFSeries): underwater_chart = UnderwaterChart(series, rotate_x_axis=True) underwater_chart.add_decorator(TopDrawdownDecorator(series, 5)) underwater_chart.add_decorator(AxesLabelDecorator(y_label="Drawdown")) underwater_chart.add_decorator(TitleDecorator("Drawdown")) return underwater_chart def _get_large_perf_chart(self, series_list): return self.__get_perf_chart(series_list, True) def _get_small_perf_chart(self, series_list): return self.__get_perf_chart(series_list, False) def __get_perf_chart(self, series_list, is_large_chart): strategy = series_list[0].to_prices( 1) # the main strategy should be the first series log_scale = True if strategy[ -1] > 5 else False # use log scale for returns above 500 % if is_large_chart: chart = LineChart(start_x=strategy.index[0], end_x=strategy.index[-1], log_scale=log_scale) position_decorator = AxesPositionDecorator( *self.full_image_axis_position) chart.add_decorator(position_decorator) else: chart = LineChart(log_scale=log_scale, rotate_x_axis=True) line_decorator = HorizontalLineDecorator(1, key="h_line", linewidth=1) chart.add_decorator(line_decorator) legend = LegendDecorator() for series in series_list: strategy_tms = series.to_prices(1) series_elem = DataElementDecorator(strategy_tms) chart.add_decorator(series_elem) legend.add_entry(series_elem, strategy_tms.name) chart.add_decorator(legend) title_decorator = TitleDecorator("Strategy Performance", key="title") chart.add_decorator(title_decorator) return chart def _get_leverage_chart(self, leverage: QFSeries, rotate_x_axis: bool = False): chart = LineChart(rotate_x_axis=rotate_x_axis) series_elem = DataElementDecorator(leverage) chart.add_decorator(series_elem) title_decorator = TitleDecorator("Leverage over time", key="title") chart.add_decorator(title_decorator) return chart def _add_statistics_table(self, ta_list: List[TimeseriesAnalysis]): table = Table(css_class="table stats-table") for ta in ta_list: ta.populate_table(table) self.document.add_element(table)
class TrendStrengthSheet(object): """ Creates a PDF containing main statistics of strength of a day trend """ def __init__(self, settings: Settings, pdf_exporter: PDFExporter, price_provider: DataProvider, window_len=128): self.settings = settings self.pdf_exporter = pdf_exporter self.price_provider = price_provider self.window_len = window_len self.start_date = None self.end_date = None self.title = None self.document = None self.tickers = None self.ticker_to_trend_dict = None # if True, the daily trend will be calculated from Open to Open of the next day, # if False, the daily trend will be calculated from Open to Close of the same day self.use_next_open_instead_of_close = None # position is linked to the position of axis in tearsheet.mplstyle self.image_size = (8, 2.4) self.full_image_axis_position = (0.06, 0.1, 0.94, 0.80) self.dpi = 400 def build_document(self, tickers: Sequence[Ticker], start_date: datetime, end_date: datetime, use_next_open_instead_of_close=False, title="Trend Strength"): """ tickers tickers of all tested instruments start_date start date of the study end_date end date of the study use_next_open_instead_of_close if True, the daily trend will be calculated from Open to Open of the next day, if False, the daily trend will be calculated from Open to Close of the same day title title of the document to be created """ self.use_next_open_instead_of_close = use_next_open_instead_of_close suffix = "O-O" if use_next_open_instead_of_close else "O-C" self.title = "{} {}".format(title, suffix) self.tickers = tickers self.document = Document(title) self.start_date = start_date self.end_date = end_date self.ticker_to_trend_dict = {} for ticker in self.tickers: try: self._add_page(ticker) print("Finished evaluating trend strength of: {}".format(ticker.as_string())) except Exception: print("Error while processing {}".format(ticker.as_string())) self._add_summary() 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_header(self): logo_path = join(get_starting_dir_abs_path(), self.settings.logo_path) company_name = self.settings.company_name self.document.add_element(PageHeaderElement(logo_path, company_name, self.title)) def _insert_table_with_overall_measures(self, prices_df: PricesDataFrame, ticker: Ticker): table = Table(column_names=["Measure", "Value"], css_class="table stats-table") table.add_row(["Instrument", ticker.as_string()]) series = prices_df[PriceField.Close] table.add_row(["Start date", date_to_str(series.index[0])]) table.add_row(["End date", date_to_str(series.index[-1])]) trend_strength_overall = trend_strength(prices_df, self.use_next_open_instead_of_close) table.add_row(["Overall strength of the day trends", trend_strength_overall]) trend_strength_1y = trend_strength(prices_df.tail(252), self.use_next_open_instead_of_close) table.add_row(["Strength of the day trends in last 1Y", trend_strength_1y]) self.ticker_to_trend_dict[ticker] = (trend_strength_1y, trend_strength_overall) table.add_row(["Up trends strength", up_trend_strength(prices_df, self.use_next_open_instead_of_close)]) table.add_row(["Down trends strength", down_trend_strength(prices_df, self.use_next_open_instead_of_close)]) self.document.add_element(table) def _add_price_chart(self, prices_df: QFDataFrame): close_tms = prices_df[PriceField.Close] price_tms = close_tms.to_prices(1) chart = LineChart(start_x=price_tms.index[0], end_x=price_tms.index[-1]) price_elem = DataElementDecorator(price_tms) chart.add_decorator(price_elem) line_decorator = HorizontalLineDecorator(1, key="h_line", linewidth=1) chart.add_decorator(line_decorator) legend = LegendDecorator() legend.add_entry(price_elem, "Close Price") chart.add_decorator(legend) title_decorator = TitleDecorator("Price of the instrument", key="title") chart.add_decorator(title_decorator) self._add_axes_position_decorator(chart) self.document.add_element(ChartElement(chart, figsize=self.image_size, dpi=self.dpi)) def _add_trend_strength_chart(self, prices_df: QFDataFrame): def _fun(df): return trend_strength(df, self.use_next_open_instead_of_close) trend_strength_tms = prices_df.rolling_time_window(window_length=self.window_len, step=1, func=_fun) chart = LineChart() trend_elem = DataElementDecorator(trend_strength_tms, color='black') chart.add_decorator(trend_elem) legend = LegendDecorator(legend_placement=Location.BEST, key='legend') legend.add_entry(trend_elem, 'Trend strength') chart.add_decorator(legend) title_decorator = TitleDecorator("Strength of the trend - rolling {} days".format(self.window_len), key="title") chart.add_decorator(title_decorator) self._add_axes_position_decorator(chart) self.document.add_element(ChartElement(chart, figsize=self.image_size, dpi=self.dpi)) def _add_up_and_down_trend_strength(self, prices_df: QFDataFrame): def _down_trend_fun(df): return down_trend_strength(df, self.use_next_open_instead_of_close) def _up_trend_fun(df): return up_trend_strength(df, self.use_next_open_instead_of_close) up_trend_strength_tms = prices_df.rolling_time_window(window_length=self.window_len, step=1, func=_down_trend_fun) down_trend_strength_tms = prices_df.rolling_time_window(window_length=self.window_len, step=1, func=_up_trend_fun) chart = LineChart() up_trend_elem = DataElementDecorator(up_trend_strength_tms) down_trend_elem = DataElementDecorator(down_trend_strength_tms) chart.add_decorator(up_trend_elem) chart.add_decorator(down_trend_elem) legend = LegendDecorator(legend_placement=Location.BEST, key='legend') legend.add_entry(up_trend_elem, 'Up trend strength') legend.add_entry(down_trend_elem, 'Down trend strength') chart.add_decorator(legend) title = "Strength of the up and down trend - rolling {} days".format(self.window_len) title_decorator = TitleDecorator(title, key="title") chart.add_decorator(title_decorator) self._add_axes_position_decorator(chart) self.document.add_element(ChartElement(chart, figsize=self.image_size, dpi=self.dpi)) def _add_summary(self): self.document.add_element(ParagraphElement("\n")) self.document.add_element(HeadingElement(2, "Summary")) self.document.add_element(ParagraphElement("\n")) self.document.add_element(ParagraphElement("1Y strength - Overall strength - Ticker\n")) pairs_sorted_by_value = sorted(self.ticker_to_trend_dict.items(), key=lambda pair: pair[1], reverse=True) for ticker, trend_strength_values in pairs_sorted_by_value: paragraph_str = "{:12.3f} - {:12.3f} - {}".format( trend_strength_values[0], trend_strength_values[1], ticker.as_string()) self.document.add_element(ParagraphElement(paragraph_str)) def _add_axes_position_decorator(self, chart: Chart): left, bottom, width, height = self.full_image_axis_position position_decorator = AxesPositionDecorator(left, bottom, width, height) chart.add_decorator(position_decorator) def save(self): output_sub_dir = "trend_strength" # Set the style for the report plt.style.use(['tearsheet']) filename = "%Y_%m_%d-%H%M {}.pdf".format(self.title) filename = datetime.now().strftime(filename) self.pdf_exporter.generate([self.document], output_sub_dir, filename)
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())