Sell when price is higher than 1.75 standard deviations above the 20-day Moving Average after closing any existing long position.

Buy when price is lower than 1.75 standard deviations below the 20-day Moving Average after closing any existing short position.

I expect there to be one or more errors.

Regards,

Peter

1785
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
import pandas

def process_df(df):
df = df.rename(columns={'Close': 'price'})
df = df.fillna(method='ffill')
df['MA20']=pandas.stats.moments.rolling_mean(df['price'], 20)
df['ABS']=abs(df['price']-df['MA20'])
df['STDDEV']=pandas.stats.moments.rolling_std(df['ABS'], 20)
df['UPPER_BB']=df['MA20']+2*df['STDDEV']
df['LOWER_BB']=df['MA20']-2*df['STDDEV']
return df

def initialize(context):
fetch_csv('https://raw.githubusercontent.com/pcawthron/StockData/master/CMG%202011%20Daily%20Close.csv',
date_column='Date',
symbol='CMG',
usecols=['Close'],
post_func = process_df,
date_format='%d/%m/%Y'
)
context.stock = sid(28016)
context.qty = 400
context.stddev_limit = 1.75

def handle_data(context, data):
if str(data['CMG'].datetime.year) == "2011":
record(CMG=data['CMG'].price)
record(Upper=data['CMG']['UPPER_BB'])
record(MA20=data['CMG']['MA20'])
record(Lower=data['CMG']['LOWER_BB'])
order_handling(context, data)

def order_handling(context, data):
# At top of bands?
if data['CMG'].price >  data['CMG']['MA20'] + data['CMG']['STDDEV'] * context.stddev_limit :
# Are we long or neutral?
if context.portfolio.positions[sid(28016)].amount >= 0:
# Close our long position if we have one
close_position(context, data)
order(sid(28016), -context.qty)
# At bottom of bands?
if data['CMG'].price < data['CMG']['MA20'] - data['CMG']['STDDEV'] * context.stddev_limit :
# Are we short or neutral?
if context.portfolio.positions[sid(28016)].amount <= 0:
# Close our short position if we have one
close_position(context, data)
order(sid(28016), context.qty)
return

def close_position(context, data):
# Open position?
if  context.portfolio.positions[sid(28016)].amount > 0:
order(sid(28016), -context.qty)
elif context.portfolio.positions[sid(28016)].amount < 0:
order(sid(28016), context.qty)
return
We have migrated this algorithm to work with a new version of the Quantopian API. The code is different than the original version, but the investment rationale of the algorithm has not changed. We've put everything you need to know here on one page.
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.
18 responses

Hello Peter,

The bands seem kinda wide at the start of the backtest (around Mar 2011). How are you setting them at the outset? Seems like they shouldn't start until you have a full trailing window of data, right?

Grant

Hello Grant,

The backtest starts on 2011-01-01 and uses a 20-period Moving Average which are NaNs until 2011-01-31. The MA20 is then used to calculate a 20-period Standard Deviation so this is NaNs until 2011-02-28. The chart correctly starts on 2011-02-28 i.e.day 39 (I think).

I think the chart is right as I see the same thing in an Excel chart. The share price went from 218.92 on 2011-01-31 to 272.95 on 2011-02-14 hence the width of the bands at the start. (I'm using Yahoo prices which are slightly different to Quantopian ones.)

The Excel chart is here:

Maybe Quantopian could adopt this format and show the 'early' data? It seems that if one of the four 'record' data items is a NaN then none of them are shown.

Regards,

Peter

Thanks Peter,

I got it...we don't see the trailing window of data on which the initial bands are based. Presumably, this could be remedied with some logic, so that you start plotting the prices immediately, but hold off on plotting the bands until there is enough data.

By the way, do people actually make money consistently with algorithms based on Bollinger Bands? It would seem that you'd need a quasi-stationary price time series. My thought is that first one would screen for stationarity (not sure how to do this), and then apply something like the Bollinger Bands. How did you come up with CMG as a stock to backtest?

Grant

Hello Grant,

That's an interesting idea on holding off - I might try that as an exercise. I don't know if many people actually use Bollinger Bands now - I just like them as an arithmetic exercise. People talk about 'mean reversion' quite a bit so it makes sense to me. By chance I was reading something from 2004 yesterday about stationary time-series when I was looking for the definition of Bollinger Bands: see: http://www.trade2win.com/boards/technical-analysis/7930-bollinger-band-excel-calculation.html

I did the first run of the Computational Investing course on Coursera (see: https://www.coursera.org/course/compinvesting1) which I really enjoyed. We did some Excel homework and I used CMG as one of the stocks there, so when I wanted to play with Bollinger Bands in Excel I used that as I still had the spreadsheet! I've no idea how I picked it originally - I think I saw it in ThinkorSwim as a large mover on the day or similar.

Regards,

Peter

Hello Grant,

There is some revised code at https://www.quantopian.com/posts/fethcher-post-func-and-accessing-calculations-in-handle-data where you can see a slightly different result for the recording/charting.

Regards,

Peter

Good stuff Peter, and thanks for sharing!

But, I wonder why you are calculating the intermediate column 'ABS'? Shouldn't the STDDEV be calculated calculated simply based on 'price'? That is one reason why it takes 40 days to get out of NaN land (looking back on the lookbacked data), and I think stddev based on the abs() might be a different metric.

Take a look at what I plot in yahoo finance for the delicious CMG over 2011 with the same BB and lookback parameters:

Thoughts?

Hello Ken,

You're probably right about the absolute value - I got that from a nine year old post here: http://www.trade2win.com/boards/technical-analysis/7930-bollinger-band-excel-calculation.html

I think there is a lot of confususion about Bollinger Bands and I've just seen a few different calculations. This is from 'Bollinger On Bollinger Bands':

"To calculate standard deviation you first measure the average of the data set and then subtract that average from each of the points in the data set. The result is a list of the deviations from the average -some negative, some positive. The more volatile the series, the greater the dispersion of the list. The next step is to sum the list. However, the list as is will total to zero, because the pluses will offset the minuses. In order to measure the dispersion it is necessary to get rid of the negative signs. This can be done simply by canceling the minus signs. The resulting measure, mean absolute deviation, was one of the calculations that were initially considered. Squaring the members of the list also eliminates the negative numbers - a negative number multiplied by a negative number is a positive number - that's the method used in standard deviation. The last steps are easy-having squared the list of deviations, calculate the average squared deviation and take the square root."

The calculation in Excel linked to here http://www.futuresmag.com/pages/downloads/spreadsheets.php agrees with me and produces the first bands on the day of the 40th price. Many others calculate the average of the first 20 days and then subtract it from each day in a kind of time travel. What a muddle.

Regards,

Peter

Wow - now, I love Futures Mag, but that Excel spreadsheet is wrong on yet another level :(. Look at the calc for the each band, it is taking the standard deviation of the close data AND the SMA !?! For example, on F64 and G64, it should be .... STDDEV(C44:C64), not C44:D64. The latter makes no sense, to add the SMA into the statistics, and is probably just an unfortunate typo.

I don't think you did that though - you did put what the trade2win's esiotrot said, which I guess i just disagree with.

[ Oh, I wonder if that C44:D64 typo essentially does the same thing in the end as esiotrot in essence - maybe so. ]

The stddev function already subtracts out the average of the dataset for each point, so we don't need to do that again. Otherwise, you're subtracting out the average twice which will result in a less volatile bands. See how much smoother yours are than in the URL chart I posted.

So, I'd recommend:
....
df['MA20']=pandas.stats.moments.rolling_mean(df['price'], 20)
df['STDDEV']=pandas.stats.moments.rolling_std(df['price'], 20)
df['UPPER_BB']=df['MA20']+2*df['STDDEV']
df['LOWER_BB']=df['MA20']-2*df['STDDEV']
....

Like I said, compare in yahoo finance or whatever commercial package to be sure. Something like that is probably the best independent source to validate. I'm happy to be incorrect and learn too :).

Nice use of the new fetch tool!

With regards, and thanks,
Ken

EDIT: PS, see http://www.bollingeronbollingerbands.com/support/?s=ovindi in the top portion.
However, in some more searching I have seen many "formulations", even on with the std dev of the SMA.
So, I agree there's a lot of muddle out there. Hopefully this clears it up in this case.

Ironically - your coding "works better" as far as total returns and max drawdown!
So, who's to argue? Ha! :)

Nice algo!

Hey guys, I really do not know a lot about programming but I have this dataset that I would like to test this algorithm on but I keep getting an error about not being able to create logs, anyone understand the problem? Thanks https://raw.github.com/SjengJanssen/KEES/master/KEES3.csv

Hell Jonas,

Would you be willing to post some/all of the algorithm? It's kinda hard to troubleshoot with just the data you are presumably using as an input to the algorithm.

Grant

Jonas, I don't recognize that problem. Please drop me a note at [email protected] with the full error, or better yet, some sample code I can repro the problem with. Thanks!

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

Please forget what I said, it said "this backtest didn't create any logs" after which it gave a date error when I was experimenting. Basically, what I am trying to do is to change the code in such a way that instead of the CMG stock it can do a backtest on the "Think AEX tracker" ( http://www.bloomberg.com/quote/TDT:NA ). But the major issue to start with seems to be that I cannot find this tracker (it does find the index itself though (36255), but only for 2008/2009) and then another issue would be whether this algorithm would actually work with trackers at all?

Jonas, we only have stocks and ETFs traded in the US in our database. That tracker appears to be traded in Amsterdam.

Hello Jonas,

This unfinished/untested version uses your github data. It's just a programming exercise for me but it may help you a little.

Regards,

Peter

52
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
import pandas

price = 0
upperBB = 0
lowerBB = 0

def process_df(df):
df = df.rename(columns={'Close': 'price'})
return df

@batch_transform(window_length=20)
def get_bbands(df):
df['MA20']=pandas.stats.moments.rolling_mean(df['price'], 20)
df['STDDEV']=pandas.stats.moments.rolling_std(df['price'], 20)
return df

def initialize(context):
fetch_csv('https://raw.github.com/SjengJanssen/KEES/master/KEES3.csv',
date_column='Date',
symbol='MyData',
usecols=['Close'],
post_func = process_df,
date_format='%d/%m/%Y'
)
context.stock = sid(24)
context.qty = 1000
context.band = 2.0

def handle_data(context, data):
global price, lowerBB, upperBB
results = get_bbands(data)
if results is None:
return
price = results.price['MyData'][19]
ma20 = results.MA20['MyData'][19]
stddev = results.STDDEV['MyData'][19]
lowerBB = results.MA20['MyData'][19] - stddev * context.band
upperBB = results.MA20['MyData'][19] + stddev * context.band
record(Price=price, \
MA20=ma20, \
Lower=lowerBB, \
Upper=upperBB)
order_handling(context, data)

def order_handling(context, data):
global price, lowerBB, upperBB
# At top of bands?
if price >  upperBB:
# Are we long?
if context.portfolio.positions[sid(24)].amount >= 0:
# Close our long position if we have one
close_position(context, data)
order(sid(24), -context.qty)
# At bottom of bands?
if price < lowerBB:
# Are we short?
if context.portfolio.positions[sid(24)].amount <= 0:
# Close our short position if we have one
close_position(context, data)
order(sid(24), context.qty)
return

def close_position(context, data):
# Open position?
if  context.portfolio.positions[sid(24)].amount > 0:
order(sid(24), -context.qty)
elif context.portfolio.positions[sid(24)].amount < 0:
order(sid(24), context.qty)
return
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

Thank you very much Peter, I'll try to work with it

can you make 2. one for long positions and one for short positions? In other words, an algo for going long tracking long positions only. and 1 tracking short postitions

Hello Dave,

I'm not too sure what you mean. As an aside this is the simplest implementation of creating the bands I could come up with.

Regards,

Peter

56
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
def initialize(context):
context.stock=sid(24)

def handle_data(context, data):
if data[context.stock].stddev(20) <> None:
record(Price=data[context.stock].price, \
MA=data[context.stock].mavg(20), \
Lower=data[context.stock].mavg(20) - 2 * data[context.stock].stddev(20), \
Upper=data[context.stock].mavg(20) + 2 * data[context.stock].stddev(20))
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.