Donchian Seasonal Filter
The Donchian Seasonal Filter strategy is built around Donchian Channels with seasonal filter.
Testing indicates an average annual return of about 70%. It performs best in the stocks market.
Signals trigger when Donchian confirms filtered entries on intraday (15m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like DonchianPeriod, SeasonalThreshold. Adjust these defaults to balance risk and reward.
Details
- Entry Criteria: see implementation for indicator conditions.
- Long/Short: Both directions.
- Exit Criteria: opposite signal or stop logic.
- Stops: Yes, using indicator-based calculations.
- Default Values:
DonchianPeriod = 20SeasonalThreshold = 0.5mCandleType = TimeSpan.FromMinutes(15).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Donchian, Seasonal
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (15m)
- Seasonality: Yes
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy based on Donchian Channels with seasonal filter.
/// </summary>
public class DonchianSeasonalStrategy : Strategy
{
private readonly StrategyParam<int> _donchianPeriod;
private readonly StrategyParam<decimal> _seasonalThreshold;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _seasonalDataCount;
private DonchianChannels _donchian = null!;
// Seasonal data storage
private readonly SynchronizedDictionary<Months, decimal> _monthlyReturns = [];
private decimal _seasonalStrength;
private decimal? _previousUpperBand;
private decimal? _previousLowerBand;
private decimal? _previousMiddleBand;
private decimal? _previousClosePrice;
private int _cooldownRemaining;
/// <summary>
/// Donchian Channel period.
/// </summary>
public int DonchianPeriod
{
get => _donchianPeriod.Value;
set => _donchianPeriod.Value = value;
}
/// <summary>
/// Seasonal strength threshold for entry.
/// </summary>
public decimal SeasonalThreshold
{
get => _seasonalThreshold.Value;
set => _seasonalThreshold.Value = value;
}
/// <summary>
/// Number of years used for seasonal analysis.
/// </summary>
public int SeasonalDataCount
{
get => _seasonalDataCount.Value;
set => _seasonalDataCount.Value = value;
}
/// <summary>
/// Number of closed candles to wait before allowing the next entry.
/// </summary>
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
/// <summary>
/// Candle type to use for the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="DonchianSeasonalStrategy"/>.
/// </summary>
public DonchianSeasonalStrategy()
{
_donchianPeriod = Param(nameof(DonchianPeriod), 40)
.SetDisplay("Donchian Period", "Donchian Channel period", "Donchian")
.SetOptimize(10, 50, 5);
_seasonalThreshold = Param(nameof(SeasonalThreshold), 0.5m)
.SetDisplay("Seasonal Threshold", "Seasonal strength threshold for entry", "Seasonal")
.SetOptimize(0.2m, 1.0m, 0.1m);
_seasonalDataCount = Param(nameof(SeasonalDataCount), 5)
.SetDisplay("Seasonal Years", "Years of seasonal data", "Seasonal")
.SetGreaterThanZero()
.SetOptimize(1, 10, 1);
_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
.SetNotNegative()
.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before a new breakout entry", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
// Initialize monthly returns with neutral values
foreach (Months month in Enum.GetValues(typeof(Months)))
{
_monthlyReturns[month] = 0m;
}
// Simulated historical seasonal data (in a real strategy, this would come from analysis of historical data)
// These are example values that suggest certain months tend to be bullish or bearish
_monthlyReturns[Months.January] = 0.8m;
_monthlyReturns[Months.February] = 0.3m;
_monthlyReturns[Months.March] = 0.6m;
_monthlyReturns[Months.April] = 0.9m;
_monthlyReturns[Months.May] = 0.2m;
_monthlyReturns[Months.June] = -0.4m;
_monthlyReturns[Months.July] = -0.2m;
_monthlyReturns[Months.August] = -0.7m;
_monthlyReturns[Months.September] = -0.9m;
_monthlyReturns[Months.October] = -0.1m;
_monthlyReturns[Months.November] = 0.5m;
_monthlyReturns[Months.December] = 0.7m;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_seasonalStrength = 0;
_previousUpperBand = null;
_previousLowerBand = null;
_previousMiddleBand = null;
_previousClosePrice = null;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create Donchian Channel indicator
_donchian = new DonchianChannels
{
Length = DonchianPeriod
};
// Create subscription and bind indicator
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_donchian, ProcessCandle)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _donchian);
DrawOwnTrades(area);
}
// Setup position protection
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(2, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue donchianValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var donchianTyped = (DonchianChannelsValue)donchianValue;
if (donchianTyped.UpperBand is not decimal upperBand ||
donchianTyped.LowerBand is not decimal lowerBand ||
donchianTyped.Middle is not decimal middleBand)
{
return;
}
// Calculate seasonal strength for current month
UpdateSeasonalStrength(candle.OpenTime);
if (_previousUpperBand is null || _previousLowerBand is null || _previousMiddleBand is null || _previousClosePrice is null)
{
_previousUpperBand = upperBand;
_previousLowerBand = lowerBand;
_previousMiddleBand = middleBand;
_previousClosePrice = candle.ClosePrice;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_previousUpperBand = upperBand;
_previousLowerBand = lowerBand;
_previousMiddleBand = middleBand;
_previousClosePrice = candle.ClosePrice;
return;
}
var previousUpperBand = _previousUpperBand.Value;
var previousLowerBand = _previousLowerBand.Value;
var previousMiddleBand = _previousMiddleBand.Value;
var previousClosePrice = _previousClosePrice.Value;
// Trading logic
// Donchian channels include the current bar, so the breakout must be checked against the previous channel.
if (Position > 0 && candle.ClosePrice < previousMiddleBand)
{
SellMarket(Position);
_cooldownRemaining = SignalCooldownBars;
}
else if (Position < 0 && candle.ClosePrice > previousMiddleBand)
{
BuyMarket(-Position);
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 &&
previousClosePrice <= previousUpperBand &&
candle.ClosePrice > previousUpperBand &&
_seasonalStrength > SeasonalThreshold &&
Position <= 0)
{
BuyMarket(Volume + (Position < 0 ? -Position : 0m));
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 &&
previousClosePrice >= previousLowerBand &&
candle.ClosePrice < previousLowerBand &&
_seasonalStrength < -SeasonalThreshold &&
Position >= 0)
{
SellMarket(Volume + (Position > 0 ? Position : 0m));
_cooldownRemaining = SignalCooldownBars;
}
_previousUpperBand = upperBand;
_previousLowerBand = lowerBand;
_previousMiddleBand = middleBand;
_previousClosePrice = candle.ClosePrice;
}
private void UpdateSeasonalStrength(DateTimeOffset time)
{
// Get current month
Months currentMonth = (Months)time.Month;
// Get historical return for this month
_seasonalStrength = _monthlyReturns[currentMonth];
// Log seasonal information at the beginning of each month
if (time.Day == 1)
{
LogInfo($"Monthly Seasonal Data: {currentMonth} has historical strength of {_seasonalStrength:F2} over {SeasonalDataCount} years");
}
}
/// <summary>
/// Enumeration for months of the year.
/// </summary>
private enum Months
{
January = 1,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
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_seasonal_filter_strategy(Strategy):
"""
Strategy based on Donchian Channels with seasonal filter.
"""
def __init__(self):
super(donchian_seasonal_filter_strategy, self).__init__()
self._donchian_period = self.Param("DonchianPeriod", 40) \
.SetDisplay("Donchian Period", "Donchian Channel period", "Donchian") \
.SetCanOptimize(True) \
.SetOptimize(10, 50, 5)
self._seasonal_threshold = self.Param("SeasonalThreshold", 0.5) \
.SetDisplay("Seasonal Threshold", "Seasonal strength threshold for entry", "Seasonal") \
.SetCanOptimize(True) \
.SetOptimize(0.2, 1.0, 0.1)
self._seasonal_data_count = self.Param("SeasonalDataCount", 5) \
.SetGreaterThanZero() \
.SetDisplay("Seasonal Years", "Years of seasonal data", "Seasonal") \
.SetCanOptimize(True) \
.SetOptimize(1, 10, 1)
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 12) \
.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before a new breakout entry", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._donchian = None
self._monthly_returns = {}
self._seasonal_strength = 0.0
self._previous_upper_band = None
self._previous_lower_band = None
self._previous_middle_band = None
self._previous_close_price = None
self._cooldown_remaining = 0
# Initialize monthly returns
self._monthly_returns[1] = 0.8
self._monthly_returns[2] = 0.3
self._monthly_returns[3] = 0.6
self._monthly_returns[4] = 0.9
self._monthly_returns[5] = 0.2
self._monthly_returns[6] = -0.4
self._monthly_returns[7] = -0.2
self._monthly_returns[8] = -0.7
self._monthly_returns[9] = -0.9
self._monthly_returns[10] = -0.1
self._monthly_returns[11] = 0.5
self._monthly_returns[12] = 0.7
@property
def DonchianPeriod(self):
return self._donchian_period.Value
@DonchianPeriod.setter
def DonchianPeriod(self, value):
self._donchian_period.Value = value
@property
def SeasonalThreshold(self):
return self._seasonal_threshold.Value
@SeasonalThreshold.setter
def SeasonalThreshold(self, value):
self._seasonal_threshold.Value = value
@property
def SeasonalDataCount(self):
return self._seasonal_data_count.Value
@SeasonalDataCount.setter
def SeasonalDataCount(self, value):
self._seasonal_data_count.Value = value
@property
def SignalCooldownBars(self):
return self._signal_cooldown_bars.Value
@SignalCooldownBars.setter
def SignalCooldownBars(self, value):
self._signal_cooldown_bars.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnReseted(self):
super(donchian_seasonal_filter_strategy, self).OnReseted()
self._donchian = None
self._seasonal_strength = 0.0
self._previous_upper_band = None
self._previous_lower_band = None
self._previous_middle_band = None
self._previous_close_price = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(donchian_seasonal_filter_strategy, self).OnStarted2(time)
self._donchian = DonchianChannels()
self._donchian.Length = self.DonchianPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(self._donchian, self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._donchian)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(2, UnitTypes.Percent)
)
def ProcessCandle(self, candle, donchian_value):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
if donchian_value.UpperBand is None or donchian_value.LowerBand is None or donchian_value.Middle is None:
return
upper_band = float(donchian_value.UpperBand)
lower_band = float(donchian_value.LowerBand)
middle_band = float(donchian_value.Middle)
self.UpdateSeasonalStrength(candle.OpenTime)
if self._previous_upper_band is None or self._previous_lower_band is None or self._previous_middle_band is None or self._previous_close_price is None:
self._previous_upper_band = upper_band
self._previous_lower_band = lower_band
self._previous_middle_band = middle_band
self._previous_close_price = float(candle.ClosePrice)
return
if not self.IsFormedAndOnlineAndAllowTrading():
self._previous_upper_band = upper_band
self._previous_lower_band = lower_band
self._previous_middle_band = middle_band
self._previous_close_price = float(candle.ClosePrice)
return
close_price = float(candle.ClosePrice)
if self.Position > 0 and close_price < self._previous_middle_band:
self.SellMarket(self.Position)
self._cooldown_remaining = self.SignalCooldownBars
elif self.Position < 0 and close_price > self._previous_middle_band:
self.BuyMarket(abs(self.Position))
self._cooldown_remaining = self.SignalCooldownBars
elif self._cooldown_remaining == 0 and \
self._previous_close_price <= self._previous_upper_band and \
close_price > self._previous_upper_band and \
self._seasonal_strength > self.SeasonalThreshold and \
self.Position <= 0:
vol = self.Volume
if self.Position < 0:
vol = self.Volume + abs(self.Position)
self.BuyMarket(vol)
self._cooldown_remaining = self.SignalCooldownBars
elif self._cooldown_remaining == 0 and \
self._previous_close_price >= self._previous_lower_band and \
close_price < self._previous_lower_band and \
self._seasonal_strength < -self.SeasonalThreshold and \
self.Position >= 0:
vol = self.Volume
if self.Position > 0:
vol = self.Volume + self.Position
self.SellMarket(vol)
self._cooldown_remaining = self.SignalCooldownBars
self._previous_upper_band = upper_band
self._previous_lower_band = lower_band
self._previous_middle_band = middle_band
self._previous_close_price = close_price
def UpdateSeasonalStrength(self, time):
current_month = time.Month
self._seasonal_strength = self._monthly_returns.get(current_month, 0.0)
def CreateClone(self):
return donchian_seasonal_filter_strategy()