Backtrader: First Script

Now that we have our environment setup, it time to write our first script!

Scope

This tutorial aims to set up a simple indicator based strategy using as simple code as possible. I will try to avoid some more advanced concepts found in the documentation and Python in general. For example lines such as:

if __name__ == '__main__’:
will not be included as I feel that beginners would need to spend time googling it and detracting from the objective which is getting a functional working strategy (even if some professional programmers may scoff at the code quality).
 
Backtesting Scope:
For the sake of simplicity, the tutorial also shall not include account commissions, spreads or other more advanced backtesting considerations such as benchmarking, optimization and trade analysis. I have also chosen to leave out printing / logging from this example to keep the code as spartan as possible. (Although I do think logging / printing is important for debugging once things go get more complex!)

The Strategy

At the time of writing we have been in a long long bull market. When trading equities in such a market, one might reasonably only wish to go long. The reason for this being so they are on the right side of the longer term underlying upwards momentum.
 
With that in mind, let’s have a bit of fun implementing a long only strategy that will go long when a simple daily RSI indicator is oversold and then hold until the RSI reaches the overbought level.
 
Entry
  • When RSI < 30
Exit
  • When RSI > 70
Trade Management and Position Sizing
  • No trade management shall be implemented. No scaling in / out. Just simple buying and selling with a single open position at a time.
  • Position size wise, we will keep things simple and just buy / sell 100 shares at a time without doing any calculations to see if we have enough cash for position size. (if we don’t have enough cash, backtrader is smart enough to reject the order)
Indicator Settings
  • Period = 21
  • Lets use a longer look back period than the default 14. In theory this should result in less false signals and price should have to come down / rise much further before it is considered overbought / over sold.

Before we start

If you download the full script, you will notice a mighty license at the top of the code. To explain it is simple terms, an MIT license lets people do anything they want with the code as long as they provide attribution back to backtest-rookies and don’t hold us liable. It is designed to be a simple and permissive as possible.

Requirements

This script pulls data online from Yahoo. I think this simplifies things for a first script as you will not need to download your own data. However, because of this, the script requires backtrader version: 1.9.49.116 or greater due to recent changes in the Yahoo API. In order to check the  version of backtrader you are using you have a few options. First you can check pip by simply entering:
pip3 list
(Note for windows users it may be pip instead of pip3 if you only have one version of python installed)
 
Then looking for the backtrader entry in the output. Note Linux and Mac users can make this process a little quicker by piping the output to grep.
pip3 list | grep backtrader
I did notice that on one of my Linux machines, pip3 was reporting a lower version than what was actually installed (have not quite figured out why yet). So if the version reported does not seem right, you can also check it by opening a python shell, importing backtrader and printing the version.
import backtrader as bt
print(bt.__version__)
If you are not on the latest version, fire up a terminal (or command prompt) and enter
pip3 install --upgrade backtrader
Matplotlib
 
In order to see the results in a nice chart at the end of the test, you will need to have a 3rd party python module call “Matplotlib” installed. This module is the defacto graphing library for many scientists, analysts and researchers using Python.
 
Make sure you have it installed by opening a terminal or command prompt and entering.
pip3 install matplotlib

And so we begin….

There are two main parts to the script. The fist part is we setup a new class which has all the strategy logic. In the second part we setup our environment (such as adding the framework & providing a data source for testing etc).
 
First, the full script is pasted below. More detailed information and commentary will follow after the code.
'''
Author: www.backtest-rookies.com

MIT License

Copyright (c) 2017 backtest-rookies.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''

import backtrader as bt
from datetime import datetime

class firstStrategy(bt.Strategy):

    def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)

    def next(self):
        if not self.position:
            if self.rsi < 30:
                self.buy(size=100)
        else:
            if self.rsi > 70:
                self.sell(size=100)


#Variable for our starting cash
startcash = 10000

#Create an instance of cerebro
cerebro = bt.Cerebro()

#Add our strategy
cerebro.addstrategy(firstStrategy)

#Get Apple data from Yahoo Finance.
data = bt.feeds.Quandl(
    dataname='AAPL',
    fromdate = datetime(2016,1,1),
    todate = datetime(2017,1,1),
    buffered= True
    )

#Add the data to Cerebro
cerebro.adddata(data)

# Set our desired cash start
cerebro.broker.setcash(startcash)

# Run over everything
cerebro.run()

#Get final portfolio Value
portvalue = cerebro.broker.getvalue()
pnl = portvalue - startcash

#Print out the final result
print('Final Portfolio Value: ${}'.format(portvalue))
print('P/L: ${}'.format(pnl))

#Finally plot the end results
cerebro.plot(style='candlestick')

Important Note: The data in this tutorial uses the Quandl API. With this API you are limited to the number calls you can make per day. To have unlimited access to their free data, sign up for an API key here and add the apikeykeyword to the quandl data call like so:

data = bt.feeds.Quandl(
    dataname='F',
    fromdate = datetime(2016,1,1),
    todate = datetime(2017,1,1),
    buffered= True,
    apikey="INSERT YOUR API KEY"
    )

Reference: https://www.quandl.com/?modal=register

Imports

import backtrader as bt
from datetime import datetime

The first import should be pretty obvious. We are importing the backtrader framework. In addition to this, we import the datetime module from the standard python library. This is used for setting the start and end dates for our backtest. Backtrader expects to receive datetime objects when creating data feeds. For more information regarding the datetime module, please check out the documentation here:

The Strategy.

class firstStrategy(bt.Strategy):

    def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)

    def next(self):
        if not self.position:
            if self.rsi < 30:
                self.buy(size=100)
        else:
            if self.rsi > 70:
                self.sell(size=100)

When you create a strategy in backtrader, you inherit many methods and attributes from the base class `bt.Strategy`. If that sounds confusing, basically we are taking an existing skeleton strategy that has been written in the backtest framework and adding our logic to it. In programming when we use inheritance, we get to use all of the code that was written for the base strategy and just overwrite the bits we want to change. This means you don’t have to worry about all the code behind the scenes which allows you to interact with the data, orders, broker notifications and so on. The framework takes care of that! The new strategy class can be short, clean and easy to read. However, if you wish, you can dig deeper and tweak to your heart’s content.

 
Our class is called “firstStrategy” (rather aptly) and are just going to write code for the initialization of the strategy and the “next” method.
 
The initialization is just a single line, we just want to initialize an RSI indicator from the backtrader library. To do this we add it during the initialization of the strategy (__init__). Once it is set up in here, backtrader takes care of tracking the data, calculating the results and adding it to it to the graph at the end.
 
If you are new to programming it is important to note that you can call your RSI indicator anything you like but you need to prefix it with ‘self.’. This will allow you to access it from other methods (also known as functions when we are not talking about a class). In this case, we have kept it simple and initialized the indicator as `self.rsi`.
 
The next method (function) is called every new bar or in other words, every time a new candle is received. It is here where you write the “if this then that” logic. You can see we are first checking if we are in a position. As noted in the strategy section, we will only place one trade at a time. If we are not in a position then we have a look to see what the RSI indicator level is. If it is below 30, we will buy 100 shares. If it is above, we do nothing. (Because we didn’t write any code for what happens above 30 when we are not in a position). The second bit of logic states what happens when we ARE already in a position. We are now looking for an opportunity to sell. If the RSI is over 70, we sell all 100 shares. If not, again we do nothing. This repeats every time a new candle comes in (every day) until all the data has been checked.

The Setup

After we have written the strategy we get to the setup and execution. This basically boils down to:
  • Calling the backtrader engine (cerebro)
  • Getting some data and loading it into the engine.
  • Setting how much cash we have to trade (startcash)
  • Loading our strategy
  • Running the test
  • Plotting the results

A few things to point out:

When plotting, I use the keyword argument ‘style=candlestick’, if you do not use this, you will get a line chart of the closing price.
cerebro.plot(style='candlestick')
When setting up the data feed:
data = bt.feeds.YahooFinanceData(
    dataname='AAPL',
    fromdate = datetime(2016,1,1),
    todate = datetime(2017,1,1),
    buffered= True
    )
This is where we call the datetime module as noted above. We are passing a datetime object directly to the fromdate and todate keyword arguments. The buffered keyword argument means backtrader will buffer all the requested data before parsing starts.

Running the script

I assume you have done the basic python training mentioned in my last post. So just copy the code, fire up the script and check the results.

The results

2 profitable trades. 100% win rate. We are going to be rich!
 
On a more serious tone, take these results with a pinch of salt. We have not compared the results against a benchmark or a simple buy and hold strategy. We have not added profit draining commissions and we do not have enough long term test data to validate the strategy. One test for one company in one year should not fill us with confidence but at least we have made a base to explore further.