/ TRADING

Backtrader: Getting Started Backtesting

Backtrader is an open-source python framework for trading and backtesting. Backtrader allows you to focus on writing reusable trading strategies, indicators, and analyzers instead of having to spend time building infrastructure.

I think of Backtrader as a Swiss Army Knife for Python trading and backtesting. It supports live trading and quick analysis of trading strategies. I use Backtrader for my live trading and initial strategy testing, and then run my strategy through Zipline for further alpha factor analysis.

Backtrader also has great documentation and an active trading community. You can download the code from this post at the Analyzing Alpha Github.

Backtrader Installation

You’ll want to create a conda or pip environment, and then install the packages you’ll be using. I’m adding Pandas and SQLAlchemy as I’ll be using data from my local securities database. If you’re interested in building from source, check out the Backtrader installation guide.

conda create --name backtrader
conda activate backtrader
conda install pandas matplotlib
conda install -c anaconda sqlalchemy
pip install backtrader

Using Backtrader: A Quick Overview

Let’s understand how to use Backtrader. If you run into any questions, read the Backtrader quickstart. Most of what I’ve written about here can be found in there.

Developing and testing a trading strategy in Backtrader generally follows five steps:

  1. Initialize the engine
  2. Configure the broker
  3. Add the data
  4. Create the strategy
  5. Analyze the performance

Initialize the Engine

Below we import backtrader and then instantiate an instance of it using backtrader.Cerebro(). We then check to see if our program backtrader_initialize.py is the main programming running. If it isn’t and our program was imported into another program, the name wouldn’t be main, it would be __name__ == 'backtrader_initialize'.

Confgure the Broker

Instantiating backtrader using backtrader.Cerebro() creates a broker instance in the background for convenience. We set the starting balance to $1,337.00. If we didn’t set the cash, the default balance is 10k. While we only set the cash on our broker instance, the Backtrader broker is robust and supports different order types, slippage models, and commission schemes.

import backtrader as bt


if __name__ == '__main__':
    cerebro = bt.Cerebro()
    cerebro.broker.setcash(1337.0)

    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    cerebro.run()

    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

Add the Data

Backtrader provides a bunch of built-in data feed options and the ability to create your own. For instance, we can easily add Yahoo Finance data by adding feeds.YahooFinanceData.

Adding Data from Yahoo

data = bt.feeds.YahooFinanceData(dataname='AAPL',
                                  fromdate=datetime(2017, 1, 1),
                                  todate=datetime(2017, 12, 31))

Adding Yahoo CSV Data

We could have also added the Yahoo data from a CSV file.

data = btfeeds.YahooFinanceCSVData(dataname='yahoo_finance_aapl.csv')

Adding Data from Pandas

For those of you that follow my blog, you know that I enjoy using Python & Pandas. We can create a Pandas Datafeed in Backtrader by inheriting from the base class feed.DataBase.

An example using data from PostgreSQL and Pandas is shown in the Connors RSI strategy below.

class PandasData(feed.DataBase):
    '''
    The ``dataname`` parameter inherited from ``feed.DataBase`` is the pandas
    DataFrame
    '''

    params = (
        # Possible values for datetime (must always be present)
        #  None : datetime is the "index" in the Pandas Dataframe
        #  -1 : autodetect position or case-wise equal name
        #  >= 0 : numeric index to the colum in the pandas dataframe
        #  string : column name (as index) in the pandas dataframe
        ('datetime', None),

        # Possible values below:
        #  None : column not present
        #  -1 : autodetect position or case-wise equal name
        #  >= 0 : numeric index to the colum in the pandas dataframe
        #  string : column name (as index) in the pandas dataframe
        ('open', -1),
        ('high', -1),
        ('low', -1),
        ('close', -1),
        ('volume', -1),
        ('openinterest', -1),
    )

Backtrader Strategy Examples

Now that Cerebro has data let’s create a few strategies. Strategies generally follow a four-step process:

  1. Initiation
  2. Pre-processing
  3. Processing
  4. Post-processing

Pre-processing occurs because we need to process 15 bars (period=15) before we can use our simple moving average indicator. Once the pre-processing has been completed, processing will start and will call next.

class MyStrategy(bt.Strategy):

    def __init__(self):  # Initiation
        self.sma = btind.SimpleMovingAverage(period=15)  # Processing

    def next(self):  # Processing
        if self.sma > self.data.close:
            # Do something
            pass

        elif self.sma < self.data.close: # Post-processing
            # Do something else
            pass

Dual Moving Average (DMA) Strategy

If you’re going to follow along with me, you’ll need to install the requests module to grab data from Yahoo Finance.

conda install requests

The first strategy is a simple dual moving average (DMA) strategy trading Apple (AAPL) for 2017. We start with $1,337.00 and end with $1,354.77. As you can see, the code to create a DMA strategy in Backtrader is more simple than in the Zipline DMA strategy, but as stated previously, the return analysis is more simplistic, too.

from datetime import datetime
import backtrader as bt


class SmaCross(bt.SignalStrategy):
    def __init__(self):
        sma1, sma2 = bt.ind.SMA(period=10), bt.ind.SMA(period=20)
        crossover = bt.ind.CrossOver(sma1, sma2)
        self.signal_add(bt.SIGNAL_LONG, crossover)

if __name__ == '__main__':
    cerebro = bt.Cerebro()
    cerebro.addstrategy(SmaCross)
    cerebro.broker.setcash(1337.0)
    cerebro.broker.setcommission(commission=0.001)

    data = bt.feeds.YahooFinanceData(dataname='AAPL',
                                     fromdate=datetime(2017, 1, 1),
                                     todate=datetime(2017, 12, 31))
    cerebro.adddata(data)
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    cerebro.run()
    print('Ending Portfolio Value: %.2f' % cerebro.broker.getvalue())
    cerebro.plot()

Backtrader DMA Strategy Results

Donchain Channels Strategy

Donchian channels are not implemented in Backtrader, so we need to create an indicator by inheriting from backtrader.Indicator. Donchain channels are named after Richard Donchain and can be used in a variety of ways, but are most often used to buy breakouts. We inherit bt.Indicator to create the DonchainChannels class, and code up the logic. We buy when the price breaks through the 20-period high and sell when the price drops below the 20-period low. We can also add graphing through plotlines as seen below.

from datetime import datetime
import backtrader as bt


class DonchianChannels(bt.Indicator):
    '''
    Params Note:
      - `lookback` (default: -1)
        If `-1`, the bars to consider will start 1 bar in the past and the
        current high/low may break through the channel.
        If `0`, the current prices will be considered for the Donchian
        Channel. This means that the price will **NEVER** break through the
        upper/lower channel bands.
    '''

    alias = ('DCH', 'DonchianChannel',)

    lines = ('dcm', 'dch', 'dcl',)  # dc middle, dc high, dc low
    params = dict(
        period=20,
        lookback=-1,  # consider current bar or not
    )

    plotinfo = dict(subplot=False)  # plot along with data
    plotlines = dict(
        dcm=dict(ls='--'),  # dashed line
        dch=dict(_samecolor=True),  # use same color as prev line (dcm)
        dcl=dict(_samecolor=True),  # use same color as prev line (dch)
    )

    def __init__(self):
        hi, lo = self.data.high, self.data.low
        if self.p.lookback:  # move backwards as needed
            hi, lo = hi(self.p.lookback), lo(self.p.lookback)

        self.l.dch = bt.ind.Highest(hi, period=self.p.period)
        self.l.dcl = bt.ind.Lowest(lo, period=self.p.period)
        self.l.dcm = (self.l.dch + self.l.dcl) / 2.0  # avg of the above


class MyStrategy(bt.Strategy):
    def __init__(self):
        self.myind = DonchianChannels()

    def next(self):
        if self.data[0] > self.myind.dch[0]:
            self.buy()
        elif self.data[0] < self.myind.dcl[0]:
            self.sell()

if __name__ == '__main__':
    cerebro = bt.Cerebro()
    cerebro.addstrategy(MyStrategy)
    cerebro.broker.setcash(1337.0)
    cerebro.broker.setcommission(commission=0.001)

    data = bt.feeds.YahooFinanceData(dataname='AAPL',
                                     fromdate=datetime(2017, 1, 1),
                                     todate=datetime(2017, 12, 31))
    cerebro.adddata(data)
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    cerebro.run()
    print('Ending Portfolio Value: %.2f' % cerebro.broker.getvalue())
    cerebro.plot()

Backtrader Donchain Strategy Results

Connors RSI Strategy Using Pandas

Created by Larry Connors, Connors RSI Strategy three different indicators together. We will sell if the Connors RSI hits 90 and buy if it reaches 10. If you want to follow along with this strategy, you’ll need to create an equity database and import the stock data.

from datetime import datetime
import backtrader as bt
import pandas as pd
import sqlalchemy
import setup_psql_environment
from models import Security, SecurityPrice


class Streak(bt.ind.PeriodN):
    '''
    Keeps a counter of the current upwards/downwards/neutral streak
    '''
    lines = ('streak',)
    params = dict(period=2)  # need prev/cur days (2) for comparisons

    curstreak = 0

    def next(self):
        d0, d1 = self.data[0], self.data[-1]

        if d0 > d1:
            self.l.streak[0] = self.curstreak = max(1, self.curstreak + 1)
        elif d0 < d1:
            self.l.streak[0] = self.curstreak = min(-1, self.curstreak - 1)
        else:
            self.l.streak[0] = self.curstreak = 0


class ConnorsRSI(bt.Indicator):
    '''
    Calculates the ConnorsRSI as:
        - (RSI(per_rsi) + RSI(Streak, per_streak) + PctRank(per_rank)) / 3
    '''
    lines = ('crsi',)
    params = dict(prsi=3, pstreak=2, prank=100)

    def __init__(self):
        # Calculate the components
        rsi = bt.ind.RSI(self.data, period=self.p.prsi)
        streak = Streak(self.data)
        rsi_streak = bt.ind.RSI(streak.data, period=self.p.pstreak)
        prank = bt.ind.PercentRank(self.data, period=self.p.prank)

        # Apply the formula
        self.l.crsi = (rsi + rsi_streak + prank) / 3.0


class MyStrategy(bt.Strategy):
    def __init__(self):
        self.myind = ConnorsRSI()

    def next(self):
        if self.myind.crsi[0] <= 10:
            self.buy()
        elif self.myind.crsi[0] >= 90:
            self.sell()

if __name__ == '__main__':
    cerebro = bt.Cerebro()
    cerebro.broker.setcash(1337.0)
    cerebro.broker.setcommission(commission=0.001)

    db = setup_psql_environment.get_database()
    session = setup_psql_environment.get_session()

    query = session.query(SecurityPrice).join(Security). \
        filter(Security.ticker == 'AAPL'). \
        filter(SecurityPrice.date >= '2017-01-01'). \
        filter(SecurityPrice.date <= '2017-12-31').statement
    dataframe = pd.read_sql(query, db, index_col='date', parse_dates=['date'])
    dataframe = dataframe[['adj_open',
                           'adj_high',
                           'adj_low',
                           'adj_close',
                           'adj_volume']]
    dataframe.columns = columns = ['open', 'high', 'low', 'close', 'volume']
    dataframe['openinterest'] = 0
    dataframe.sort_index(inplace=True)

    data = bt.feeds.PandasData(dataname=dataframe)

    cerebro.adddata(data)
    cerebro.addstrategy(MyStrategy)
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    cerebro.run()
    print('Ending Portfolio Value: %.2f' % cerebro.broker.getvalue())
    cerebro.plot()

Backtrader Connors RSI Strategy Results

Backtrader Plotting & Visualization

Backtrader enables visual strategy analysis by using matplotlib to plot the results. It’s easy to craft a strategy and quickly plot it using cerebro.plot() before putting the strategy through further analysis in Zipline. There are multiple options when plotting in Backtrader.

Backtrader Alternatives

While I’m not an expert on the following tools, I’ve heard good things about QuantConnect and QuantRocket from the community.

If you come from the Windows world, Quantconnect’s Lean is a trading platform developed in C# that you can run locally and is making significant progress.

If you’re looking for an online hosted trading platform and are not worried about privacy concerns, QuantConnect and QuantRocket both may fit your needs.

leo

Leo Smigel

Based in Pittsburgh, Analyzing Alpha is a blog by Leo Smigel exploring what works in the markets.