예제 #1
0
    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)
예제 #2
0
    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)
예제 #3
0
    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)
예제 #4
0
    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)
예제 #5
0
    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)
예제 #6
0
    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)
예제 #7
0
    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()
예제 #8
0
    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))
예제 #9
0
    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)
예제 #10
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))
예제 #11
0
    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")
예제 #12
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
예제 #13
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)
예제 #14
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())