Optimizing Strategy Backtesting in Python with Backtrader

Pretty often strategies you backtest have quite a lot of parameters and it’s pretty hard to find out which parameters work the best. Of course, you can change parameters manually and run backtest multiple times. But there are better ways to do that. In this article, I will show you how you run multiple backtests in Backtrader in Python and find the best working parameters configuration.

First, let’s import the libraries we need for this example:

import backtrader as bt
import backtrader.analyzers as btanalyzers
import pandas as pd
import matplotlib
from datetime import datetime
import qgrid

Next, we have to create a class for our strategy. To run parameter optimization in Backtrader you have to include parameters in class and use it in your indicator/signals calculations.

To add parameters you can just add a list of parameters with default values to the class like that:

params = (
        ('fast_length', 10),
        ('slow_length', 50)
    )

And then use these parameters with self.params prefix when computing your indicators:

def __init__(self):
        ma_fast = bt.ind.SMA(period = self.params.fast_length)
        ma_slow = bt.ind.SMA(period = self.params.slow_length)
        
        self.crossover = bt.ind.CrossOver(ma_fast, ma_slow)

Full class definition looks like that:

class MaCrossStrategy(bt.Strategy):

    params = (
        ('fast_length', 10),
        ('slow_length', 50)
    )
    
    def __init__(self):
        ma_fast = bt.ind.SMA(period = self.params.fast_length)
        ma_slow = bt.ind.SMA(period = self.params.slow_length)
        
        self.crossover = bt.ind.CrossOver(ma_fast, ma_slow)

    def next(self):
        if not self.position:
            if self.crossover > 0: 
                self.buy()
        elif self.crossover < 0: 
            self.close()

After doing that you can start adding you strategy to engine following way adding parameters:

cerebro.addstrategy(MaCrossStrategy, fast_length = 10, slow_length = 20)

After that, you can write a bunch of nested loops and inside it add strategy and run the engine multiple times to optimize your strategy parameters. But there is a better way to do that. You can add your strategy to the engine with optstrategy method specifying the range of parameters you want to test. It looks like that:

strats = cerebro.optstrategy(
        MaCrossStrategy,
        fast_length = range(1, 11), 
        slow_length = range(25, 76, 5))

When you add strategy this way Backtrader will understand that you want to optimize your strategy and will run the engine multiple times for all parameters combination.

Here is an example of the entire code for running it:

cerebro = bt.Cerebro()

data = bt.feeds.YahooFinanceData(dataname = 'AAPL', fromdate = datetime(2010, 1, 1), todate = datetime(2020, 1, 1))
cerebro.adddata(data)

strats = cerebro.optstrategy(
        MaCrossStrategy,
        fast_length = range(1, 11), 
        slow_length = range(25, 76, 5))


cerebro.broker.setcash(1000000.0)

cerebro.addsizer(bt.sizers.PercentSizer, percents = 10)
cerebro.addanalyzer(btanalyzers.SharpeRatio, _name = "sharpe")
cerebro.addanalyzer(btanalyzers.DrawDown, _name = "drawdown")
cerebro.addanalyzer(btanalyzers.Returns, _name = "returns")

back = cerebro.run()

So we saved the result of the run to back variable, now we have to parse it. The result of optimization is simply a list of backtesting results, so it’s pretty easy to parse it to a convenient format.

I’ll use Python’s list comprehension to extract parameters and metrics I need from optimizations results:

par_list = [[x[0].params.fast_length, 
             x[0].params.slow_length,
             x[0].analyzers.returns.get_analysis()['rnorm100'], 
             x[0].analyzers.drawdown.get_analysis()['max']['drawdown'],
             x[0].analyzers.sharpe.get_analysis()['sharperatio']
            ] for x in back]

This will output me list of lists of params and metrics. Next I will transform them to a Pandas DataFrame:

par_df = pd.DataFrame(par_list, columns = ['length_fast', 'length_slow', 'return', 'dd', 'sharpe'])

Now all results are in a nice DataFrame so you can view them with qgrid for example:

qgrid.show_grid(par_df)

This will output you a nice table:

In this table, you can view all your backtesting results, filter, and sort them. So it will be pretty easy to spot results that work the best for you.

Leave a Reply

Your email address will not be published. Required fields are marked *