KDJ 指标专家策略
概述
该策略复刻了 senlin ge 开发的 MetaTrader 5 “KDJ Expert Advisor”。它围绕 KDJ 振荡指标运行,这是一种在传统随机指标基础上加入两次平滑处理的扩展。策略只针对单个品种交易,当检测到 %K 与 %D 线的差值(即原始 EA 中的 KDC/J 线)出现动量反转时才会入场。每次建仓都会立即分配一个以点(pip)为单位的固定止损和止盈,这些距离会根据证券的价格步长自动折算成绝对价格。
实现采用 StockSharp 的高级 API:订阅蜡烛数据、调用内置 Stochastic 指标,并按照 MQL5 版本的参数设置 KDJ 的周期。代码能够自动识别带 3 位或 5 位小数的外汇合约,从而自动调整点值。
指标逻辑
KDJ 的计算分为三个阶段:
- RSV 计算——对每根已完成的蜡烛,使用
KDJ Length根历史数据计算原始随机值 (Raw Stochastic Value)。 - %K 平滑——对最近
Smooth %K个 RSV 求平均,得到 %K 线。 - %D 平滑——对最近
Smooth %D个 %K 求平均,得到 %D 线。
策略重点观察 K - D 的符号变化以及 %K 的斜率,用来捕捉动量反转。
入场规则
仅当当前没有持仓时才会产生新的市场单。所有条件均基于收盘后的完整蜡烛:
- 做多:满足以下任一条件即触发
K - D从负值上穿到正值;K - D已经大于 0,且 %K 线继续上升(K_current > K_previous)。
- 做空:满足以下任一条件即触发
K - D从正值下穿到负值;K - D已经小于 0,且 %K 线继续下降(K_current < K_previous)。
逻辑与原始 MQL5 EA 完全一致,保证交易时机一致。
风险管理
- 每笔成交都会根据参数生成固定止损和止盈,单位为点,随后转换成价格距离。若参数为 0,则关闭对应的保护。
- 策略不会加仓或摊平,始终保持单一方向持仓,直到保护单或人工操作将其平掉。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
| Candle Type | 用于运算的蜡烛类型/周期。 | 15 分钟蜡烛 |
| KDJ Length | 计算 RSV 的回溯周期。 | 30 |
| Smooth %K | 平滑 %K 时使用的 RSV 数量。 | 3 |
| Smooth %D | 平滑 %D 时使用的 %K 数量。 | 6 |
| Stop Loss (pips) | 止损距离(点)。设为 0 可禁用。 | 25 |
| Take Profit (pips) | 止盈距离(点)。设为 0 可禁用。 | 45 |
| Order Volume | 每次下单的数量。 | 1 |
所有参数都支持与原 EA 相同的优化区间,便于在 StockSharp 测试器中调参。
使用提示
- 在测试或实时环境中配置目标证券与连接器。
- 根据希望复刻的 MetaTrader 图表周期设置
Candle Type。 - 如有需要,可对 KDJ 参数、止损、止盈或下单量进行优化。
- 启动策略后仅在蜡烛收盘时评估信号并下单。
- 图表会自动绘制蜡烛、KDJ 指标以及成交记录,便于视觉验证。
与原 EA 的差异
- 直接使用 StockSharp 的
Stochastic指标实现 KDJ,无需额外的自定义指标文件。 - 通过
StartProtection管理止盈止损,当触发时自动发送市价单平仓。 - 下单量改为固定参数,而非 MQL5 版本的
MoneyFixedMargin风险模型,使得示例更专注于信号逻辑。
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 that replicates the MetaTrader KDJ Expert Advisor logic.
/// Uses the KDJ oscillator to detect momentum reversals and opens a single position with fixed take-profit and stop-loss levels.
/// </summary>
public class KdjExpertAdvisorStrategy : Strategy
{
private readonly StrategyParam<int> _kdjPeriod;
private readonly StrategyParam<int> _smoothK;
private readonly StrategyParam<int> _smoothD;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<DataType> _candleType;
private decimal? _previousK;
private decimal? _previousKdc;
private decimal _pipSize;
/// <summary>
/// Main lookback period used to calculate RSV for the KDJ oscillator.
/// </summary>
public int KdjPeriod
{
get => _kdjPeriod.Value;
set => _kdjPeriod.Value = value;
}
/// <summary>
/// Smoothing period applied to the %K line.
/// </summary>
public int SmoothK
{
get => _smoothK.Value;
set => _smoothK.Value = value;
}
/// <summary>
/// Smoothing period applied to the %D line.
/// </summary>
public int SmoothD
{
get => _smoothD.Value;
set => _smoothD.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Volume applied to every market order.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="KdjExpertAdvisorStrategy"/> class.
/// </summary>
public KdjExpertAdvisorStrategy()
{
_kdjPeriod = Param(nameof(KdjPeriod), 30)
.SetGreaterThanZero()
.SetDisplay("KDJ Length", "Lookback period for KDJ RSV calculation", "KDJ")
.SetOptimize(10, 60, 5);
_smoothK = Param(nameof(SmoothK), 3)
.SetGreaterThanZero()
.SetDisplay("Smooth %K", "Smoothing length for %K", "KDJ")
.SetOptimize(1, 10, 1);
_smoothD = Param(nameof(SmoothD), 6)
.SetGreaterThanZero()
.SetDisplay("Smooth %D", "Smoothing length for %D", "KDJ")
.SetOptimize(1, 15, 1);
_stopLossPips = Param(nameof(StopLossPips), 250)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
.SetOptimize(0, 1000, 50);
_takeProfitPips = Param(nameof(TakeProfitPips), 450)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
.SetOptimize(0, 1500, 50);
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Quantity used for entries", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for KDJ calculation", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousK = null;
_previousKdc = null;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
var stopLossUnit = StopLossPips > 0 ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : null;
var takeProfitUnit = TakeProfitPips > 0 ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : null;
StartProtection(
takeProfit: takeProfitUnit,
stopLoss: stopLossUnit,
useMarketOrders: true);
var kdj = new StochasticOscillator
{
K = { Length = KdjPeriod },
D = { Length = SmoothD }
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(kdj, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, kdj);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue kdjValue)
{
if (candle.State != CandleStates.Finished)
return;
var stochastic = (StochasticOscillatorValue)kdjValue;
if (stochastic.K is not decimal k || stochastic.D is not decimal d)
return;
var kdc = k - d;
var buySignal = false;
var sellSignal = false;
if (_previousKdc.HasValue)
{
buySignal |= _previousKdc.Value < 0m && kdc > 0m;
sellSignal |= _previousKdc.Value > 0m && kdc < 0m;
}
if (_previousK.HasValue)
{
buySignal |= kdc > 0m && _previousK.Value < k;
sellSignal |= kdc < 0m && _previousK.Value > k;
}
if (buySignal || sellSignal)
{
if (Position == 0)
{
if (buySignal)
{
LogInfo($"Buy signal at {candle.ClosePrice}: K={k:F2}, D={d:F2}, K-D={kdc:F2}");
BuyMarket();
}
else if (sellSignal)
{
LogInfo($"Sell signal at {candle.ClosePrice}: K={k:F2}, D={d:F2}, K-D={kdc:F2}");
SellMarket();
}
}
}
_previousK = k;
_previousKdc = kdc;
}
private decimal CalculatePipSize()
{
var security = Security;
if (security == null)
return 1m;
var step = security.PriceStep ?? 1m;
var decimals = security.Decimals;
var multiplier = (decimals == 3 || decimals == 5) ? 10m : 1m;
return step * multiplier;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import StochasticOscillator
from StockSharp.Algo.Strategies import Strategy
class kdj_expert_advisor_strategy(Strategy):
"""
KDJ Expert Advisor: uses Stochastic Oscillator K/D crossover
for momentum reversals with pip-based SL/TP via StartProtection.
"""
def __init__(self):
super(kdj_expert_advisor_strategy, self).__init__()
self._kdj_period = self.Param("KdjPeriod", 30) \
.SetDisplay("KDJ Length", "Lookback period for KDJ", "KDJ")
self._smooth_d = self.Param("SmoothD", 6) \
.SetDisplay("Smooth %D", "Smoothing for %D", "KDJ")
self._stop_loss_pips = self.Param("StopLossPips", 250) \
.SetDisplay("Stop Loss (pips)", "Stop distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 450) \
.SetDisplay("Take Profit (pips)", "Profit target in pips", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe for KDJ", "Data")
self._prev_k = None
self._prev_kdc = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(kdj_expert_advisor_strategy, self).OnReseted()
self._prev_k = None
self._prev_kdc = None
def _calculate_pip_size(self):
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step <= 0:
step = 1.0
decimals = 0
if self.Security is not None and self.Security.Decimals is not None:
decimals = int(self.Security.Decimals)
multiplier = 10.0 if decimals in (3, 5) else 1.0
return step * multiplier
def OnStarted2(self, time):
super(kdj_expert_advisor_strategy, self).OnStarted2(time)
pip_size = self._calculate_pip_size()
sl_pips = self._stop_loss_pips.Value
tp_pips = self._take_profit_pips.Value
tp_val = Decimal(float(tp_pips) * pip_size) if tp_pips > 0 else Decimal(0)
sl_val = Decimal(float(sl_pips) * pip_size) if sl_pips > 0 else Decimal(0)
tp_unit = Unit(tp_val, UnitTypes.Absolute) if tp_pips > 0 else Unit()
sl_unit = Unit(sl_val, UnitTypes.Absolute) if sl_pips > 0 else Unit()
self.StartProtection(tp_unit, sl_unit)
kdj = StochasticOscillator()
kdj.K.Length = self._kdj_period.Value
kdj.D.Length = self._smooth_d.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(kdj, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, kdj)
self.DrawOwnTrades(area)
def _process_candle(self, candle, kdj_value):
if candle.State != CandleStates.Finished:
return
k = kdj_value.K
d = kdj_value.D
if k is None or d is None:
return
k = float(k)
d = float(d)
kdc = k - d
buy_signal = False
sell_signal = False
if self._prev_kdc is not None:
if self._prev_kdc < 0 and kdc > 0:
buy_signal = True
if self._prev_kdc > 0 and kdc < 0:
sell_signal = True
if self._prev_k is not None:
if kdc > 0 and self._prev_k < k:
buy_signal = True
if kdc < 0 and self._prev_k > k:
sell_signal = True
pos = float(self.Position)
if abs(pos) < 0.0001:
if buy_signal:
self.BuyMarket()
elif sell_signal:
self.SellMarket()
self._prev_k = k
self._prev_kdc = kdc
def CreateClone(self):
return kdj_expert_advisor_strategy()