Stochastic Dynamic Zones
The Stochastic Dynamic Zones strategy is built around Stochastic Oscillator with Dynamic Overbought/Oversold Zones.
Testing indicates an average annual return of about 52%. It performs best in the crypto market.
Signals trigger when Stochastic confirms trend changes on intraday (5m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like StochPeriod, StochKPeriod. 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:
StochPeriod = 14StochKPeriod = 3StochDPeriod = 3LookbackPeriod = 20StandardDeviationFactor = 2.0mCandleType = TimeSpan.FromMinutes(5).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Stochastic
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
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()