Bollinger bands by design have all the elements needed to implement a complete mean reversion strategy. The Bollinger’s middle line is a simple moving average which is suitable for representing the mean. Furthermore, the upper and lower bands represent a standard deviation above/below the median line. This is ideal for indicating when price has moved away from the mean.
Mean Reversion for beginners
Mean reversion is simply a nice way to describe something that is moving back (reverting) to an “average” price (the mean). Generally, a trader will employ a mean reversion strategy when the price of asset/stock/currency has moved quite far away from the historical average and there is a belief, it will eventually move back.
User beware!
Mean reversion strategies can be risky if you are placing trades on the wrong side of a heavily trending market. As such, this strategy should be employed with care.
The Strategy
The strategy we will implement shall attempt to capture reversion to the mean (middle line) once price moves beyond the upper or lower bands. In order to try and capture as much of the move as possible, the strategy will use stop orders to enter a position right at the moment price retouches the outer bands (only after already having moved beyond them). This does have a potential downside though. Price might move back inside the band before reversing and continuing to move away from the mean. As such, some people may prefer to wait until price closes inside band.
To exit the positions, limit orders shall be used with a limit price equal to the middle line. This will exit as soon as price has reverted to the mean.
The Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
''' Author: www.backtest-rookies.com MIT License Copyright (c) 2018 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 BOLLStrat(bt.Strategy): ''' This is a simple mean reversion bollinger band strategy. Entry Critria: - Long: - Price closes below the lower band - Stop Order entry when price crosses back above the lower band - Short: - Price closes above the upper band - Stop order entry when price crosses back below the upper band Exit Critria - Long/Short: Price touching the median line ''' params = ( ("period", 20), ("devfactor", 2), ("size", 20), ("debug", False) ) def __init__(self): self.boll = bt.indicators.BollingerBands(period=self.p.period, devfactor=self.p.devfactor) #self.sx = bt.indicators.CrossDown(self.data.close, self.boll.lines.top) #self.lx = bt.indicators.CrossUp(self.data.close, self.boll.lines.bot) def next(self): orders = self.broker.get_orders_open() # Cancel open orders so we can track the median line if orders: for order in orders: self.broker.cancel(order) if not self.position: if self.data.close > self.boll.lines.top: self.sell(exectype=bt.Order.Stop, price=self.boll.lines.top[0], size=self.p.size) if self.data.close < self.boll.lines.bot: self.buy(exectype=bt.Order.Stop, price=self.boll.lines.bot[0], size=self.p.size) else: if self.position.size > 0: self.sell(exectype=bt.Order.Limit, price=self.boll.lines.mid[0], size=self.p.size) else: self.buy(exectype=bt.Order.Limit, price=self.boll.lines.mid[0], size=self.p.size) if self.p.debug: print('---------------------------- NEXT ----------------------------------') print("1: Data Name: {}".format(data._name)) print("2: Bar Num: {}".format(len(data))) print("3: Current date: {}".format(data.datetime.datetime())) print('4: Open: {}'.format(data.open[0])) print('5: High: {}'.format(data.high[0])) print('6: Low: {}'.format(data.low[0])) print('7: Close: {}'.format(data.close[0])) print('8: Volume: {}'.format(data.volume[0])) print('9: Position Size: {}'.format(self.position.size)) print('--------------------------------------------------------------------') def notify_trade(self,trade): if trade.isclosed: dt = self.data.datetime.date() print('---------------------------- TRADE ---------------------------------') print("1: Data Name: {}".format(trade.data._name)) print("2: Bar Num: {}".format(len(trade.data))) print("3: Current date: {}".format(dt)) print('4: Status: Trade Complete') print('5: Ref: {}'.format(trade.ref)) print('6: PnL: {}'.format(round(trade.pnl,2))) print('--------------------------------------------------------------------') #Variable for our starting cash startcash = 10000 # Create an instance of cerebro cerebro = bt.Cerebro() # Add our strategy cerebro.addstrategy(BOLLStrat) data = bt.feeds.Quandl( dataname='AMZN', fromdate = datetime(2017,1,1), todate = datetime(2018,1,1), buffered= True, ) # Add the data to Cerebro cerebro.adddata(data) # Add a sizer cerebro.addsizer(bt.sizers.FixedReverser, stake=10) # 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(round(portvalue,2))) print('P/L: ${}'.format(round(pnl,2))) # Finally plot the end results cerebro.plot(style='candlestick') |
You might notice that self.broker.get_orders_open()
is frequently used to cancel all open orders. This is because, at the time of writing, Backtrader does not have a supported method to amend open orders. Another thing to be aware of is that most stores (IB, Oanda etc) do not have support for the get_orders_open()
method. Therefore, this strategy can not be used live without some tweaks.
Some Results
In the interests of fairly showing the strengths and weaknesses of this strategy, one of the results will show the strategy running in an ideal environment whilst the other example was run in sub-optimal conditions. It should be pointed out that a fixed position size was used for testing. As such, the focus should be on the strike rate and dollar differences between winners and losers instead of the actual PnL value.
Ford 2016 – 2017
A good example of a ranging equity. Lots of nice positive trades. Volatility was nice during this period with the price often reverting to the mean.
Amazon 2017-2018
This is a good example of a strong trending stock. In this scenario, the strategy just about broke even. If given more trades, I suspect it would have been a losing strategy over the long term. In general, the bands were quite tight during Amazons’ steady uptrend. This resulted in a large losing position where price only just broke above the band and initiated a short position. The stock then continued to rise upwards for quite a long time.
”’
This is a simple mean reversion bollinger band strategy.
Entry Critria:
– Long: Price crossing/close below the upper band
– Short: Price crossing/close above the lower band
Exit Critria
– Long/Short: Price touching the median line
”’
You may want to correct your description of the strategy above?
It seems to be backwards; and criteria was misspelled.
Entry Criteria:
Long: Price crossing/closing above the lower band.
Short: Price crossing/closing below the upper band.
Thanks John!
Good catches. The article has been updated.
Your opinions on trading are spectacular, I am going to throw a number of my followers right onto your pathway.
I can I ask how would Bollinger Band strategy normally look like because I can not see what are the “Mean Reversion ” part are.
How can the self.boll in __init__ not call on a data source? How does it know what data to calculate Bollinger bands from? Default = self.data?
orders = self.broker.get_orders_open()
# Cancel open orders so we can track the median line
if orders:
for order in orders:
self.broker.cancel(order)
Can someone help me understand this snippet of code?
Wouldn’t it continuously cancel new orders?