Quant500 Academy

2. Anatomy of a backtest with Backtrader

Level 12: Backtesting and Real Risk Metrics

Backtesting is a key component in the development of effective trading systems. It is carried out by reconstructing, with historical data, the trades that would have taken place in the past following the rules of a given strategy. The Backtrader library provides the tools to run this kind of test in Python.

The general approach has three steps: gather the data, define the backtesting strategy, and extract the performance statistics to compare them against a benchmark.

!pip install backtrader

Let us start with the canonical example: a moving average crossover.

import backtrader as bt
import yfinance as yf
import datetime

class MovingAverageCrossStrategy(bt.Strategy):
    params = (
        ('short_window', 40),
        ('long_window', 100),
    )

    def __init__(self):
        self.data_close = self.datas[0].close
        self.short_ma = bt.indicators.SimpleMovingAverage(
            self.data_close, period=self.params.short_window
        )
        self.long_ma = bt.indicators.SimpleMovingAverage(
            self.data_close, period=self.params.long_window
        )
        self.crossover = bt.indicators.CrossOver(self.short_ma, self.long_ma)

    def next(self):
        if self.crossover > 0:      # the short moving average crosses above
            self.buy()
        elif self.crossover < 0:    # the short moving average crosses below
            self.sell()

Two methods and you already have a strategy. It is worth understanding what each of them does.

The __init__ method runs exactly once, before the simulation begins. This is where you declare the indicators. And here is the first big difference from the naive backtest you wrote in Level 5 with pandas: when you write SimpleMovingAverage(self.data_close, period=40) you are not computing a number, you are declaring a line that Backtrader will fill in bar by bar. CrossOver returns +1 on the bar where the first line crosses above the second, -1 when it crosses below, and 0 the rest of the time.

The next method runs once per bar, in chronological order, and only has access to the information available up to and including that bar. This is what makes the engine event-driven and what protects you from look-ahead bias: by construction you cannot read the future, because the future has not been loaded into the lines yet.

One important detail: next does not start at the first bar of the history. Backtrader waits until every indicator has enough data. With a 100-period moving average, the first 99 bars are consumed by the warm-up period and the simulation effectively begins at bar 100.

Now the assembly. Cerebro is the engine that orchestrates everything: data, strategy, simulated broker and analyzers.

data_df = yf.download('AAPL', start='2020-01-01', end='2022-01-01')
data = bt.feeds.PandasData(dataname=data_df)

cerebro = bt.Cerebro()
cerebro.addstrategy(MovingAverageCrossStrategy)
cerebro.adddata(data)
cerebro.broker.set_cash(100000)

cerebro.addsizer(bt.sizers.FixedSize, stake=1000)
cerebro.broker.setcommission(commission=0.001)

print('Starting portfolio value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final portfolio value: %.2f' % cerebro.broker.getvalue())

cerebro.plot()

Notice that self.buy() takes no arguments: the size is decided by the sizer. With FixedSize and stake=1000, every order is for 1000 shares, come rain or shine. That is a risk management decision hidden in a single line, and we will come back to it in the next module.

Backtrader ships with no predefined strategies: it gives you a robust framework and built-in indicators so that you define the logic yourself. Within that framework you can implement moving average crossovers, mean reversion, momentum, Bollinger Bands, MACD, RSI, pairs trading, machine-learning-based strategies, portfolio rebalancing or trend following, among many others.

To go deeper into this lesson

The rest of Quant500

What you are learning, applied to the index every day:

The model portfolio, weekly in your inbox

Every Saturday, its week in one minute: how it went, the best and the worst, and which insiders bought with their own money. Plus, the day the model is rebuilt, which stocks come in and which go out. A simulation, not advice. At most one email a week, no ads, no account; one-click unsubscribe. Made by one person: Pol Gracia.

A model portfolio with simulated results, not a personalized recommendation. Your email is used for this only: privacy.

Behind the Academy and the rest of Quant500 there is one person: Pol Gracia Serrahima.