Ver en GitHub

Estrategia del Asesor Experto KDJ

Descripción general

Esta estrategia replica el MetaTrader 5 «KDJ Expert Advisor» de senlin ge. Opera en un solo símbolo usando señales del oscilador KDJ, una evolución del oscilador estocástico donde la línea %K se suaviza dos veces. La estrategia observa la diferencia entre las líneas %K y %D (a menudo llamada línea J) para identificar reversiones de impulso, abriendo solo una posición a la vez. La gestión de operaciones refleja el asesor experto original: cada operación recibe inmediatamente un stop-loss fijo y take-profit expresados en pips y traducidos a distancia de precio usando la configuración del instrumento.

La implementación usa la API de alto nivel de StockSharp con una suscripción de velas y el indicador Stochastic integrado, configurado para coincidir con los parámetros KDJ de la versión MQL5. El código detecta automáticamente símbolos Forex de 3 o 5 dígitos y ajusta el valor del pip en consecuencia.

Lógica del indicador

El indicador subyacente funciona en tres etapas:

  1. Cálculo RSV – Para cada vela terminada, calcular el Valor Estocástico Bruto en KDJ Length velas: [ RSV = \frac{Close - LowestLow}{HighestHigh - LowestLow} \times 100 ]
  2. Suavizado %K – Promediar los últimos valores Smooth %K de RSV para obtener la línea %K.
  3. Suavizado %D – Promediar los últimos valores Smooth %D de %K para obtener la línea %D.

La estrategia luego analiza K - D (referido como KDC en la fuente original) y la pendiente de %K para detectar reversiones.

Criterios de entrada

Una posición de mercado se abre solo si no hay ninguna posición existente para el símbolo. Las señales se evalúan en velas completadas:

  • Compra cuando cualquiera de las siguientes condiciones es verdadera:
    • K - D cruza por encima de cero (de negativo a positivo); o
    • K - D está por encima de cero y la línea %K está subiendo (K_current > K_previous).
  • Venta cuando cualquiera de las siguientes condiciones es verdadera:
    • K - D cruza por debajo de cero (de positivo a negativo); o
    • K - D está por debajo de cero y la línea %K está bajando (K_current < K_previous).

Esto coincide con la estructura booleana del asesor experto MQL5 original, garantizando un timing de operación idéntico.

Gestión de riesgos

  • Cada orden ejecutada recibe un stop-loss protector y take-profit, medidos en pips y convertidos a distancia de precio a través del tamaño del tick del instrumento. Un valor de cero deshabilita el tramo de protección correspondiente.
  • La estrategia no realiza pirámide ni promedia posiciones. Permanece plana hasta que la posición actual sea cerrada por las órdenes protectoras o por intervención manual.

Parámetros

Parámetro Descripción Predeterminado
Candle Type Tipo de datos/marco temporal de las velas de entrada. Marco temporal de 15 minutos
KDJ Length Número de velas para el cálculo RSV. 30
Smooth %K Número de valores RSV usados para suavizar la línea %K. 3
Smooth %D Número de valores %K usados para suavizar la línea %D. 6
Stop Loss (pips) Distancia del stop-loss protector. Establezca en 0 para deshabilitar. 25
Take Profit (pips) Distancia del take-profit protector. Establezca en 0 para deshabilitar. 45
Order Volume Cantidad enviada con las órdenes de mercado. 1

Todos los parámetros admiten rangos de optimización idénticos a las entradas del asesor experto original.

Notas de uso

  1. Configure el instrumento y el conector deseados en el tester o en el entorno en vivo.
  2. Ajuste el tipo de vela para coincidir con el marco temporal del gráfico que desea emular desde MetaTrader.
  3. Opcionalmente optimice los parámetros KDJ, stop-loss, take-profit o volumen de la orden.
  4. Inicie la estrategia. Las órdenes se generan solo en velas completamente formadas.
  5. El gráfico muestra automáticamente velas, el indicador KDJ y las operaciones ejecutadas para confirmación visual.

Diferencias con el EA original

  • Usa el indicador Stochastic de StockSharp con períodos de suavizado para replicar los buffers KDJ de MQL5; no se requiere ningún archivo de indicador externo.
  • Las órdenes protectoras se gestionan a través de StartProtection, que envía salidas de mercado cuando se activan.
  • El volumen es un parámetro fijo en lugar del modelo de riesgo MoneyFixedMargin de MQL5, manteniendo la implementación concisa y enfocada en la lógica de señal.
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;
	}
}