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 _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)
Beispiel #3
0
    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_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_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 if isinstance(
                tickers[0], FutureTicker) else tickers[0].as_string())

        for description, grid in title_to_grid.items():
            self.document.add_element(
                HeadingElement(3, "{} - {}".format(description, tickers_used)))
            self.document.add_element(grid)
    def build_document(self):
        self._add_header()

        self.document.add_element(ParagraphElement("\n"))
        self.document.add_element(
            HeadingElement(level=2, text="Open Positions in the Portfolio"))
        self._add_open_positions_table()
    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))
Beispiel #8
0
    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)
Beispiel #9
0
    def create_tickers_analysis(self, ticker: Ticker):
        """
        For the given ticker add candlestick charts representing OHLC prices with highlighted signals generated by each
        of the alpha models. In case if the model generates a BUY signal the background for the given day is highlighted
        with green color, in case of a SELL signal - red.
        """
        prices_df = self.get_prices(ticker)
        alpha_model_signals: Dict[AlphaModel, QFSeries] = {}

        self.document.add_element(HeadingElement(level=2, text=ticker.name))

        for alpha_model in self.alpha_models:
            exposures = []
            dates = []

            prev_exposure = Exposure.OUT

            for date in date_range(self.start_date, self.end_date, freq="B"):
                try:
                    self.timer.set_current_time(date)
                    new_exposure = alpha_model.get_signal(
                        ticker, prev_exposure).suggested_exposure
                    exposures.append(new_exposure.value)
                    dates.append(date)

                    if not self.only_entry_signals:
                        prev_exposure = new_exposure
                except NoValidTickerException as e:
                    print(e)

            exposures_series = QFSeries(data=exposures, index=dates)
            alpha_model_signals[alpha_model] = exposures_series

            candlestick_chart = CandlestickChart(prices_df,
                                                 title=str(alpha_model))
            candlestick_chart.add_highlight(exposures_series)
            self.document.add_element(
                ChartElement(candlestick_chart,
                             figsize=self.full_image_size,
                             dpi=self.dpi))

        candlestick_chart = CandlestickChart(prices_df,
                                             title="All models summary")
        for model, exposures_series in alpha_model_signals.items():
            candlestick_chart.add_highlight(exposures_series)

        self.document.add_element(
            ChartElement(candlestick_chart,
                         figsize=self.full_image_size,
                         dpi=self.dpi))
Beispiel #10
0
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())
    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 _plot_ticker_performance(self, ticker_name: str, performance):
        self.document.add_element(HeadingElement(level=2, text="PnL of {}".format(ticker_name)))

        chart = LineChart()
        legend = LegendDecorator(key="legend_decorator")
        line_colors = iter(("#add8e6", "#000000", "#fa8072"))

        for title, pnl_series in performance.iteritems():
            # Plot series only in case if it consist anything else then 0
            if (pnl_series != 0).any():
                data_series = DataElementDecorator(pnl_series, **{"color": next(line_colors)})
                legend.add_entry(data_series, title)
                chart.add_decorator(data_series)

        chart.add_decorator(legend)
        self.document.add_element(ChartElement(chart, figsize=self.full_image_size, dpi=self.dpi))
Beispiel #13
0
    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))
Beispiel #14
0
    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
Beispiel #15
0
    def add_models_implementation(self):
        """
        Add the source code of the classes (in order to simplify the implementation the full contents of the file
        containing.

        If there are multiple alpha models which are different instances of the same class, the implementation will be
        printed only once.
        """
        alpha_model_types = {
            alpha_model.__class__
            for alpha_model in self.alpha_models
        }
        for model_type in alpha_model_types:
            self.document.add_element(
                HeadingElement(
                    2, "Implementation of {}".format(model_type.__name__)))
            self.document.add_element(ParagraphElement("\n"))

            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))
Beispiel #16
0
    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))