Ver en GitHub

KDJ Expert Advisor Strategy

Overview

This strategy replicates the MetaTrader 5 "KDJ Expert Advisor" by senlin ge. It trades a single symbol using signals from the KDJ oscillator, an evolution of the stochastic oscillator where the %K line is smoothed twice. The strategy observes the difference between the %K and %D lines (often called the J line) to identify momentum reversals, opening only one position at a time. Trade management mirrors the original expert advisor: each trade immediately receives a fixed stop-loss and take-profit that are expressed in pips and translated into price distance using the instrument settings.

The implementation uses StockSharp's high-level API with a candle subscription and the built-in Stochastic indicator, configured to match the KDJ parameters from the MQL5 version. The code automatically detects 3- or 5-digit Forex symbols and adjusts the pip value accordingly.

Indicator Logic

The underlying indicator works in three stages:

  1. RSV calculation – For each finished candle, compute the Raw Stochastic Value over KDJ Length candles: [ RSV = \frac{Close - LowestLow}{HighestHigh - LowestLow} \times 100 ]
  2. %K smoothing – Average the last Smooth %K RSV values to obtain the %K line.
  3. %D smoothing – Average the last Smooth %D %K values to obtain the %D line.

The strategy then analyses K - D (referred to as KDC in the original source) and the slope of %K to detect reversals.

Entry Rules

A market position is opened only if there is no existing position for the symbol. Signals are evaluated on completed candles:

  • Buy when either of the following conditions is true:
    • K - D crosses above zero (from negative to positive); or
    • K - D is above zero and the %K line is rising (K_current > K_previous).
  • Sell when either of the following conditions is true:
    • K - D crosses below zero (from positive to negative); or
    • K - D is below zero and the %K line is falling (K_current < K_previous).

This matches the boolean structure from the original MQL5 expert advisor, ensuring identical trade timing.

Risk Management

  • Each filled order receives a protective stop-loss and take-profit, measured in pips and converted into price distance via the instrument's tick size. A value of zero disables the corresponding protection leg.
  • The strategy does not pyramid or average positions. It remains flat until the current position is closed by the protective orders or by manual intervention.

Parameters

Parameter Description Default
Candle Type Data type/timeframe of the input candles. 15-minute time frame
KDJ Length Number of candles for RSV calculation. 30
Smooth %K Number of RSV values used to smooth the %K line. 3
Smooth %D Number of %K values used to smooth the %D line. 6
Stop Loss (pips) Distance of the protective stop-loss. Set to 0 to disable. 25
Take Profit (pips) Distance of the protective take-profit. Set to 0 to disable. 45
Order Volume Quantity sent with market orders. 1

All parameters support optimization ranges identical to the original expert's inputs.

Usage Notes

  1. Configure the desired security and connector in the tester or live environment.
  2. Adjust the candle type to match the chart timeframe you want to emulate from MetaTrader.
  3. Optionally optimize the KDJ parameters, stop-loss, take-profit, or order volume.
  4. Start the strategy. Orders are generated only on fully formed candles.
  5. The chart automatically displays candles, the KDJ indicator, and executed trades for visual confirmation.

Differences from the Original EA

  • Uses StockSharp's Stochastic indicator with smoothing periods to replicate the MQL5 KDJ buffers; no external indicator file is required.
  • Protective orders are managed through StartProtection, which submits market exits when triggered.
  • Volume is a fixed parameter instead of the MQL5 MoneyFixedMargin risk model, keeping the implementation concise and focused on the signal logic.
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;
	}
}