Donchian Width Mean Reversion
The Donchian Width Mean Reversion strategy focuses on extreme readings of the Donchian to exploit reversion. Wide departures from the normal level rarely last.
Testing indicates an average annual return of about 121%. It performs best in the crypto market.
Trades trigger when the indicator swings far from its mean and then begins to reverse. Both long and short setups include a protective stop.
Suited for swing traders expecting oscillations, the strategy closes out once the Donchian returns toward balance. Starting parameter DonchianPeriod = 20.
Details
- Entry Criteria: Indicator crosses back toward mean.
- Long/Short: Both directions.
- Exit Criteria: Indicator reverts to average.
- Stops: Yes.
- Default Values:
DonchianPeriod= 20LookbackPeriod= 20DeviationMultiplier= 2.0mStopLossPercent= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: Donchian
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Short-term
- 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>
/// Donchian width mean reversion strategy.
/// Trades contractions and expansions of Donchian Channel width around its recent average.
/// </summary>
public class DonchianWidthMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _donchianPeriod;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _deviationMultiplier;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private DonchianChannels _donchian;
private decimal[] _widthHistory;
private int _currentIndex;
private int _filledCount;
private int _cooldown;
/// <summary>
/// Donchian Channel period.
/// </summary>
public int DonchianPeriod
{
get => _donchianPeriod.Value;
set => _donchianPeriod.Value = value;
}
/// <summary>
/// Lookback period for width statistics.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Deviation multiplier for mean reversion detection.
/// </summary>
public decimal DeviationMultiplier
{
get => _deviationMultiplier.Value;
set => _deviationMultiplier.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Cooldown bars between orders.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="DonchianWidthMeanReversionStrategy"/>.
/// </summary>
public DonchianWidthMeanReversionStrategy()
{
_donchianPeriod = Param(nameof(DonchianPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Donchian Period", "Donchian Channel period", "Indicators")
.SetOptimize(10, 50, 5);
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Lookback period for width statistics", "Strategy Parameters")
.SetOptimize(10, 50, 5);
_deviationMultiplier = Param(nameof(DeviationMultiplier), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Deviation Multiplier", "Deviation multiplier for mean reversion detection", "Strategy Parameters")
.SetOptimize(1m, 3m, 0.5m);
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management");
_cooldownBars = Param(nameof(CooldownBars), 1200)
.SetRange(1, 5000)
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle type for strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_donchian = null;
_currentIndex = default;
_filledCount = default;
_cooldown = default;
_widthHistory = new decimal[LookbackPeriod];
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_donchian = new DonchianChannels { Length = DonchianPeriod };
_widthHistory = new decimal[LookbackPeriod];
_currentIndex = 0;
_filledCount = 0;
_cooldown = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_donchian, ProcessDonchian)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _donchian);
DrawOwnTrades(area);
}
StartProtection(new(), new Unit(StopLossPercent, UnitTypes.Percent));
}
private void ProcessDonchian(ICandleMessage candle, IIndicatorValue donchianValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_donchian.IsFormed)
return;
var typedValue = (DonchianChannelsValue)donchianValue;
if (typedValue.UpperBand is not decimal upperBand ||
typedValue.LowerBand is not decimal lowerBand)
return;
var width = upperBand - lowerBand;
_widthHistory[_currentIndex] = width;
_currentIndex = (_currentIndex + 1) % LookbackPeriod;
if (_filledCount < LookbackPeriod)
_filledCount++;
if (_filledCount < LookbackPeriod)
return;
var avgWidth = 0m;
var sumSq = 0m;
for (var i = 0; i < LookbackPeriod; i++)
avgWidth += _widthHistory[i];
avgWidth /= LookbackPeriod;
for (var i = 0; i < LookbackPeriod; i++)
{
var diff = _widthHistory[i] - avgWidth;
sumSq += diff * diff;
}
var stdWidth = (decimal)Math.Sqrt((double)(sumSq / LookbackPeriod));
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var narrowThreshold = avgWidth - stdWidth * DeviationMultiplier;
var wideThreshold = avgWidth + stdWidth * DeviationMultiplier;
if (Position == 0)
{
if (width < narrowThreshold)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (width > wideThreshold)
{
SellMarket();
_cooldown = CooldownBars;
}
}
else if (Position > 0 && width >= avgWidth)
{
SellMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
else if (Position < 0 && width <= avgWidth)
{
BuyMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan, Math
from StockSharp.Messages import DataType, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import DonchianChannels
from StockSharp.Algo.Strategies import Strategy
class donchian_width_mean_reversion_strategy(Strategy):
"""
Donchian width mean reversion strategy.
Trades contractions and expansions of Donchian Channel width around its recent average.
"""
def __init__(self):
super(donchian_width_mean_reversion_strategy, self).__init__()
self._donchian_period = self.Param("DonchianPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Donchian Period", "Donchian Channel period", "Indicators")
self._lookback_period = self.Param("LookbackPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Lookback Period", "Lookback period for width statistics", "Strategy Parameters")
self._deviation_multiplier = self.Param("DeviationMultiplier", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("Deviation Multiplier", "Deviation multiplier for mean reversion detection", "Strategy Parameters")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management")
self._cooldown_bars = self.Param("CooldownBars", 1200) \
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candle type for strategy", "General")
self._donchian = None
self._width_history = None
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(donchian_width_mean_reversion_strategy, self).OnReseted()
self._donchian = None
lb = int(self._lookback_period.Value)
self._width_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
def OnStarted2(self, time):
super(donchian_width_mean_reversion_strategy, self).OnStarted2(time)
lb = int(self._lookback_period.Value)
self._width_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._donchian = DonchianChannels()
self._donchian.Length = int(self._donchian_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(self._donchian, self._process_donchian).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._donchian)
self.DrawOwnTrades(area)
self.StartProtection(Unit(), Unit(self._stop_loss_percent.Value, UnitTypes.Percent))
def _process_donchian(self, candle, donchian_value):
if candle.State != CandleStates.Finished:
return
if not self._donchian.IsFormed:
return
upper_band = donchian_value.UpperBand
lower_band = donchian_value.LowerBand
if upper_band is None or lower_band is None:
return
upper_val = float(upper_band)
lower_val = float(lower_band)
width = upper_val - lower_val
lb = int(self._lookback_period.Value)
self._width_history[self._current_index] = width
self._current_index = (self._current_index + 1) % lb
if self._filled_count < lb:
self._filled_count += 1
if self._filled_count < lb:
return
avg_width = 0.0
for i in range(lb):
avg_width += self._width_history[i]
avg_width /= float(lb)
sum_sq = 0.0
for i in range(lb):
diff = self._width_history[i] - avg_width
sum_sq += diff * diff
std_width = math.sqrt(sum_sq / float(lb))
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown > 0:
self._cooldown -= 1
return
dm = float(self._deviation_multiplier.Value)
narrow_threshold = avg_width - std_width * dm
wide_threshold = avg_width + std_width * dm
if self.Position == 0:
if width < narrow_threshold:
self.BuyMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif width > wide_threshold:
self.SellMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position > 0 and width >= avg_width:
self.SellMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position < 0 and width <= avg_width:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
def CreateClone(self):
return donchian_width_mean_reversion_strategy()