Execute the following steps to backtest a strategy based on the Bollinger Bands.
- Import the libraries:
import backtrader as bt
import datetime
import pandas as pd
- The template of the strategy is presented:
class BBand_Strategy(bt.Strategy):
params = (('period', 20),
('devfactor', 2.0),)
def __init__(self):
# some code
def log(self, txt):
# some code
def notify_order(self, order):
# some code
def notify_trade(self, trade):
# some code
def next_open(self):
# some code
The __init__ block is defined as:
def __init__(self):
# keep track of close price in the series
self.data_close = self.datas[0].close
self.data_open = self.datas[0].open
# keep track of pending orders/buy price/buy commission
self.order = None
self.price = None
self.comm = None
# add Bollinger Bands indicator and track the buy/sell signals
self.b_band = bt.ind.BollingerBands(self.datas[0],
period=self.p.period,
devfactor=self.p.devfactor)
self.buy_signal = bt.ind.CrossOver(self.datas[0],
self.b_band.lines.bot)
self.sell_signal = bt.ind.CrossOver(self.datas[0],
self.b_band.lines.top)
The log block is defined as:
def log(self, txt):
dt = self.datas[0].datetime.date(0).isoformat()
print(f'{dt}, {txt}')
The notify_order block is defined as:
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return
if order.status in [order.Completed]:
if order.isbuy():
self.log(
f'BUY EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}'
)
self.price = order.executed.price
self.comm = order.executed.comm
else:
self.log(
f'SELL EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}'
)
elif order.status in [order.Canceled, order.Margin,
order.Rejected]:
self.log('Order Failed')
self.order = None
The notify_trade block is defined as:
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log(f'OPERATION RESULT --- Gross: {trade.pnl:.2f}, Net: {trade.pnlcomm:.2f}')
The next_open block is defined as:
def next_open(self):
if not self.position:
if self.buy_signal > 0:
size = int(self.broker.getcash() / self.datas[0].open)
self.log(f'BUY CREATED --- Size: {size}, Cash: {self.broker.getcash():.2f}, Open: {self.data_open[0]}, Close: {self.data_close[0]}')
self.buy(size=size)
else:
if self.sell_signal < 0:
self.log(f'SELL CREATED --- Size: {self.position.size}')
self.sell(size=self.position.size)
- Download the data:
data = bt.feeds.YahooFinanceData(
dataname='MSFT',
fromdate=datetime.datetime(2018, 1, 1),
todate=datetime.datetime(2018, 12, 31)
)
- Set up the backtest:
cerebro = bt.Cerebro(stdstats = False, cheat_on_open=True)
cerebro.addstrategy(BBand_Strategy)
cerebro.adddata(data)
cerebro.broker.setcash(10000.0)
cerebro.broker.setcommission(commission=0.001)
cerebro.addobserver(bt.observers.BuySell)
cerebro.addobserver(bt.observers.Value)
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='time_return')
- Run the backtest:
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
backtest_result = cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
- Plot the results:
cerebro.plot(iplot=True, volume=False)
The resulting graph is presented below:
The log is presented below:
We can see that the strategy managed to make money, even after accounting for commission costs. We now turn to an inspection of the analyzers.
- Run the following code to investigate different returns metrics:
print(backtest_result[0].analyzers.returns.get_analysis())
The output of the preceding line is as follows:
OrderedDict([('rtot', 0.06155731237239935),
('ravg', 0.00024622924948959743),
('rnorm', 0.06401530037885826),
('rnorm100', 6.401530037885826)])
- Create a plot of daily portfolio returns:
returns_dict = backtest_result[0].analyzers.time_return.get_analysis()
returns_df = pd.DataFrame(list(returns_dict.items()),
columns = ['report_date', 'return']) \
.set_index('report_date')
returns_df.plot(title='Portfolio returns')
Running the code results in the following plot:
The flat lines represent periods when we have no open positions.