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="[enter-your-token-here]",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!

## 23 comments

I like your explanation. It’s a pity this strategy doesn’t provide good returns. Keep on looking 🙂

Thanks! I will continue the eternal search for that “winning strategy” 😉

Hey thanks for your posts, hope you keep them coming. These are great!

Thanks for the comment! Also – I replied to your other comment about running code using data stored locally on your PC but didn’t hear back from you. i am happy to try to help you refactor the code to use locally held data – I just need to know a bit more about what format it is stored in. 😀

Hello Sir! I have a question regarding how you download the Dax data. What does authtoken=”5GGEggAyyGa6_mVsKrxZ” mean?? Is it a website? Also, if I want to get the data for a certain stock such as Apple, what will be the corresponding code? Thank you in advance!

The “authtoken” is something that is used when downloading data from the Quandl API.

Pandas have fixed their DataReader so that the Yahoo Finance API can be used again now – so the simplest solution to your problem is to use something in the following format:

from pandas_datareader import data, wb

dax_data = web.DataReader(“^GDAXI”, ‘yahoo’, ’01/01/2000′, ’07/09/2017′)

That will get you Dax cash index data between those two dates alter the dates as necessary. hope that helps!

Hey! Thanks for these posts. Really enjoy them. Shouldn’t it be: df[‘Strategy Return’] = df[‘Market Return’] * df[‘Position’].shift(1) in order to be correct?

Hi Aleksander – I think you are indeed correct; because we are using settle price to determine whether the bollinger bands have been hit, the position determined the day before will be the position that affects the next day’s returns. So in short, yes i think you are correct. I shall change the code when I have a moment. Well spotted – thanks for that!

Thank you very much for this. Helps beginners like myself port this over to several other domains like Cryptocurrencies. Keep on posting this great content! Very helpful to see how easy python can test an idea.

No problem, thanks for the comment – I’ll be trying to post an article very soon along the same lines but this time using the Stochastic Oscillator as a signal.

The best one!

When you execute a sale, shouldn’t the following rows be filled with 0 after -1? You effectively stopped making a loss/gain then? If you continue multiplying by -1 in my understanding you will continue ‘making a loss’ in the sum even though you sold?

Hi Jed – actually this strategy operates on the assumption that you go “long” when the price hits the lower band and go “short” when the prices hits the upper band, rather than just selling your long position when the upper band is hit. SO there is always a position being held, whether long or short. Hope that explains it.

hi thanks for the post really helped me learning algorithm in python from you website, I have a small question when is run a for loop to calculate position my position dataframe dosent change it show none as selected earlier cna you help me out in this it would really help thx

Hi Hardik – are you using the code I have written above or are you adapting it and changing it in any way?

I follow your strategies and they are very well explained. I saw you use lot of looping. We can create additional 3 columns for ‘Settle’, ‘Bollinger High’, ‘Bollinger Low’ using shift(1) on the existing columns in the dataframe. This will avoid looping and vectorized operation is much faster. This is just a suggestion.

Hi Gurudutt, that is a good point – I should really try to avoid looping operations and go for more vectorised operations for sure!

Hi S666,

I have very much enjoyed reading your posts about using pandas for quickly testing simple trading strategies. I have one question:

It seems to me that you are able to change your position before you get the signal.

In the code you calculate the strategy return by multiplying the (log) return with the current position:

df[‘Market Return’] = np.log(df[‘Settle’] / df[‘Settle’].shift(1))

df[‘Strategy Return’] = df[‘Market Return’] * df[‘Position’]

I’m wondering if it shouldn’t be

df[‘Market Return’] = np.log(df[‘Settle’] / df[‘Settle’].shift(1))

df[‘Strategy Return’] = df[‘Market Return’] * df[‘Position’].shift(1)

Let me give an example:

t | price | returns | pos

1 | 100 | | 1

2 | 99 | -1 | -1

at t = 1 we get the signal to enter a long position and buy one instrument.

at t = 2 we get the signal to enter a short position sell the instrument we already bought and short sell another.

Using the code without shifting the position, the strategy makes a (non log) return of -1*-1 = 1, but in reality we bought 1 at a price of 100 at t= 1 and sold it for 99 at t=2 giving a strategy return of -1 (then we also short sold another in order to enter a short position, but that first influence the strategy return at t=3 when we see which way the price went).

Am I completely off here, i’m very curious of what i’m missing?

best regards

Nich

I’m sorry for the messed up formatting of my post, the forum seems to eat white spaces. I hope you still can read it, otherwise shoot me an email.

Hi Nich, you are correct it seems – I have failed to shift the position forward to align with the correct signal as you have shown. Apologies for that – always happy for people to point out errors of logic like that – thanks very much!

Seems so easy … so good explanation … thanks you Sir

Love your articles and the brute force method!

Hey Chris – Glad you like the content 😉 – if there is anything in particular you would like to see a post about, just shout and I will see what I can do.