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 backtesting. It supports live trading and quick analysis of trading strategies. I used to use Backtrader for my live trading and backtesting.
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 there.
Developing and testing a trading strategy in Backtrader generally follows five steps:
- Initialize the engine
- Configure the broker
- Add the data
- Create the strategy
- 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.
Configure 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 it from the base class feed.DataBase.
An example using data from PostgreSQL and Pandas is shown in the Connors RSI indicator:
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:
- Initiation
- Pre-processing
- Processing
- 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()

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()

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 this strategy, you’ll need to create an equity database and import the stock data using Python.
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 enables visual strategy analysis by using matplotlib to visualize 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.
You can see how I reviewed them and more in the best python trading tools.