Adaptive KDJ (MTF)
The Adaptive KDJ strategy blends KDJ oscillator values from three timeframes. Each timeframe is smoothed with an EMA and combined using adjustable weights. Trend strength is measured with an SMA of the combined oscillator, which adapts the overbought and oversold levels.
The strategy enters long when the J line is below the adaptive buy level and the K line crosses above the D line. It enters short when the J line is above the adaptive sell level and the K line crosses below the D line.
Details
- Entry Criteria: KDJ cross with J below/above dynamic levels.
- Long/Short: Both directions.
- Exit Criteria: Opposite signal.
- Stops: No.
- Default Values:
TimeFrame1= TimeSpan.FromMinutes(1)TimeFrame2= TimeSpan.FromMinutes(3)TimeFrame3= TimeSpan.FromMinutes(15)KdjLength= 9SmoothingLength= 5TrendLength= 40WeightOption= 1
- Filters:
- Category: Oscillator
- Direction: Both
- Indicators: Stochastic, EMA, SMA
- Stops: No
- Complexity: Intermediate
- Timeframe: Intraday
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Strategy based on KDJ (Stochastic-based) signals with smoothing.
/// Uses Stochastic K/D crossover with J-value extremes for entry/exit.
/// </summary>
public class AdaptiveKdjMtfStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _kdjLength;
private readonly StrategyParam<int> _smoothingLength;
private readonly StrategyParam<decimal> _buyLevel;
private readonly StrategyParam<decimal> _sellLevel;
private readonly StrategyParam<int> _cooldownBars;
private decimal _prevK;
private decimal _prevD;
private decimal _smoothK;
private decimal _smoothD;
private bool _hasPrev;
private int _cooldownRemaining;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int KdjLength { get => _kdjLength.Value; set => _kdjLength.Value = value; }
public int SmoothingLength { get => _smoothingLength.Value; set => _smoothingLength.Value = value; }
public decimal BuyLevel { get => _buyLevel.Value; set => _buyLevel.Value = value; }
public decimal SellLevel { get => _sellLevel.Value; set => _sellLevel.Value = value; }
public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
public AdaptiveKdjMtfStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle type", "General");
_kdjLength = Param(nameof(KdjLength), 9)
.SetGreaterThanZero()
.SetDisplay("KDJ Length", "Base length for Stochastic", "Parameters")
.SetOptimize(3, 15, 3);
_smoothingLength = Param(nameof(SmoothingLength), 5)
.SetGreaterThanZero()
.SetDisplay("Smoothing Length", "EMA smoothing length", "Parameters")
.SetOptimize(3, 15, 2);
_buyLevel = Param(nameof(BuyLevel), 30m)
.SetDisplay("Buy Level", "J value threshold for buy signal", "Parameters")
.SetOptimize(15m, 40m, 5m);
_sellLevel = Param(nameof(SellLevel), 70m)
.SetDisplay("Sell Level", "J value threshold for sell signal", "Parameters")
.SetOptimize(60m, 85m, 5m);
_cooldownBars = Param(nameof(CooldownBars), 10)
.SetDisplay("Cooldown Bars", "Bars between trades", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevK = 50m;
_prevD = 50m;
_smoothK = 50m;
_smoothD = 50m;
_hasPrev = false;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var stoch = new StochasticOscillator();
stoch.K.Length = KdjLength;
stoch.D.Length = 3;
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(stoch, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, stoch);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue stochValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (stochValue.IsEmpty)
return;
var sv = (IStochasticOscillatorValue)stochValue;
if (sv.K is not decimal k || sv.D is not decimal d)
return;
// Compute J = 3K - 2D (KDJ formula)
var j = 3m * k - 2m * d;
// EMA smoothing
var alpha = 2m / (SmoothingLength + 1m);
_smoothK = alpha * k + (1m - alpha) * _smoothK;
_smoothD = alpha * d + (1m - alpha) * _smoothD;
var smoothJ = alpha * j + (1m - alpha) * j;
if (!_hasPrev)
{
_prevK = _smoothK;
_prevD = _smoothD;
_hasPrev = true;
return;
}
if (_cooldownRemaining > 0)
{
_cooldownRemaining--;
_prevK = _smoothK;
_prevD = _smoothD;
return;
}
var crossUp = _prevK <= _prevD && _smoothK > _smoothD;
var crossDown = _prevK >= _prevD && _smoothK < _smoothD;
var buySignal = smoothJ < BuyLevel && crossUp;
var sellSignal = smoothJ > SellLevel && crossDown;
if (buySignal && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
_cooldownRemaining = CooldownBars;
}
else if (sellSignal && Position >= 0)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(Volume);
_cooldownRemaining = CooldownBars;
}
_prevK = _smoothK;
_prevD = _smoothD;
}
}
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, CandleStates
from StockSharp.Algo.Indicators import StochasticOscillator
from StockSharp.Algo.Strategies import Strategy
class adaptive_kdj_mtf_strategy(Strategy):
"""Adaptive KDJ MTF Strategy."""
def __init__(self):
super(adaptive_kdj_mtf_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Candle type", "General")
self._kdj_length = self.Param("KdjLength", 9) \
.SetDisplay("KDJ Length", "Base length for Stochastic", "Parameters")
self._smoothing_length = self.Param("SmoothingLength", 5) \
.SetDisplay("Smoothing Length", "EMA smoothing length", "Parameters")
self._buy_level = self.Param("BuyLevel", 30.0) \
.SetDisplay("Buy Level", "J value threshold for buy signal", "Parameters")
self._sell_level = self.Param("SellLevel", 70.0) \
.SetDisplay("Sell Level", "J value threshold for sell signal", "Parameters")
self._cooldown_bars = self.Param("CooldownBars", 10) \
.SetDisplay("Cooldown Bars", "Bars between trades", "Risk")
self._prev_k = 50.0
self._prev_d = 50.0
self._smooth_k = 50.0
self._smooth_d = 50.0
self._has_prev = False
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(adaptive_kdj_mtf_strategy, self).OnReseted()
self._prev_k = 50.0
self._prev_d = 50.0
self._smooth_k = 50.0
self._smooth_d = 50.0
self._has_prev = False
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(adaptive_kdj_mtf_strategy, self).OnStarted2(time)
stoch = StochasticOscillator()
stoch.K.Length = int(self._kdj_length.Value)
stoch.D.Length = 3
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(stoch, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, stoch)
self.DrawOwnTrades(area)
def _on_process(self, candle, stoch_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if stoch_value.IsEmpty:
return
if stoch_value.K is None or stoch_value.D is None:
return
k = float(stoch_value.K)
d = float(stoch_value.D)
j = 3.0 * k - 2.0 * d
alpha = 2.0 / (float(self._smoothing_length.Value) + 1.0)
self._smooth_k = alpha * k + (1.0 - alpha) * self._smooth_k
self._smooth_d = alpha * d + (1.0 - alpha) * self._smooth_d
smooth_j = alpha * j + (1.0 - alpha) * j
cooldown = int(self._cooldown_bars.Value)
if not self._has_prev:
self._prev_k = self._smooth_k
self._prev_d = self._smooth_d
self._has_prev = True
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._prev_k = self._smooth_k
self._prev_d = self._smooth_d
return
cross_up = self._prev_k <= self._prev_d and self._smooth_k > self._smooth_d
cross_down = self._prev_k >= self._prev_d and self._smooth_k < self._smooth_d
buy_signal = smooth_j < float(self._buy_level.Value) and cross_up
sell_signal = smooth_j > float(self._sell_level.Value) and cross_down
if buy_signal and self.Position <= 0:
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self.BuyMarket(self.Volume)
self._cooldown_remaining = cooldown
elif sell_signal and self.Position >= 0:
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self.SellMarket(self.Volume)
self._cooldown_remaining = cooldown
self._prev_k = self._smooth_k
self._prev_d = self._smooth_d
def CreateClone(self):
return adaptive_kdj_mtf_strategy()