自适应KDJ (MTF)
自适应KDJ策略将三个不同时间框架的KDJ振荡器值进行加权平均,并使用EMA进行平滑处理。合成振荡器的SMA用来衡量趋势强度,从而动态调整超买和超卖水平。
当J线位于自适应买入水平下方且K线向上穿越D线时,策略做多。J线高于自适应卖出水平且K线向下穿越D线时,策略做空。
详情
- 入场条件: KDJ交叉且J线低于/高于动态水平。
- 多空方向: 双向。
- 出场条件: 反向信号。
- 止损: 无。
- 默认值:
TimeFrame1= TimeSpan.FromMinutes(1)TimeFrame2= TimeSpan.FromMinutes(3)TimeFrame3= TimeSpan.FromMinutes(15)KdjLength= 9SmoothingLength= 5TrendLength= 40WeightOption= 1
- 过滤器:
- 类别: Oscillator
- 方向: 双向
- 指标: Stochastic, EMA, SMA
- 止损: 无
- 复杂度: 中等
- 时间框架: 日内
- 季节性: 无
- 神经网络: 无
- 背离: 无
- 风险等级: 中等
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Adaptive KDJ (MTF) strategy.
/// KDJ is calculated on TimeFrame1, TimeFrame2 and TimeFrame3, each line is smoothed by an EMA of SmoothingLength and the three
/// timeframes are blended with the weights selected by WeightOption. An SMA of the blended J over TrendLength bars measures the trend:
/// above 50 the oversold/overbought levels are 40/80, otherwise 20/60. A long opens when J is below the oversold level and K crosses
/// above D, a short when J is above the overbought level and K crosses below D; the opposite signal reverses the position.
/// </summary>
public class AdaptiveKdjMtfStrategy : Strategy
{
private sealed class KdjState
{
public readonly List<decimal> Highs = [];
public readonly List<decimal> Lows = [];
public decimal? K;
public decimal? D;
public decimal? SmoothK;
public decimal? SmoothD;
public decimal? SmoothJ;
public int Count;
}
private readonly StrategyParam<DataType> _timeFrame1;
private readonly StrategyParam<DataType> _timeFrame2;
private readonly StrategyParam<DataType> _timeFrame3;
private readonly StrategyParam<int> _kdjLength;
private readonly StrategyParam<int> _smoothingLength;
private readonly StrategyParam<int> _trendLength;
private readonly StrategyParam<int> _weightOption;
private KdjState[] _states;
private SimpleMovingAverage _trend;
private decimal? _prevK;
private decimal? _prevD;
/// <summary>
/// Shortest timeframe, also used for signals.
/// </summary>
public DataType TimeFrame1
{
get => _timeFrame1.Value;
set => _timeFrame1.Value = value;
}
/// <summary>
/// Middle timeframe.
/// </summary>
public DataType TimeFrame2
{
get => _timeFrame2.Value;
set => _timeFrame2.Value = value;
}
/// <summary>
/// Longest timeframe.
/// </summary>
public DataType TimeFrame3
{
get => _timeFrame3.Value;
set => _timeFrame3.Value = value;
}
/// <summary>
/// KDJ lookback.
/// </summary>
public int KdjLength
{
get => _kdjLength.Value;
set => _kdjLength.Value = value;
}
/// <summary>
/// EMA length that smooths each timeframe's KDJ.
/// </summary>
public int SmoothingLength
{
get => _smoothingLength.Value;
set => _smoothingLength.Value = value;
}
/// <summary>
/// SMA length of the trend strength.
/// </summary>
public int TrendLength
{
get => _trendLength.Value;
set => _trendLength.Value = value;
}
/// <summary>
/// Weighting of the timeframes: 1 favours the shortest (0.5/0.3/0.2), 2 is equal, 3 favours the longest (0.2/0.3/0.5).
/// </summary>
public int WeightOption
{
get => _weightOption.Value;
set => _weightOption.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public AdaptiveKdjMtfStrategy()
{
_timeFrame1 = Param(nameof(TimeFrame1), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Time Frame 1", "Shortest timeframe, also used for signals", "General");
_timeFrame2 = Param(nameof(TimeFrame2), TimeSpan.FromMinutes(3).TimeFrame())
.SetDisplay("Time Frame 2", "Middle timeframe", "General");
_timeFrame3 = Param(nameof(TimeFrame3), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Time Frame 3", "Longest timeframe", "General");
_kdjLength = Param(nameof(KdjLength), 9)
.SetGreaterThanZero()
.SetDisplay("KDJ Length", "KDJ lookback", "KDJ");
_smoothingLength = Param(nameof(SmoothingLength), 5)
.SetGreaterThanZero()
.SetDisplay("Smoothing Length", "EMA length that smooths each timeframe's KDJ", "KDJ");
_trendLength = Param(nameof(TrendLength), 40)
.SetGreaterThanZero()
.SetDisplay("Trend Length", "SMA length of the trend strength", "KDJ");
_weightOption = Param(nameof(WeightOption), 1)
.SetRange(1, 3)
.SetDisplay("Weight Option", "1 favours the shortest timeframe, 2 is equal, 3 favours the longest", "KDJ");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return new[] { TimeFrame1, TimeFrame2, TimeFrame3 }.Distinct().Select(dt => (Security, dt));
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_states = null;
_trend = null;
_prevK = null;
_prevD = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_states = [new(), new(), new()];
_trend = new SimpleMovingAverage { Length = TrendLength };
_prevK = null;
_prevD = null;
var main = SubscribeCandles(TimeFrame1);
main.Bind(ProcessMain).Start();
SubscribeCandles(TimeFrame2).Bind(c => UpdateKdj(_states[1], c)).Start();
SubscribeCandles(TimeFrame3).Bind(c => UpdateKdj(_states[2], c)).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, main);
DrawOwnTrades(area);
}
}
private void UpdateKdj(KdjState state, ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
state.Highs.Add(candle.HighPrice);
state.Lows.Add(candle.LowPrice);
if (state.Highs.Count > KdjLength)
{
state.Highs.RemoveAt(0);
state.Lows.RemoveAt(0);
}
if (state.Highs.Count < KdjLength)
return;
var highest = state.Highs.Max();
var lowest = state.Lows.Min();
var rsv = highest > lowest ? 100m * (candle.ClosePrice - lowest) / (highest - lowest) : 50m;
// Classic KDJ: K and D are 1/3 smoothings of RSV and K.
var k = ((state.K ?? 50m) * 2m + rsv) / 3m;
var d = ((state.D ?? 50m) * 2m + k) / 3m;
var j = 3m * k - 2m * d;
state.K = k;
state.D = d;
var alpha = 2m / (SmoothingLength + 1m);
state.SmoothK = state.SmoothK is decimal sk ? sk + alpha * (k - sk) : k;
state.SmoothD = state.SmoothD is decimal sd ? sd + alpha * (d - sd) : d;
state.SmoothJ = state.SmoothJ is decimal sj ? sj + alpha * (j - sj) : j;
state.Count++;
}
private bool IsReady(KdjState state) => state.Count >= SmoothingLength;
private void ProcessMain(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdateKdj(_states[0], candle);
if (!_states.All(IsReady))
return;
var (w1, w2, w3) = WeightOption switch
{
2 => (1m / 3m, 1m / 3m, 1m / 3m),
3 => (0.2m, 0.3m, 0.5m),
_ => (0.5m, 0.3m, 0.2m),
};
var k = w1 * _states[0].SmoothK.Value + w2 * _states[1].SmoothK.Value + w3 * _states[2].SmoothK.Value;
var d = w1 * _states[0].SmoothD.Value + w2 * _states[1].SmoothD.Value + w3 * _states[2].SmoothD.Value;
var j = w1 * _states[0].SmoothJ.Value + w2 * _states[1].SmoothJ.Value + w3 * _states[2].SmoothJ.Value;
var trend = _trend.Process(j, candle.ServerTime, true).ToDecimal();
var prevK = _prevK;
var prevD = _prevD;
_prevK = k;
_prevD = d;
if (!_trend.IsFormed || prevK is not decimal pk || prevD is not decimal pd)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var bullish = trend > 50m;
var buyLevel = bullish ? 40m : 20m;
var sellLevel = bullish ? 80m : 60m;
var crossUp = pk <= pd && k > d;
var crossDown = pk >= pd && k < d;
if (j < buyLevel && crossUp && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (j > sellLevel && crossDown && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class _kdj_state(object):
def __init__(self):
self.highs = []
self.lows = []
self.k = None
self.d = None
self.smooth_k = None
self.smooth_d = None
self.smooth_j = None
self.count = 0
class adaptive_kdj_mtf_strategy(Strategy):
"""
Adaptive KDJ (MTF) strategy.
KDJ is calculated on TimeFrame1, TimeFrame2 and TimeFrame3, each line is smoothed by an EMA of SmoothingLength and the three
timeframes are blended with the weights selected by WeightOption. An SMA of the blended J over TrendLength bars measures the trend:
above 50 the oversold/overbought levels are 40/80, otherwise 20/60. A long opens when J is below the oversold level and K crosses
above D, a short when J is above the overbought level and K crosses below D; the opposite signal reverses the position.
"""
def __init__(self):
super(adaptive_kdj_mtf_strategy, self).__init__()
self._time_frame1 = self.Param("TimeFrame1", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Time Frame 1", "Shortest timeframe, also used for signals", "General")
self._time_frame2 = self.Param("TimeFrame2", DataType.TimeFrame(TimeSpan.FromMinutes(3))).SetDisplay("Time Frame 2", "Middle timeframe", "General")
self._time_frame3 = self.Param("TimeFrame3", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Time Frame 3", "Longest timeframe", "General")
self._kdj_length = self.Param("KdjLength", 9).SetGreaterThanZero().SetDisplay("KDJ Length", "KDJ lookback", "KDJ")
self._smoothing_length = self.Param("SmoothingLength", 5).SetGreaterThanZero().SetDisplay("Smoothing Length", "EMA length that smooths each timeframe's KDJ", "KDJ")
self._trend_length = self.Param("TrendLength", 40).SetGreaterThanZero().SetDisplay("Trend Length", "SMA length of the trend strength", "KDJ")
self._weight_option = self.Param("WeightOption", 1).SetRange(1, 3).SetDisplay("Weight Option", "1 favours the shortest timeframe, 2 is equal, 3 favours the longest", "KDJ")
self._states = None
self._trend = None
self._prev_k = None
self._prev_d = None
def GetWorkingSecurities(self):
result = []
for dt in (self._time_frame1.Value, self._time_frame2.Value, self._time_frame3.Value):
if all(not dt.Equals(existing) for _, existing in result):
result.append((self.Security, dt))
return result
def OnReseted(self):
super(adaptive_kdj_mtf_strategy, self).OnReseted()
self._states = None
self._trend = None
self._prev_k = None
self._prev_d = None
def OnStarted2(self, time):
super(adaptive_kdj_mtf_strategy, self).OnStarted2(time)
self._states = [_kdj_state(), _kdj_state(), _kdj_state()]
self._trend = SimpleMovingAverage()
self._trend.Length = self._trend_length.Value
self._prev_k = None
self._prev_d = None
main = self.SubscribeCandles(self._time_frame1.Value)
main.Bind(self._process_main).Start()
self.SubscribeCandles(self._time_frame2.Value).Bind(lambda c: self._update_kdj(self._states[1], c)).Start()
self.SubscribeCandles(self._time_frame3.Value).Bind(lambda c: self._update_kdj(self._states[2], c)).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, main)
self.DrawOwnTrades(area)
def _update_kdj(self, state, candle):
if candle.State != CandleStates.Finished:
return
length = self._kdj_length.Value
state.highs.append(float(candle.HighPrice))
state.lows.append(float(candle.LowPrice))
if len(state.highs) > length:
state.highs.pop(0)
state.lows.pop(0)
if len(state.highs) < length:
return
highest = max(state.highs)
lowest = min(state.lows)
close = float(candle.ClosePrice)
rsv = 100.0 * (close - lowest) / (highest - lowest) if highest > lowest else 50.0
# Classic KDJ: K and D are 1/3 smoothings of RSV and K.
k = ((state.k if state.k is not None else 50.0) * 2.0 + rsv) / 3.0
d = ((state.d if state.d is not None else 50.0) * 2.0 + k) / 3.0
j = 3.0 * k - 2.0 * d
state.k = k
state.d = d
alpha = 2.0 / (self._smoothing_length.Value + 1.0)
state.smooth_k = k if state.smooth_k is None else state.smooth_k + alpha * (k - state.smooth_k)
state.smooth_d = d if state.smooth_d is None else state.smooth_d + alpha * (d - state.smooth_d)
state.smooth_j = j if state.smooth_j is None else state.smooth_j + alpha * (j - state.smooth_j)
state.count += 1
def _process_main(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_kdj(self._states[0], candle)
smoothing = self._smoothing_length.Value
if any(s.count < smoothing for s in self._states):
return
option = self._weight_option.Value
if option == 2:
w = (1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0)
elif option == 3:
w = (0.2, 0.3, 0.5)
else:
w = (0.5, 0.3, 0.2)
s = self._states
k = w[0] * s[0].smooth_k + w[1] * s[1].smooth_k + w[2] * s[2].smooth_k
d = w[0] * s[0].smooth_d + w[1] * s[1].smooth_d + w[2] * s[2].smooth_d
j = w[0] * s[0].smooth_j + w[1] * s[1].smooth_j + w[2] * s[2].smooth_j
trend = float(process_float(self._trend, j, candle.ServerTime, True).GetValue[Decimal](None))
prev_k = self._prev_k
prev_d = self._prev_d
self._prev_k = k
self._prev_d = d
if not self._trend.IsFormed or prev_k is None or prev_d is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
bullish = trend > 50.0
buy_level = 40.0 if bullish else 20.0
sell_level = 80.0 if bullish else 60.0
cross_up = prev_k <= prev_d and k > d
cross_down = prev_k >= prev_d and k < d
if j < buy_level and cross_up and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif j > sell_level and cross_down and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
def CreateClone(self):
return adaptive_kdj_mtf_strategy()