在 GitHub 上查看

KDJ 指标专家策略

概述

该策略复刻了 senlin ge 开发的 MetaTrader 5 “KDJ Expert Advisor”。它围绕 KDJ 振荡指标运行,这是一种在传统随机指标基础上加入两次平滑处理的扩展。策略只针对单个品种交易,当检测到 %K 与 %D 线的差值(即原始 EA 中的 KDC/J 线)出现动量反转时才会入场。每次建仓都会立即分配一个以点(pip)为单位的固定止损和止盈,这些距离会根据证券的价格步长自动折算成绝对价格。

实现采用 StockSharp 的高级 API:订阅蜡烛数据、调用内置 Stochastic 指标,并按照 MQL5 版本的参数设置 KDJ 的周期。代码能够自动识别带 3 位或 5 位小数的外汇合约,从而自动调整点值。

指标逻辑

KDJ 的计算分为三个阶段:

  1. RSV 计算——对每根已完成的蜡烛,使用 KDJ Length 根历史数据计算原始随机值 (Raw Stochastic Value)。
  2. %K 平滑——对最近 Smooth %K 个 RSV 求平均,得到 %K 线。
  3. %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 测试器中调参。

使用提示

  1. 在测试或实时环境中配置目标证券与连接器。
  2. 根据希望复刻的 MetaTrader 图表周期设置 Candle Type
  3. 如有需要,可对 KDJ 参数、止损、止盈或下单量进行优化。
  4. 启动策略后仅在蜡烛收盘时评估信号并下单。
  5. 图表会自动绘制蜡烛、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;
	}
}