This variation of mean reversion scales entry thresholds by the ratio of ATR to standard deviation. When volatility increases relative to typical noise, the distance needed to trigger a trade grows, helping avoid premature signals during chaotic swings.
Testing indicates an average annual return of about 115%. It performs best in the stocks market.
A long position opens when price falls below the moving average by more than the adjusted threshold. A short position opens when price rises above the average by the same measure. Positions exit once price closes back near the average level.
The adaptive threshold makes this strategy suitable for markets with changing volatility regimes. A stop-loss equal to twice the ATR limits risk while waiting for reversion.
Details
Entry Criteria:
Long: Close < MA - Multiplier * ATR / (ATR/StdDev)
Short: Close > MA + Multiplier * ATR / (ATR/StdDev)
Long/Short: Both sides.
Exit Criteria:
Long: Exit when close >= MA
Short: Exit when close <= MA
Stops: Yes, dynamic based on ATR.
Default Values:
Period = 20
Multiplier = 2
MA, ATR and StdDev (the standard deviation of closes) all span Period candles; the threshold reduces to Multiplier times StdDev. The stop is set Multiplier times the ATR from the entry close, twice the ATR by default, and checked on candle closes. An entry signal against an open position reverses it.
CandleType = TimeSpan.FromMinutes(5)
Filters:
Category: Mean Reversion
Direction: Both
Indicators: ATR, StdDev
Stops: Yes
Complexity: Intermediate
Timeframe: Intraday
Seasonality: No
Neural networks: No
Divergence: No
Risk Level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Volatility Adjusted Mean Reversion strategy.
/// The threshold is Multiplier times the ATR divided by the ATR to standard deviation ratio, all over Period candles. A close more than the
/// threshold below the Period simple moving average goes long and one that far above it goes short, reversing an opposite position.
/// A long closes once the close is back at or above the average and a short once it is back at or below it. The stop lies Multiplier ATR
/// from the entry close and is checked on candle closes.
/// </summary>
public class VolatilityAdjustedMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _period;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<DataType> _candleType;
private decimal _stopPrice;
/// <summary>
/// Period of the SMA, ATR and standard deviation.
/// </summary>
public int Period
{
get => _period.Value;
set => _period.Value = value;
}
/// <summary>
/// Multiplier of the threshold and the stop.
/// </summary>
public decimal Multiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public VolatilityAdjustedMeanReversionStrategy()
{
_period = Param(nameof(Period), 20)
.SetGreaterThanZero()
.SetDisplay("Period", "Period of the SMA, ATR and standard deviation", "Parameters");
_multiplier = Param(nameof(Multiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Multiplier", "Multiplier of the threshold and the stop", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_stopPrice = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_stopPrice = default;
var sma = new SimpleMovingAverage { Length = Period };
var atr = new AverageTrueRange { Length = Period };
var stdev = new StandardDeviation { Length = Period };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, atr, stdev, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
{
DrawIndicator(oscillators, atr);
}
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue smaValue, IIndicatorValue atrValue, IIndicatorValue stdevValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!smaValue.IsFormed || !atrValue.IsFormed || !stdevValue.IsFormed)
return;
var atr = atrValue.GetValue<decimal>();
var deviation = stdevValue.GetValue<decimal>();
if (atr <= 0 || deviation <= 0)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var sma = smaValue.GetValue<decimal>();
var threshold = Multiplier * atr / (atr / deviation);
var close = candle.ClosePrice;
if (close < sma - threshold && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_stopPrice = close - Multiplier * atr;
}
else if (close > sma + threshold && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_stopPrice = close + Multiplier * atr;
}
else if (Position > 0 && (close >= sma || close <= _stopPrice))
{
SellMarket(Position);
}
else if (Position < 0 && (close <= sma || close >= _stopPrice))
{
BuyMarket(-Position);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
class volatility_adjusted_mean_reversion_strategy(Strategy):
"""
Volatility Adjusted Mean Reversion strategy.
The threshold is Multiplier times the ATR divided by the ATR to standard deviation ratio, all over Period candles. A close more than the
threshold below the Period simple moving average goes long and one that far above it goes short, reversing an opposite position.
A long closes once the close is back at or above the average and a short once it is back at or below it. The stop lies Multiplier ATR
from the entry close and is checked on candle closes.
"""
def __init__(self):
super(volatility_adjusted_mean_reversion_strategy, self).__init__()
self._period = self.Param("Period", 20).SetGreaterThanZero().SetDisplay("Period", "Period of the SMA, ATR and standard deviation", "Parameters")
self._multiplier = self.Param("Multiplier", 2.0).SetGreaterThanZero().SetDisplay("Multiplier", "Multiplier of the threshold and the stop", "Parameters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._stop_price = Decimal(0)
def OnReseted(self):
super(volatility_adjusted_mean_reversion_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(volatility_adjusted_mean_reversion_strategy, self).OnStarted2(time)
self._reset_state()
sma = SimpleMovingAverage()
sma.Length = self._period.Value
atr = AverageTrueRange()
atr.Length = self._period.Value
stdev = StandardDeviation()
stdev.Length = self._period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, atr, stdev, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, atr)
def _process_candle(self, candle, sma_value, atr_value, stdev_value):
if candle.State != CandleStates.Finished:
return
if not sma_value.IsFormed or not atr_value.IsFormed or not stdev_value.IsFormed:
return
atr = atr_value.GetValue[Decimal](None)
deviation = stdev_value.GetValue[Decimal](None)
if atr <= 0 or deviation <= 0:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
multiplier = Decimal(self._multiplier.Value)
sma = sma_value.GetValue[Decimal](None)
threshold = multiplier * atr / (atr / deviation)
close = candle.ClosePrice
if close < sma - threshold and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._stop_price = close - multiplier * atr
elif close > sma + threshold and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._stop_price = close + multiplier * atr
elif self.Position > 0 and (close >= sma or close <= self._stop_price):
self.SellMarket(self.Position)
elif self.Position < 0 and (close <= sma or close >= self._stop_price):
self.BuyMarket(-self.Position)
def CreateClone(self):
return volatility_adjusted_mean_reversion_strategy()