Stochastic Dynamic Zones
Stochastic Dynamic Zones 策略基于 Stochastic Oscillator with Dynamic Overbought/Oversold Zones。
测试表明年均收益约为 52%,该策略在加密市场表现最佳。
当 Stochastic confirms trend changes 在日内(5m)数据上得到确认时触发信号,适合积极交易者。
止损依赖于 ATR 倍数以及 StochPeriod, StochKPeriod 等参数,可根据需要调整以平衡风险与收益。
详情
- 入场条件:参见指标条件实现.
- 多空方向:双向.
- 退出条件:反向信号或止损逻辑.
- 止损:是,基于指标计算.
- 默认值:
StochPeriod = 14StochKPeriod = 3StochDPeriod = 3LookbackPeriod = 20StandardDeviationFactor = 2.0mCandleType = TimeSpan.FromMinutes(5).TimeFrame()
- 过滤器:
- 分类: 趋势跟随
- 方向: 双向
- 指标: Stochastic
- 止损: 是
- 复杂度: 中等
- 时间框架: 日内 (5m)
- 季节性: 否
- 神经网络: 否
- 背离: 否
- 风险等级: 中等
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Strategy based on Stochastic Oscillator with dynamic overbought and oversold zones.
/// </summary>
public class StochasticWithDynamicZonesStrategy : Strategy
{
private readonly StrategyParam<int> _stochKPeriod;
private readonly StrategyParam<int> _stochDPeriod;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _stdDevFactor;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevStochK;
private decimal _stochSum;
private decimal _stochSqSum;
private int _stochCount;
private int _cooldownRemaining;
private DateTimeOffset? _lastEntryTime;
private bool _wasBelowOversold;
private readonly Queue<decimal> _stochQueue = new();
public int StochKPeriod
{
get => _stochKPeriod.Value;
set => _stochKPeriod.Value = value;
}
public int StochDPeriod
{
get => _stochDPeriod.Value;
set => _stochDPeriod.Value = value;
}
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
public decimal StdDevFactor
{
get => _stdDevFactor.Value;
set => _stdDevFactor.Value = value;
}
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public StochasticWithDynamicZonesStrategy()
{
_stochKPeriod = Param(nameof(StochKPeriod), 14)
.SetDisplay("Stoch %K Period", "Smoothing period for %K", "Indicators");
_stochDPeriod = Param(nameof(StochDPeriod), 3)
.SetDisplay("Stoch %D Period", "Smoothing period for %D", "Indicators");
_lookbackPeriod = Param(nameof(LookbackPeriod), 40)
.SetDisplay("Lookback Period", "Period for dynamic zones", "Indicators");
_stdDevFactor = Param(nameof(StdDevFactor), 3.0m)
.SetDisplay("StdDev Factor", "Factor for dynamic zones", "Indicators");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 240)
.SetDisplay("Signal Cooldown", "Bars to wait between signals", "Trading")
.SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).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();
_prevStochK = 50m;
_stochSum = 0m;
_stochSqSum = 0m;
_stochCount = 0;
_cooldownRemaining = 0;
_lastEntryTime = null;
_wasBelowOversold = false;
_stochQueue.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevStochK = 50m;
_stochSum = 0m;
_stochSqSum = 0m;
_stochCount = 0;
_cooldownRemaining = 0;
_lastEntryTime = null;
_wasBelowOversold = false;
_stochQueue.Clear();
var stochastic = new StochasticOscillator
{
K = { Length = StochKPeriod },
D = { Length = StochDPeriod },
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(stochastic, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue stochValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!stochValue.IsFormed)
return;
var stochTyped = (StochasticOscillatorValue)stochValue;
if (stochTyped.K is not decimal stochK)
return;
_stochQueue.Enqueue(stochK);
_stochSum += stochK;
_stochSqSum += stochK * stochK;
_stochCount++;
if (_stochCount > LookbackPeriod)
{
var removed = _stochQueue.Dequeue();
_stochSum -= removed;
_stochSqSum -= removed * removed;
_stochCount = LookbackPeriod;
}
if (_stochCount < LookbackPeriod)
{
_prevStochK = stochK;
return;
}
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var average = _stochSum / _stochCount;
var variance = (_stochSqSum / _stochCount) - (average * average);
var stdDev = variance <= 0m ? 0m : (decimal)Math.Sqrt((double)variance);
var dynamicOversold = Math.Max(10m, average - StdDevFactor * stdDev);
var entryOversold = Math.Min(dynamicOversold, 10m);
var isReversingUp = stochK > _prevStochK;
if (Position > 0 && stochK >= 50m)
{
SellMarket();
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && !HasEntryToday(candle) && _wasBelowOversold && stochK >= entryOversold && isReversingUp && Position == 0)
{
BuyMarket();
_cooldownRemaining = SignalCooldownBars;
_lastEntryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
}
_wasBelowOversold = stochK < entryOversold;
_prevStochK = stochK;
}
private bool HasEntryToday(ICandleMessage candle)
{
if (!_lastEntryTime.HasValue)
return false;
var candleTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
return (candleTime.Date - _lastEntryTime.Value.Date).TotalDays < 3;
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from System.Collections.Generic import Queue
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import StochasticOscillator
from StockSharp.Algo.Strategies import Strategy
class stochastic_with_dynamic_zones_strategy(Strategy):
"""
Strategy based on Stochastic Oscillator with dynamic overbought and oversold zones.
"""
def __init__(self):
super(stochastic_with_dynamic_zones_strategy, self).__init__()
self._stoch_k_period = self.Param("StochKPeriod", 14) \
.SetDisplay("Stoch %K Period", "Smoothing period for %K", "Indicators")
self._stoch_d_period = self.Param("StochDPeriod", 3) \
.SetDisplay("Stoch %D Period", "Smoothing period for %D", "Indicators")
self._lookback_period = self.Param("LookbackPeriod", 40) \
.SetDisplay("Lookback Period", "Period for dynamic zones", "Indicators")
self._std_dev_factor = self.Param("StdDevFactor", 3.0) \
.SetDisplay("StdDev Factor", "Factor for dynamic zones", "Indicators")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 240) \
.SetDisplay("Signal Cooldown", "Bars to wait between signals", "Trading") \
.SetGreaterThanZero()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._prev_stoch_k = 50.0
self._stoch_sum = 0.0
self._stoch_sq_sum = 0.0
self._stoch_count = 0
self._cooldown_remaining = 0
self._last_entry_time = None
self._was_below_oversold = False
self._stoch_queue = []
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(stochastic_with_dynamic_zones_strategy, self).OnReseted()
self._prev_stoch_k = 50.0
self._stoch_sum = 0.0
self._stoch_sq_sum = 0.0
self._stoch_count = 0
self._cooldown_remaining = 0
self._last_entry_time = None
self._was_below_oversold = False
self._stoch_queue = []
def OnStarted2(self, time):
super(stochastic_with_dynamic_zones_strategy, self).OnStarted2(time)
self._prev_stoch_k = 50.0
self._stoch_sum = 0.0
self._stoch_sq_sum = 0.0
self._stoch_count = 0
self._cooldown_remaining = 0
self._last_entry_time = None
self._was_below_oversold = False
self._stoch_queue = []
stochastic = StochasticOscillator()
stochastic.K.Length = int(self._stoch_k_period.Value)
stochastic.D.Length = int(self._stoch_d_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(stochastic, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, stoch_value):
if candle.State != CandleStates.Finished:
return
if not stoch_value.IsFormed:
return
stoch_k_val = stoch_value.K
if stoch_k_val is None:
return
stoch_k = float(stoch_k_val)
lookback = int(self._lookback_period.Value)
self._stoch_queue.append(stoch_k)
self._stoch_sum += stoch_k
self._stoch_sq_sum += stoch_k * stoch_k
self._stoch_count += 1
if self._stoch_count > lookback:
removed = self._stoch_queue.pop(0)
self._stoch_sum -= removed
self._stoch_sq_sum -= removed * removed
self._stoch_count = lookback
if self._stoch_count < lookback:
self._prev_stoch_k = stoch_k
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
average = self._stoch_sum / self._stoch_count
variance = (self._stoch_sq_sum / self._stoch_count) - (average * average)
std_dev = 0.0 if variance <= 0 else math.sqrt(variance)
sdf = float(self._std_dev_factor.Value)
dynamic_oversold = max(10.0, average - sdf * std_dev)
entry_oversold = min(dynamic_oversold, 10.0)
is_reversing_up = stoch_k > self._prev_stoch_k
cd = int(self._signal_cooldown_bars.Value)
if self.Position > 0 and stoch_k >= 50.0:
self.SellMarket()
self._cooldown_remaining = cd
elif self._cooldown_remaining == 0 and not self._has_entry_today(candle) and self._was_below_oversold and stoch_k >= entry_oversold and is_reversing_up and self.Position == 0:
self.BuyMarket()
self._cooldown_remaining = cd
close_time = candle.CloseTime
open_time = candle.OpenTime
try:
if close_time is not None and str(close_time) != "01/01/0001 00:00:00 +00:00":
self._last_entry_time = close_time
else:
self._last_entry_time = open_time
except:
self._last_entry_time = open_time
self._was_below_oversold = stoch_k < entry_oversold
self._prev_stoch_k = stoch_k
def _has_entry_today(self, candle):
if self._last_entry_time is None:
return False
close_time = candle.CloseTime
open_time = candle.OpenTime
try:
if close_time is not None and str(close_time) != "01/01/0001 00:00:00 +00:00":
candle_time = close_time
else:
candle_time = open_time
except:
candle_time = open_time
try:
diff_days = (candle_time.Date - self._last_entry_time.Date).TotalDays
return diff_days < 3
except:
return False
def CreateClone(self):
return stochastic_with_dynamic_zones_strategy()