This post contains a simple but complete mean reversion strategy that has been backtested with various cryptocurrencies on the 1-hour time frame. Having said that, there is no reason why this strategy cannot be tweaked to work on other timeframes and assets. The principles remain the same.
So what is mean reversion?
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.
Our strategy will attempt to place trades when a fast exponential moving average has moved a large distance from a slower moving average. In this scenario, the slow-moving average is employed to act as the mean (average price). As such, if recent price action moves very far from it, the theory is that price will eventually revert to the mean. A fast exponential moving average shall be used instead of the closing price to take a little bit of noise out of the price action.
Finally, to try and ensure that price is starting to move back towards the mean, a check final check is performed on the exponential moving average. We shall check to see the fast moving average has been rising (or falling for the short side) for a given number bars before entering. This should avoid instances where price steamrolls through the entry price and continues to head further and further away from the mean.
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. A couple of options have been incorporated to help avoid wipeouts such as a stop loss and switches to disable trades in certain directions. Disabling trades in one direction should allow you to switch the strategy to “buy the dips” or “sell the tops”. The key challenge for you will be to correctly identify when the market regime changes and setup accordingly!
strategy("EMA, SMA Mean Reversion", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100)
st_yr_inp = input(defval=2017, title='Backtest Start Year', type=integer)
st_mn_inp = input(defval=01, title='Backtest Start Month', type=integer)
st_dy_inp = input(defval=01, title='Backtest Start Day', type=integer)
en_yr_inp = input(defval=2025, title='Backtest End Year', type=integer)
en_mn_inp = input(defval=01, title='Backtest End Month', type=integer)
en_dy_inp = input(defval=01, title='Backtest End Day', type=integer)
sma_lookback = input(defval=100, title="Lookback Period For SMA")
ema_lookback = input(defval=10, title="Lookback Period For EMA")
long_diff_perc = input(defval=6, title="Percentage Deviation From SMA to go Long")/100
short_diff_perc = input(defval=20, title="Percentage Deviation From SMA to go Short")/100
ema_filter_bars = input(defval=4, title="The number of bars the EMA must rise/fall")
lng_allwd = input(defval=true, title="Allow Longs?")
srt_allwd = input(defval=true, title="Allow Shorts?")
use_stop = input(defval=true, title="Use Stoploss?")
stop_perc = input(defval=30, title="Stop Loss Percentage")/100
start = timestamp(st_yr_inp, st_mn_inp, st_dy_inp,00,00)
end = timestamp(en_yr_inp, en_mn_inp, en_dy_inp,00,00)
can_trade = time >= start and time <= end
// Indicators Setup
sma = sma(close, sma_lookback)
ema = ema(close, ema_lookback)
// Strategy Calcuations
close_stdev = stdev(close, sma_lookback)
sd1_upper = close + (close_stdev * 1)
sd1_lower = close - (close_stdev * 1)
close_diff = (close - sma) / sma
// Entries and Exits
longCondition = close_diff <= -long_diff_perc and rising(ema,ema_filter_bars) and lng_allwd and can_trade
stop_price = close * (1 - stop_perc)
strategy.order("Long Stoploss", false, stop=stop_price)
shortCondition = close_diff >= short_diff_perc and falling(ema,ema_filter_bars) and srt_allwd and can_trade
stop_price = close * (1 + stop_perc)
strategy.order("Short Stoploss", true, stop=stop_price)
strategy.close("Long", when=close_diff >=0)
strategy.cancel("Long Stoploss", when=close_diff>=0)
strategy.close("Short", when=close_diff <=0)
strategy.cancel("Short Stoploss", when=close_diff<=0)
sma_col = sma > sma ? green : red
ema_fill = close_diff <= -long_diff_perc ? lime : close_diff >= short_diff_perc ? maroon : aqua
p_sma = plot(sma, color=sma_col, linewidth=3)
p_ema = plot(ema, color=black, linewidth=2)
p_sd1 = plot(sd1_upper, color=black, linewidth=1, transp=85)
p_sd2 = plot(sd1_lower, color=black, linewidth=1, transp=85)
fill(p_sd1, p_sd2, title='STDEV Fill', color=silver, transp=80)
fill(p_sma, p_ema, title='EMA > Mean Percentage', color=ema_fill, transp=80)
How to use the code
As noted above, there are two main ways to use this code. A brief commentary on each method is provided below:
Mean reversion is configured out of the box. However, that does not mean that there is no work to do. First of all, the percentage deviation from the mean inputs needs to be tweaked according to the characteristics of the instrument you are trading. The second thing you need to do is decide what type of a stoploss (if any) is right for your risk tolerance. When running in mean reversion mode, I have generally prefer a wide stop loss. This is because I often see price can continue to move against the trade for some time before reverting to the mean. As such, I use the stop-loss as a failsafe against a catastrophic event.
Buy the dips / sell the tops
For those of you who are looking to ride a trend. Disabling trading in a single direction works really well in a heavily trending market. It allows you to get discounted entries whilst staying on the right side of the trend. When using this strategy, one must be on the lookout for a regime change.
Don’t forget to use the date inputs
Setting up for all market regimes is tough. Therefore, when testing use the date inputs to isolate specific regimes. This will give you an idea of how to setup for that particular regime moving forward.
Don’t overfit the moving averages
Finally, when setting up, it can be tempting to tweak the moving averages to eek every last bit of profit from the strategy. Try to avoid this temptation. All you are doing is lining up the MA’s with the peaks and troughs for one set of historical data. Aka “Curve Fitting”.
The moving averages should be employed to measure the distance from the mean. As such you should tweak them in a way that provides the best measurement. For example, adjust them if you want the SMA to represent more of the historical average or tweak the EMA to further reduce the noise.
Build on it
This code is just a starting point. Take a look at the trades and test out ideas that can improve the returns. Again, be careful not to try and turn every trade into a winner. That will lead to overfitting. Be sure to test the improvements on a range of different coins and time frames.
Bitcoin out of the box: 20 trades, 75% Strike rate with 42% profit
Ethereum buy the dip: 22 trades, 86% Strike rate with over 200% profit
Monero Out of the box: 28 trades, 67% Strike rate with 75% profit