Bollinger Band Trading Strategy Backtest in Python

So, after a long time without posting (been super busy), I thought I’d write a quick Bollinger Band Trading Strategy Backtest in Python and then run some optimisations and analysis much like we have done in the past.

It’s pretty easy and can be written in just a few lines of code, which is why I love Python so much – so many things can be quickly prototyped and tested to see if it even holds water without wasting half your life typing.

So as some of you may be aware, Yahoo Finance have pulled their financial data API, which means that we can no longer use Pandas Datareader to pull down financial data from the Yahoo Finance site. Rumour has it that Google are pulling theirs too, although I’m yet to see that confirmed. Why they have both chosen to do this, I really don’t know but it’s a bit of a pain in the backside as it means lots of the code I’ve previously written for this blog no longer works!!! Such is life I guess…

Anyway, onto bigger and better things – we can still use the awesome Quandl Python API to pull the necessary data!

Let’s start coding…

#make the necessary imports
import pandas as pd
from pandas_datareader import data, wb
import numpy as np
import matplotlib.pyplot as plt
import quandl
%matplotlib inline
 
#download Dax data from the start of 2015 and store in a Pandas DataFrame
df = quandl.get("CHRIS/EUREX_FDAX1", authtoken="5GGEggAyyGa6_mVsKrxZ",start_date="2015-01-01")

We now have a Pandas DataFrame with the daily data for the Dax continuous contract. We can take a quick look at the structure of the data using the following:

df.head()

and we get the following:

So next we get to the code for creating the actual Bollinger bands themselves:

#Set number of days and standard deviations to use for rolling lookback period for Bollinger band calculation
window = 21
no_of_std = 2
 
#Calculate rolling mean and standard deviation using number of days set above
rolling_mean = df['Settle'].rolling(window).mean()
rolling_std = df['Settle'].rolling(window).std()
 
#create two new DataFrame columns to hold values of upper and lower Bollinger bands
df['Rolling Mean'] = rolling_mean
df['Bollinger High'] = rolling_mean + (rolling_std * no_of_std)
df['Bollinger Low'] = rolling_mean - (rolling_std * no_of_std)

Let’s plot the Dax price chart, along with the upper and lower Bollinger bands we have just created.

df[['Settle','Bollinger High','Bollinger Low']].plot()

Now let’s move on to the strategy logic…

#Create an "empty" column as placeholder for our /position signals
df['Position'] = None
 
#Fill our newly created position column - set to sell (-1) when the price hits the upper band, and set to buy (1) when it hits the lower band
for row in range(len(df)):
 
    if (df['Settle'].iloc[row] > df['Bollinger High'].iloc[row]) and (df['Settle'].iloc[row-1] < df['Bollinger High'].iloc[row-1]):
        df['Position'].iloc[row] = -1
 
    if (df['Settle'].iloc[row] < df['Bollinger Low'].iloc[row]) and (df['Settle'].iloc[row-1] > df['Bollinger Low'].iloc[row-1]):
        df['Position'].iloc[row] = 1  
 
#Forward fill our position column to replace the "None" values with the correct long/short positions to represent the "holding" of our position
#forward through time
df['Position'].fillna(method='ffill',inplace=True)
 
#Calculate the daily market return and multiply that by the position to determine strategy returns
df['Market Return'] = np.log(df['Settle'] / df['Settle'].shift(1))
df['Strategy Return'] = df['Market Return'] * df['Position']
 
#Plot the strategy returns
df['Strategy Return'].cumsum().plot()

So not particularly great returns at all…in fact pretty abysmal!

Let’s try upping the window length to use a look-back of 50 days for the band calculations…

But first, lets define a “Bollinger Band trading Strategy” function that we can easily run again and again while varying the inputs:

def bollinger_strat(df,window,std):
    rolling_mean = df['Settle'].rolling(window).mean()
    rolling_std = df['Settle'].rolling(window).std()
 
    df['Bollinger High'] = rolling_mean + (rolling_std * no_of_std)
    df['Bollinger Low'] = rolling_mean - (rolling_std * no_of_std)
 
    df['Short'] = None
    df['Long'] = None
    df['Position'] = None
 
    for row in range(len(df)):
 
        if (df['Settle'].iloc[row] > df['Bollinger High'].iloc[row]) and (df['Settle'].iloc[row-1] < df['Bollinger High'].iloc[row-1]):
            df['Position'].iloc[row] = -1
 
        if (df['Settle'].iloc[row] < df['Bollinger Low'].iloc[row]) and (df['Settle'].iloc[row-1] > df['Bollinger Low'].iloc[row-1]):
            df['Position'].iloc[row] = 1
 
    df['Position'].fillna(method='ffill',inplace=True)
 
    df['Market Return'] = np.log(df['Settle'] / df['Settle'].shift(1))
    df['Strategy Return'] = df['Market Return'] * df['Position']
 
    df['Strategy Return'].cumsum().plot()

Great, now we can just run a new strategy backtest with one line! Let’s use a 50 day look back period for the band calculations…

bollinger_strat(df,50,2)

Which should get us a nice looking plot:

Well those returns are at least better than the previous back-test although definitely still not great.

If we want to get a quick idea of whether there are any lookback periods that will create a positive return we can quickly set up a couple of vectors to hold a series of daily periods and standard deviations, and then just “brute force” our way through a series of backtests which iterates over the two vectors, as follows…

#Set up "daily look back period" and "number of standard deviation" vectors
#For example the first one creates a vector of 20 evenly spaced integer values ranging from 10 to 100
#The second creates a vector of 10 evenly spaced floating point numbers from 1 to 3
windows = np.linspace(10,100,20,dtype=int)
stds = np.linspace(1,3,10)
 
#And iterate through them both, running the strategy function each time
for window in windows:
    for std in stds:
        bollinger_strat(df,window,std)

This gets us the following plot at the end:

Granted at this point we can’t be sure exactly which combination of standard deviations and daily look back periods produce which results shown in the chart above, however the fact that there are only a couple of equity curves that end up in positive territory would suggest to me that this may not be a great strategy to pursue…for the Dax at least. That’s not to say Bollinger bands are not useful, just that used in such a simple way as outlined in the above strategy most likely isn’t going to provide you with any kind of real “edge”.

Oh well..perhaps we’ll find something better next time.

until then!

It's only fair to share...Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePin on PinterestShare on Reddit
Written by s666