Plot BacktestΒΆ

visualize a backtest, including the value investent, asset price and any signals. see Backtest Example

Python source code: ../../../src/boatwright/Visualization/plot_backtest.py

from ..Backtest import Backtest
from .candles_and_volume import plot_candles, plot_volume
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import datetime as dt


def plot_backtest(backtest:Backtest, figsize=(10,4), candles:bool=True):
    """
    plots the results of a backtest, including AUM over time, asset amounts, price and buy/sell markers, volume, and any additional signals specified by strategy.plot_info()
    
    :param backtest: the backtest object to plot
    :param figsize: the size of the figure to plot. Default is (10,4)
    :param candles: whether to plot candles or just a line plot for price. Default is True

    """
    
    # dates = [dt.datetime.fromtimestamp(ts) for ts in backtest.data["timestamp"]]
    # dates = backtest.data["datetime"]
    dates = mdates.date2num(backtest.data["datetime"])
    figs = {}
    axs = {}

    # AUM and buy/hold reference
    try:
        fig, ax = plt.subplots(figsize=figsize)
        figs["aum"] = fig
        axs["aum"] = ax
        
        ax.plot(dates, backtest.data["aum"], label="aum", color="blue")
        buy_and_hold = (backtest.broker.starting_aum / backtest.data["close"].iloc[0]) * backtest.data["close"]
        ax.plot(dates, buy_and_hold, label="buy & hold", color="black")
        ax.set_ylabel(backtest.broker.quote_symbol)
        ax.legend(loc="best")
    except:
        pass

    # Assets
    try:
        fig = plt.figure(figsize=figsize)
        ax = fig.add_subplot(sharex=axs["aum"])
        figs["assets"] = fig
        axs["assets"] = ax
        quote_fraction = backtest.data["quote_amount"] / backtest.data["aum"]
        base_fraction = backtest.data["base_amount"] * backtest.data["close"] / backtest.data["aum"]
        ax.plot(dates, quote_fraction, color="black", label=backtest.strategy.broker.quote_symbol)
        ax.plot(dates, base_fraction, color="blue", label=backtest.strategy.symbol)
        ax.set_ylabel("fraction")
        ax.legend(loc="best")
    except:
        pass

    # Price, i.e. candles
    try:
        fig = plt.figure(figsize=figsize)
        ax = fig.add_subplot(sharex=axs["aum"])
        figs["price"] = fig
        axs["price"] = ax
        if candles: plot_candles(backtest.data, ax=ax)
        else: ax.plot(dates, backtest.data["close"], label=backtest.strategy.symbol, color="black")
        ax.set_ylabel(f"{backtest.strategy.symbol} [{backtest.broker.quote_symbol}]")
    except:
        pass

    # buy / sells markers
    try:
        buy_orders = [o for o in backtest.broker.orders["FILLED"].values() if o.side=="BUY" and (o.type == "MARKET" or o.type == "LIMIT")]
        buy_dates = mdates.date2num([order.time_closed for order in buy_orders])
        buy_prices = [order.info["avg_price"] for order in buy_orders]
        ax.scatter(buy_dates, buy_prices, color="lime", edgecolors="black", marker="^", label="buy", zorder=5, alpha=0.75)

        sell_orders = [o for o in backtest.broker.orders["FILLED"].values() if o.side=="SELL" and (o.type == "MARKET" or o.type == "LIMIT")]
        sell_dates = mdates.date2num([order.time_closed for order in sell_orders])
        sell_prices = [order.info["avg_price"] for order in sell_orders]
        ax.scatter(sell_dates, sell_prices, color="red", edgecolors="black", marker="v", label="sell", zorder=5, alpha=0.75)
        ax.legend(loc="upper left")
    except:
        pass

    # Volume
    try:
        fig = plt.figure(figsize=figsize)
        ax = fig.add_subplot(sharex=axs["aum"])
        figs["volume"] = fig
        axs["volume"] = ax
        plot_volume(backtest.data, ax=ax)
        ax.set_ylabel(f"volume")
    except:
        pass

    # plot any additional signals as specified by strategy.plot_info()
    for key, info in backtest.strategy.plot_info().items():
        if not "ax_id" in info.keys():
            info["ax_id"] = key
        if info["ax_id"] in axs.keys():
            ax = axs[info["ax_id"]]
        else:
            fig = plt.figure(figsize=figsize)
            ax = fig.add_subplot(sharex=axs["aum"])
            figs[info["ax_id"]] = fig
            axs[info["ax_id"]] = ax
        try:
            ax = axs[info["ax_id"]]
            plot_kwargs = info
            plot_kwargs.pop("ax_id")
            if not "label" in plot_kwargs.keys(): plot_kwargs["label"] = key
            ax.plot(dates, backtest.data[key], **plot_kwargs)
            ax.legend(loc="upper left")
        except:
            pass

    # make the dates axes pretty
    for a in axs.values():
        a.grid(True)
        a.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d, %H:%M'))
        for label in a.get_xticklabels(which='major'):
            label.set(rotation=20, horizontalalignment='right')
    
    for f in figs.values():
        f.tight_layout()
    
    return figs, axs