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 backtraderLet 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
- Backtestin the glossary