コード例 #1
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)
コード例 #2
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)
コード例 #3
0
    def _create_performance_tables(
            self, performance_df: QFDataFrame) -> List[DFTable]:
        """ Create a formatted DFTable out of the performance_df data frame. """
        numeric_columns = [
            col for col in performance_df.columns
            if is_numeric_dtype(performance_df[col])
        ]
        performance_df[numeric_columns] = performance_df[
            numeric_columns].applymap(lambda x: '{:,.0f}'.format(x))
        performance_df = performance_df.set_index("Asset").sort_index()

        # Divide the performance df into a number of data frames, so that each of them contains up to
        # self.max_col_per_page columns, but keep the first column of the original df in all of them
        split_dfs = np.array_split(performance_df,
                                   np.ceil(performance_df.num_of_columns /
                                           self._max_columns_per_page),
                                   axis=1)
        df_tables = [
            DFTable(df.reset_index(),
                    css_classes=[
                        'table', 'shrink-font', 'right-align',
                        'wide-first-column'
                    ]) for df in split_dfs
        ]
        return df_tables
コード例 #4
0
    def _get_distribution_summary_table(self, scenarios_results: SimpleReturnsSeries) -> DFTable:
        rows = []
        percentage_list = [0.05, 0.1, 0.2, 0.3]
        for percentage in percentage_list:
            rows.append(("{:.0%} Tail".format(percentage),
                         "{:.2%}".format(np.quantile(scenarios_results, percentage))))

        rows.append(("50%", "{:.2%}".format(np.quantile(scenarios_results, 0.5))))

        for percentage in reversed(percentage_list):
            rows.append(("{:.0%} Top".format(percentage),
                         "{:.2%}".format(np.quantile(scenarios_results, (1.0 - percentage)))))

        table = DFTable(data=QFDataFrame.from_records(rows, columns=["Measure", "Value"]),
                        css_classes=['table', 'left-align'])
        table.add_columns_classes(["Measure"], 'wide-column')

        return table
コード例 #5
0
    def _add_open_positions_table(self):
        open_positions_dict = self._portfolio.open_positions_dict

        contracts = open_positions_dict.keys()

        # Return a readable name for each ticker (name property for FutureTickers and ticker for Tickers)
        tickers = [
            self._portfolio.contract_ticker_mapper.contract_to_ticker(
                contract, strictly_to_specific_ticker=False)
            for contract in contracts
        ]
        tickers = [ticker.name for ticker in tickers]

        specific_tickers = [
            self._portfolio.contract_ticker_mapper.contract_to_ticker(
                contract).ticker for contract in contracts
        ]

        # Get the information whether it is a long or short position
        directions = [
            open_positions_dict[contract].direction() for contract in contracts
        ]
        directions = [
            "LONG" if direction == 1 else "SHORT" for direction in directions
        ]

        # Get the total exposure and market value for each open position
        total_exposures = [
            "{:,.2f}".format(open_positions_dict[contract].total_exposure())
            for contract in contracts
        ]
        pnls = [
            "{:,.2f}".format(open_positions_dict[contract].unrealised_pnl)
            for contract in contracts
        ]

        # Get the time of opening the positions
        start_time = [
            open_positions_dict[contract].start_time.date()
            for contract in contracts
        ]

        data = {
            "Tickers name": tickers,
            "Specific ticker": specific_tickers,
            "Direction": directions,
            "Total Exposure": total_exposures,
            "PnL": pnls,
            "Position Creation": start_time
        }

        table = DFTable(QFDataFrame.from_dict(data),
                        css_classes=['table', 'left-align'])
        self.document.add_element(table)
コード例 #6
0
    def _get_monte_carlos_simulator_outputs(self, scenarios_df: PricesDataFrame, total_returns: SimpleReturnsSeries) \
            -> DFTable:
        _, all_scenarios_number = scenarios_df.shape
        rows = []

        # Add the Median Return value
        median_return = np.median(total_returns)
        rows.append(("Median Return", "{:.2%}".format(median_return)))

        # Add the Mean Return value
        mean_return = total_returns.mean()
        rows.append(("Mean Return", "{:.2%}".format(mean_return)))

        trade_returns = QFSeries(data=[trade.percentage_pnl for trade in self.trades])
        sample_len = int(self._average_number_of_trades_per_year())
        std = trade_returns.std()
        expectation_adj_series = np.ones(sample_len) * (trade_returns.mean() - 0.5 * std * std)
        expectation_adj_series = SimpleReturnsSeries(data=expectation_adj_series)
        expectation_adj_series = expectation_adj_series.to_prices(suggested_initial_date=0)
        mean_volatility_adjusted_return = expectation_adj_series.iloc[-1] / expectation_adj_series.iloc[0] - 1.0
        rows.append(("Mean Volatility Adjusted Return", "{:.2%}".format(mean_volatility_adjusted_return)))

        # Add the Median Drawdown
        max_drawdowns = max_drawdown(scenarios_df)
        median_drawdown = np.median(max_drawdowns)
        rows.append(("Median Maximum Drawdown", "{:.2%}".format(median_drawdown)))

        # Add the Median Return / Median Drawdown
        rows.append(("Return / Drawdown", "{:.2f}".format(median_return / median_drawdown)))

        # Probability, that the return will be > 0
        scenarios_with_positive_result = total_returns[total_returns > 0.0].count()
        probability = scenarios_with_positive_result / all_scenarios_number
        rows.append(("Probability of positive return", "{:.2%}".format(probability)))

        table = DFTable(data=QFDataFrame.from_records(rows, columns=["Measure", "Value"]),
                        css_classes=['table', 'left-align'])
        table.add_columns_classes(["Measure"], 'wide-column')

        return table
コード例 #7
0
    def _get_chances_of_dropping_below_table(self, scenarios_df: PricesDataFrame) -> DFTable:
        _, all_scenarios_number = scenarios_df.shape
        rows = []

        crop_table = False
        for percentage in np.linspace(0.1, 0.9, 9):
            # Count number of scenarios, whose returns at some point of time dropped below the percentage * initial
            # value
            _, scenarios_above_percentage = scenarios_df.where(scenarios_df > (1.0 - percentage)).dropna(axis=1).shape
            probability = (all_scenarios_number - scenarios_above_percentage) / all_scenarios_number

            rows.append(("{:.0%}".format(percentage), "{:.2%}".format(probability)))

            if crop_table is True:
                break
            elif probability < 0.1:
                crop_table = True

        table = DFTable(QFDataFrame.from_records(rows, columns=["Chances of dropping below", "Probability"]),
                        css_classes=['table', 'left-align'])
        table.add_columns_classes(["Chances of dropping below"], 'wide-column')
        return table
コード例 #8
0
    def _add_open_positions_table(self):
        open_positions_dict = self._portfolio.open_positions_dict
        tickers = open_positions_dict.keys()

        # Get the information whether it is a long or short position
        directions = [open_positions_dict[t].direction() for t in tickers]
        directions = [
            "LONG" if direction == 1 else "SHORT" for direction in directions
        ]

        # Get the total exposure and market value for each open position
        total_exposures = [
            "{:,.2f}".format(open_positions_dict[t].total_exposure())
            for t in tickers
        ]
        pnls = [
            "{:,.2f}".format(open_positions_dict[t].unrealised_pnl)
            for t in tickers
        ]

        # Get the time of opening the positions
        start_time = [
            open_positions_dict[t].start_time.date() for t in tickers
        ]

        data = {
            "Tickers name": [t.name for t in tickers],
            "Specific ticker": tickers,
            "Direction": directions,
            "Total Exposure": total_exposures,
            "PnL": pnls,
            "Position Creation": start_time
        }

        table = DFTable(QFDataFrame.from_dict(data),
                        css_classes=['table', 'left-align'])
        self.document.add_element(table)
コード例 #9
0
    def _create_performance_contribution_tables(
            self, performance_df: QFDataFrame) -> List[DFTable]:
        """
        Create a list of DFTables with assets names in the index and different years / months in columns, which contains
        details on the performance contribution for each asset.
        """
        # Create a QFSeries which contains the initial amount of cash in the portfolio for each year / month
        numeric_columns = [
            col for col in performance_df.columns
            if is_numeric_dtype(performance_df[col])
        ]
        portfolio_values = performance_df[numeric_columns].sum().shift(
            fill_value=self._initial_cash).cumsum()
        performance_df[numeric_columns] = performance_df[
            numeric_columns] / portfolio_values[numeric_columns]

        # Add category column and aggregate data accordingly
        ticker_name_to_category = {
            t.name: category
            for t, category in self._ticker_to_category.items()
        }
        performance_df["Category"] = performance_df["Asset"].apply(
            lambda t: ticker_name_to_category[t])
        all_categories = list(set(ticker_name_to_category.values()))
        performance_df = performance_df.sort_values(by=["Category", "Asset"])
        performance_df = performance_df.groupby("Category").apply(
            lambda d: pd.concat([
                PricesDataFrame({
                    **{
                        "Asset": [d.name],
                        "Category": [d.name]
                    },
                    **{c: [d[c].sum()]
                       for c in numeric_columns}
                }), d
            ],
                                ignore_index=True)).drop(columns=["Category"])

        # Add the Total Performance row (divide by 2 as the df contains already aggregated data for each group)
        total_sum_row = performance_df[numeric_columns].sum() / 2
        total_sum_row["Asset"] = "Total Performance"
        performance_df = performance_df.append(total_sum_row,
                                               ignore_index=True)

        # Format the rows using the percentage formatter
        performance_df[numeric_columns] = performance_df[
            numeric_columns].applymap(lambda x: '{:.2%}'.format(x))

        # Divide the performance dataframe into a number of dataframes, so that each of them contains up to
        # self._max_columns_per_page columns
        split_dfs = np.array_split(performance_df.set_index("Asset"),
                                   np.ceil(
                                       (performance_df.num_of_columns - 1) /
                                       self._max_columns_per_page),
                                   axis=1)
        df_tables = [
            DFTable(df.reset_index(),
                    css_classes=[
                        'table', 'shrink-font', 'right-align',
                        'wide-first-column'
                    ]) for df in split_dfs
        ]

        # Get the indices of rows, which contain category info
        category_indices = performance_df[performance_df["Asset"].isin(
            all_categories)].index

        for df_table in df_tables:
            # Add table formatting, highlight rows showing the total contribution of the given category
            df_table.add_rows_styles(
                category_indices, {
                    "font-weight": "bold",
                    "font-size": "0.95em",
                    "background-color": "#cbd0d2"
                })
            df_table.add_rows_styles(
                [performance_df.index[-1]], {
                    "font-weight": "bold",
                    "font-size": "0.95em",
                    "background-color": "#b9bcbd"
                })
        return df_tables
コード例 #10
0
    def _add_stats_table(self):
        statistics = []  # type: List[Tuple]

        def append_to_statistics(measure_description: str, function: Callable, trades_containers,
                                 percentage_style: bool = False):
            style_format = "{:.2%}" if percentage_style else "{:.2f}"
            returned_values = (function(tc) for tc in trades_containers)
            returned_values = (value if is_finite_number(value) else 0.0 for value in returned_values)
            statistics.append((measure_description, *(style_format.format(val) for val in returned_values)))

        # Prepare trades data frame, used to generate all statistics
        trades_df = QFDataFrame.from_records(
            data=[(t.start_time, t.end_time, t.percentage_pnl, t.direction) for t in self.trades],
            columns=["start time", "end time", "percentage pnl", "direction"]
        )

        # In case if the initial risk is not set all the return statistic will be computed using the percentage pnl,
        # otherwise the r_multiply = percentage pnl / initial risk is used
        unit = "%" if self.initial_risk is None else "R"
        trades_df["returns"] = trades_df["percentage pnl"] if self.initial_risk is None \
            else trades_df["percentage pnl"] / self.initial_risk

        # Filter out only long and only
        long_trades_df = trades_df[trades_df["direction"] > 0]
        short_trades_df = trades_df[trades_df["direction"] < 0]
        all_dfs = [trades_df, long_trades_df, short_trades_df]

        append_to_statistics("Number of trades", len, all_dfs)
        append_to_statistics("% of trades number", lambda df: len(df) / len(trades_df) if len(trades_df) > 0 else 0,
                             all_dfs, percentage_style=True)

        period_length_in_years = Timedelta(self.end_date - self.start_date) / Timedelta(days=1) / DAYS_PER_YEAR_AVG
        append_to_statistics("Avg number of trades per year", lambda df: len(df) / period_length_in_years, all_dfs)
        append_to_statistics("Avg number of trades per year per asset",
                             lambda df: len(df) / period_length_in_years / self.nr_of_assets_traded, all_dfs)

        def percentage_of_positive_trades(df: QFDataFrame):
            return len(df[df["returns"] > 0]) / len(df) if len(df) > 0 else 0.0
        append_to_statistics("% of positive trades", percentage_of_positive_trades, all_dfs, percentage_style=True)

        def percentage_of_negative_trades(df: QFDataFrame):
            return len(df[df["returns"] < 0]) / len(df) if len(df) > 0 else 0.0
        append_to_statistics("% of negative trades", percentage_of_negative_trades, all_dfs, percentage_style=True)

        def avg_trade_duration(df: QFDataFrame):
            trades_duration = (df["end time"] - df["start time"]) / Timedelta(days=1)
            return trades_duration.mean()
        append_to_statistics("Average trade duration [days]", avg_trade_duration, all_dfs)

        append_to_statistics("Average trade return [{}]".format(unit), lambda df: df["returns"].mean(), all_dfs,
                             percentage_style=(self.initial_risk is None))
        append_to_statistics("Std trade return [{}]".format(unit), lambda df: df["returns"].std(), all_dfs,
                             percentage_style=(self.initial_risk is None))

        def avg_positive_trade_return(df: QFDataFrame):
            positive_trades = df[df["returns"] > 0]
            return positive_trades["returns"].mean()
        append_to_statistics("Average positive return [{}]".format(unit), avg_positive_trade_return, all_dfs,
                             percentage_style=(self.initial_risk is None))

        def avg_negative_trade_return(df: QFDataFrame):
            negative_trades = df[df["returns"] < 0]
            return negative_trades["returns"].mean()
        append_to_statistics("Average negative return [{}]".format(unit), avg_negative_trade_return, all_dfs,
                             percentage_style=(self.initial_risk is None))

        append_to_statistics("Best trade return [{}]".format(unit), lambda df: df["returns"].max(), all_dfs,
                             percentage_style=(self.initial_risk is None))
        append_to_statistics("Worst trade return [{}]".format(unit), lambda df: df["returns"].min(), all_dfs,
                             percentage_style=(self.initial_risk is None))

        append_to_statistics("SQN (per trade) [{}]".format(unit), lambda df: sqn(df["returns"]), all_dfs,
                             percentage_style=(self.initial_risk is None))
        append_to_statistics("SQN (per 100 trades) [{}]".format(unit), lambda df: sqn_for100trades(df["returns"]),
                             all_dfs, percentage_style=(self.initial_risk is None))

        def sqn_per_year(returns: QFSeries):
            sqn_per_year_value = sqn(returns) * sqrt(avg_nr_of_trades_per1y(returns, self.start_date, self.end_date))
            return sqn_per_year_value
        append_to_statistics("SQN (per year) [{}]".format(unit), lambda df: sqn_per_year(df["returns"]), all_dfs,
                             percentage_style=(self.initial_risk is None))

        statistics_df = QFDataFrame.from_records(statistics, columns=["Measure", "All trades", "Long trades",
                                                                      "Short trades"])
        table = DFTable(statistics_df, css_classes=['table', 'left-align'])
        table.add_columns_classes(["Measure"], 'wide-column')
        self.document.add_element(table)