Открыть на GitHub

Стратегия KDJ Expert Advisor

Обзор

Стратегия полностью повторяет MetaTrader 5 эксперт "KDJ Expert Advisor" от senlin ge. Используется осциллятор KDJ — модифицированный стохастик с двойным сглаживанием линии %K. Алгоритм отслеживает разницу между линиями %K и %D (в оригинале — буфер KDC/J) и открывает только одну позицию, когда обнаруживает разворот импульса. Каждая сделка сразу получает фиксированные уровни стоп-лосса и тейк-профита, задаваемые в пунктах и автоматически переводимые в абсолютное расстояние по цене.

Реализация построена на высокоуровневом API StockSharp: подключение свечной подписки, использование встроенного индикатора Stochastic с параметрами, идентичными MQL5-версии, и автоматический подбор pip-значения для инструментов с 3 или 5 знаками после запятой.

Логика индикатора

Расчёт KDJ состоит из трёх шагов:

  1. RSV — вычисление Raw Stochastic Value за KDJ Length последних свечей.
  2. %K — усреднение последних Smooth %K значений RSV.
  3. %D — усреднение последних Smooth %D значений %K.

Далее стратегия анализирует величину K - D и изменение наклона %K, чтобы определить моменты разворота.

Правила входа

Новые сделки открываются только при отсутствии текущей позиции. Сигналы проверяются на закрывшихся свечах:

  • Покупка, если выполняется одно из условий:
    • K - D пересекает нулевой уровень снизу вверх;
    • K - D уже положительное, а %K растёт (K_current > K_previous).
  • Продажа, если выполняется одно из условий:
    • K - D пересекает ноль сверху вниз;
    • K - D уже отрицательное, а %K падает (K_current < K_previous).

Такой набор логических условий соответствует оригинальному MQL5 коду, обеспечивая совпадение точек входа.

Управление риском

  • Для каждой сделки выставляются защитный стоп и тейк, расстояние задаётся в пунктах. Значение 0 отключает соответствующий уровень.
  • Стратегия не усредняется и не наращивает позицию: после входа она ждёт, пока защитные ордера или ручное вмешательство закроют позицию.

Параметры

Параметр Описание Значение по умолчанию
Candle Type Тип/таймфрейм свечей для расчётов. Свечи 15 минут
KDJ Length Длина окна для RSV. 30
Smooth %K Количество RSV для сглаживания %K. 3
Smooth %D Количество %K для сглаживания %D. 6
Stop Loss (pips) Стоп-лосс в пунктах. 0 — отключено. 25
Take Profit (pips) Тейк-профит в пунктах. 0 — отключено. 45
Order Volume Объём рыночной заявки. 1

Каждый параметр имеет диапазоны оптимизации, повторяющие настройки оригинального советника.

Рекомендации по использованию

  1. Настройте соединение и выберите инструмент в тестере или в рабочем подключении.
  2. Установите Candle Type в соответствии с таймфреймом, который использовался в MetaTrader.
  3. При необходимости запустите оптимизацию параметров KDJ, стоп-лосса, тейк-профита или объёма.
  4. Запустите стратегию — сигналы рассчитываются только на завершённых свечах.
  5. На графике автоматически отображаются свечи, индикатор KDJ и совершённые сделки для визуального контроля.

Отличия от оригинального EA

  • Используется встроенный индикатор Stochastic, поэтому не требуется отдельный файл индикатора.
  • Управление стоп-лоссом и тейк-профитом реализовано через StartProtection, который отправляет рыночные ордера при срабатывании уровней.
  • Объём сделок задаётся фиксированным параметром вместо алгоритма 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;
	}
}