Low Volatility Reversion
This mean-reversion strategy activates only during quiet markets. It measures ATR over a lookback window and enters when volatility falls below a percentage of that average and price deviates from its moving average.
Testing indicates an average annual return of about 139%. It performs best in the stocks market.
By trading against small moves in calm conditions, it aims to capture snap backs without chasing large trends.
Positions exit once price touches the moving average or the ATR-based stop-loss is reached.
Details
- Entry Criteria: Price away from moving average while ATR is below threshold.
- Long/Short: Both directions.
- Exit Criteria: Price returns to MA or stop triggers.
- Stops: Yes.
- Default Values:
MAPeriod= 20AtrPeriod= 14AtrLookbackPeriod= 20AtrThresholdPercent= 50mAtrMultiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: ATR, MA
- 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>
/// Strategy that trades on mean reversion during periods of low volatility.
/// It identifies periods of low ATR and opens positions when price
/// deviates from its moving average, expecting a return to the mean.
/// </summary>
public class LowVolReversionStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<int> _atrLookbackPeriod;
private readonly StrategyParam<decimal> _atrThresholdPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _cooldownBars;
private decimal _avgAtr;
private int _lookbackCounter;
private int _cooldown;
/// <summary>
/// Period for Moving Average calculation.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Period for ATR calculation.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Lookback period for ATR average calculation.
/// </summary>
public int AtrLookbackPeriod
{
get => _atrLookbackPeriod.Value;
set => _atrLookbackPeriod.Value = value;
}
/// <summary>
/// ATR threshold as percentage of average ATR.
/// </summary>
public decimal AtrThresholdPercent
{
get => _atrThresholdPercent.Value;
set => _atrThresholdPercent.Value = value;
}
/// <summary>
/// Type of candles used for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Cooldown bars between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Initialize the Low Volatility Reversion strategy.
/// </summary>
public LowVolReversionStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20)
.SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
.SetOptimize(10, 50, 5);
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "Period for ATR calculation", "Indicators")
.SetOptimize(7, 21, 7);
_atrLookbackPeriod = Param(nameof(AtrLookbackPeriod), 20)
.SetDisplay("ATR Lookback", "Lookback period for ATR average calculation", "Indicators")
.SetOptimize(10, 50, 10);
_atrThresholdPercent = Param(nameof(AtrThresholdPercent), 80m)
.SetDisplay("ATR Threshold %", "ATR threshold as percentage of average ATR", "Entry")
.SetOptimize(30m, 90m, 10m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_cooldownBars = Param(nameof(CooldownBars), 500)
.SetRange(1, 1000)
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_avgAtr = default;
_lookbackCounter = default;
_cooldown = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_avgAtr = 0;
_lookbackCounter = 0;
_cooldown = 0;
var sma = new SimpleMovingAverage { Length = MAPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(sma, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Gather ATR values for average calculation
if (_lookbackCounter < AtrLookbackPeriod)
{
if (_lookbackCounter == 0)
_avgAtr = atrValue;
else
_avgAtr = (_avgAtr * _lookbackCounter + atrValue) / (_lookbackCounter + 1);
_lookbackCounter++;
return;
}
else
{
_avgAtr = (_avgAtr * (AtrLookbackPeriod - 1) + atrValue) / AtrLookbackPeriod;
}
if (_cooldown > 0)
{
_cooldown--;
return;
}
// Check if we're in a low volatility period
decimal atrThreshold = _avgAtr * (AtrThresholdPercent / 100);
bool isLowVolatility = atrValue < atrThreshold;
if (Position == 0 && isLowVolatility)
{
if (candle.ClosePrice < smaValue)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (candle.ClosePrice > smaValue)
{
SellMarket();
_cooldown = CooldownBars;
}
}
else if (Position > 0)
{
if (candle.ClosePrice > smaValue)
{
SellMarket();
_cooldown = CooldownBars;
}
}
else if (Position < 0)
{
if (candle.ClosePrice < smaValue)
{
BuyMarket();
_cooldown = CooldownBars;
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class low_vol_reversion_strategy(Strategy):
"""
Low volatility mean reversion strategy.
Trades when ATR is below average, expecting price to revert to MA.
"""
def __init__(self):
super(low_vol_reversion_strategy, self).__init__()
self._ma_period = self.Param("MAPeriod", 20).SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14).SetDisplay("ATR Period", "Period for ATR calculation", "Indicators")
self._atr_lookback = self.Param("AtrLookbackPeriod", 20).SetDisplay("ATR Lookback", "Lookback period for ATR average calculation", "Indicators")
self._atr_threshold = self.Param("AtrThresholdPercent", 80.0).SetDisplay("ATR Threshold %", "ATR threshold as percentage of average ATR", "Entry")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._cooldown_bars = self.Param("CooldownBars", 500).SetDisplay("Cooldown Bars", "Bars to wait between trades", "General")
self._avg_atr = 0.0
self._lookback_counter = 0
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(low_vol_reversion_strategy, self).OnReseted()
self._avg_atr = 0.0
self._lookback_counter = 0
self._cooldown = 0
def OnStarted2(self, time):
super(low_vol_reversion_strategy, self).OnStarted2(time)
self._avg_atr = 0.0
self._lookback_counter = 0
self._cooldown = 0
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, sma_val, atr_val):
if candle.State != CandleStates.Finished:
return
sv = float(sma_val)
av = float(atr_val)
lb = self._atr_lookback.Value
if self._lookback_counter < lb:
if self._lookback_counter == 0:
self._avg_atr = av
else:
self._avg_atr = (self._avg_atr * self._lookback_counter + av) / (self._lookback_counter + 1)
self._lookback_counter += 1
return
else:
self._avg_atr = (self._avg_atr * (lb - 1) + av) / lb
if self._cooldown > 0:
self._cooldown -= 1
return
threshold = self._avg_atr * (float(self._atr_threshold.Value) / 100.0)
is_low_vol = av < threshold
close = float(candle.ClosePrice)
cd = self._cooldown_bars.Value
if self.Position == 0 and is_low_vol:
if close < sv:
self.BuyMarket()
self._cooldown = cd
elif close > sv:
self.SellMarket()
self._cooldown = cd
elif self.Position > 0:
if close > sv:
self.SellMarket()
self._cooldown = cd
elif self.Position < 0:
if close < sv:
self.BuyMarket()
self._cooldown = cd
def CreateClone(self):
return low_vol_reversion_strategy()