The ATR Mean Reversion strategy measures how far price travels away from a moving average relative to recent volatility. The Average True Range (ATR) provides an adaptive gauge so thresholds expand during active periods and contract when markets quiet down.
Testing indicates an average annual return of about 109%. It performs best in the crypto market.
A long setup occurs when price closes below the moving average by more than Multiplier times the ATR. A short setup appears when price closes above the moving average by the same distance. Positions are exited once price returns to the moving average.
This technique is intended for short-term traders expecting prices to revert after excessive moves. The ATR-based stop keeps risk proportional to current market conditions.
Details
Entry Criteria:
Long: Close < MA - Multiplier * ATR
Short: Close > MA + Multiplier * ATR
Long/Short: Both sides.
Exit Criteria:
Long: Exit when close >= MA
Short: Exit when close <= MA
Stops: Yes, stop-loss around 2*ATR by default.
Default Values:
MaPeriod = 20
AtrPeriod = 14
Multiplier = 2
MA is the MaPeriod simple moving average and ATR the AtrPeriod average true range. The stop is set Multiplier times the ATR from the entry close, 2 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: MA, ATR
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>
/// ATR Mean Reversion strategy.
/// A close more than Multiplier times the AtrPeriod ATR below the MaPeriod 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 AtrMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<DataType> _candleType;
private decimal _stopPrice;
/// <summary>
/// Period of the simple moving average.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Period of the ATR.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// ATR multiplier for the entry distance 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 AtrMeanReversionStrategy()
{
_maPeriod = Param(nameof(MaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Period of the simple moving average", "Parameters");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Period of the ATR", "Parameters");
_multiplier = Param(nameof(Multiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Multiplier", "ATR multiplier for the entry distance 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 = MaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, atr, 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)
{
if (candle.State != CandleStates.Finished)
return;
if (!smaValue.IsFormed || !atrValue.IsFormed)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var sma = smaValue.GetValue<decimal>();
var distance = Multiplier * atrValue.GetValue<decimal>();
var close = candle.ClosePrice;
if (close < sma - distance && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_stopPrice = close - distance;
}
else if (close > sma + distance && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_stopPrice = close + distance;
}
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
from StockSharp.Algo.Strategies import Strategy
class atr_mean_reversion_strategy(Strategy):
"""
ATR Mean Reversion strategy.
A close more than Multiplier times the AtrPeriod ATR below the MaPeriod 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(atr_mean_reversion_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Period of the simple moving average", "Parameters")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Period of the ATR", "Parameters")
self._multiplier = self.Param("Multiplier", 2.0).SetGreaterThanZero().SetDisplay("Multiplier", "ATR multiplier for the entry distance 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(atr_mean_reversion_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(atr_mean_reversion_strategy, self).OnStarted2(time)
self._reset_state()
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(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)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, atr)
def _process_candle(self, candle, sma_value, atr_value):
if candle.State != CandleStates.Finished:
return
if not sma_value.IsFormed or not atr_value.IsFormed:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
sma = sma_value.GetValue[Decimal](None)
distance = Decimal(self._multiplier.Value) * atr_value.GetValue[Decimal](None)
close = candle.ClosePrice
if close < sma - distance and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._stop_price = close - distance
elif close > sma + distance and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._stop_price = close + distance
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 atr_mean_reversion_strategy()