Donchian Seasonal Filter
Donchian Seasonal Filter 策略基于 Donchian Channels with seasonal filter。
测试表明年均收益约为 70%,该策略在股票市场表现最佳。
当 Donchian confirms filtered entries 在日内(15m)数据上得到确认时触发信号,适合积极交易者。
止损依赖于 ATR 倍数以及 DonchianPeriod, SeasonalThreshold 等参数,可根据需要调整以平衡风险与收益。
详情
- 入场条件:参见指标条件实现.
- 多空方向:双向.
- 退出条件:反向信号或止损逻辑.
- 止损:是,基于指标计算.
- 默认值:
DonchianPeriod = 20SeasonalThreshold = 0.5mCandleType = TimeSpan.FromMinutes(15).TimeFrame()
- 过滤器:
- 分类: 趋势跟随
- 方向: 双向
- 指标: Donchian, Seasonal
- 止损: 是
- 复杂度: 中等
- 时间框架: 日内 (15m)
- 季节性: 是
- 神经网络: 否
- 背离: 否
- 风险等级: 中等
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()