Ver en GitHub

Estrategia Ytg ADX Cruce de Nivel

Esta estrategia porta el asesor experto _ADX.mq5 de Yuriy Tokman a la API de alto nivel de StockSharp. Monitorea el Average Directional Index y reacciona cuando los componentes +DI o -DI superan umbrales configurables. Las órdenes se abren solo de una en una, reflejando la lógica MQL original, y los niveles de stop-loss y take-profit de protección expresados en puntos de precio se aplican automáticamente.

Descripción general

  • Régimen de mercado: Funciona en movimientos tendenciales o fuertemente direccionales donde los picos de DI acompañan rupturas.
  • Dirección: Abre posiciones largas o cortas, pero nunca ambas simultáneamente.
  • Marco temporal: Controlado por el parámetro CandleType (por defecto velas de 1 hora).
  • Datos: Usa velas finalizadas para calcular los valores ADX/DI del indicador AverageDirectionalIndex.

Lógica de trading

  1. Suscribirse a la serie de velas seleccionada y construir el indicador ADX con el AdxPeriod configurado.
  2. Para cada vela finalizada, recopilar los valores +DI y -DI y mantener solo la cantidad de historial requerida por el parámetro Shift. Un Shift de 1, idéntico al predeterminado de MQL, evalúa la vela cerrada anterior.
  3. Entrada larga: Activada cuando el valor +DI desplazado sube por encima de LevelPlus mientras su valor anterior estaba por debajo del mismo umbral. La estrategia verifica que no haya posición abierta actualmente antes de comprar a mercado.
  4. Entrada corta: Activada cuando el valor -DI desplazado sube por encima de LevelMinus mientras su valor anterior estaba por debajo de ese nivel. Se envía una venta a mercado solo si no hay posición activa.
  5. Las salidas son manejadas exclusivamente por órdenes de protección iniciadas a través de StartProtection: un take-profit y stop-loss fijos medidos en puntos de precio, equivalentes a TP y SL del código original.

Esta implementación evita intencionalmente el promediado en posiciones, reentradas mientras las operaciones están activas, o filtros adicionales, coincidiendo con el comportamiento ligero del EA fuente.

Parámetros

Parámetro Predeterminado Descripción
CandleType Marco temporal de 1 hora Marco temporal de la suscripción de velas usada para el cálculo de ADX.
AdxPeriod 28 Longitud del Average Directional Index y sus cálculos de DI.
LevelPlus 5 Umbral que la serie +DI debe superar para abrir una posición larga.
LevelMinus 5 Umbral que la serie -DI debe superar para abrir una posición corta.
Shift 1 Número de velas cerradas a mirar atrás al evaluar el cruce de DI (1 = vela anterior).
TakeProfitPoints 500 Distancia en puntos de precio para la orden de take-profit. Multiplicada internamente por el tamaño de tick del instrumento.
StopLossPoints 500 Distancia en puntos de precio para la orden de stop-loss de protección.
TradeVolume 0.1 Volumen base para nuevas órdenes de mercado, coincidiendo con el ajuste Lots en el experto MQL.

Gestión de riesgos

  • StartProtection convierte los valores de take-profit y stop-loss basados en puntos en distancias de precio absolutas usando el PriceStep del instrumento.
  • No se aplica trailing stop ni lógica de punto de equilibrio; las salidas ocurren únicamente a través de las órdenes de protección configuradas.

Notas y consejos

  • Los umbrales de DI extremadamente bajos pueden llevar a operaciones de vaivén frecuentes, mientras que niveles más altos esperan impulsos direccionales más fuertes.
  • El parámetro Shift puede aumentarse cuando se necesita confirmación de velas anteriores, por ejemplo en marcos temporales más altos para filtrar el ruido intrábarra.
  • Debido a que la estrategia opera solo una posición a la vez, se deben evitar las interferencias manuales o las operaciones externas en la misma cuenta para prevenir conflictos con el seguimiento interno de posición.
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>
/// Reimplementation of the YTG ADX threshold breakout expert using high level StockSharp API.
/// The strategy waits for the +DI or -DI line to break above configurable levels and opens
/// a position in the corresponding direction with protective stop-loss and take-profit.
/// </summary>
public class YtgAdxLevelCrossStrategy : Strategy
{
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<int> _levelPlus;
	private readonly StrategyParam<int> _levelMinus;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;

	private AverageDirectionalIndex _adx;

	private readonly List<decimal> _plusDiHistory = [];
	private readonly List<decimal> _minusDiHistory = [];

	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	public int LevelPlus
	{
		get => _levelPlus.Value;
		set => _levelPlus.Value = value;
	}

	public int LevelMinus
	{
		get => _levelMinus.Value;
		set => _levelMinus.Value = value;
	}

	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public YtgAdxLevelCrossStrategy()
	{
		_adxPeriod = Param(nameof(AdxPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ADX Period", "Period for the Average Directional Index", "Indicators")
			
			.SetOptimize(10, 40, 2);

		_levelPlus = Param(nameof(LevelPlus), 15)
			.SetNotNegative()
			.SetDisplay("+DI Level", "Threshold that the +DI line must break", "Signals")
			
			.SetOptimize(5, 40, 5);

		_levelMinus = Param(nameof(LevelMinus), 15)
			.SetNotNegative()
			.SetDisplay("-DI Level", "Threshold that the -DI line must break", "Signals")
			
			.SetOptimize(5, 40, 5);

		_shift = Param(nameof(Shift), 1)
			.SetNotNegative()
			.SetDisplay("Signal Shift", "Number of closed candles to look back", "Signals")
			
			.SetOptimize(0, 3, 1);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance to take profit in price points", "Risk");

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Distance to stop loss in price points", "Risk");

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Base volume for market orders", "Orders");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for the strategy", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	protected override void OnReseted()
	{
		base.OnReseted();

		_plusDiHistory.Clear();
		_minusDiHistory.Clear();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = TradeVolume;

		_adx = new AverageDirectionalIndex
		{
			Length = AdxPeriod
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var step = Security.PriceStep ?? 1m;
		Unit takeProfit = null;
		Unit stopLoss = null;

		if (TakeProfitPoints > 0)
			takeProfit = new Unit(TakeProfitPoints * step, UnitTypes.Absolute);

		if (StopLossPoints > 0)
			stopLoss = new Unit(StopLossPoints * step, UnitTypes.Absolute);

		if (takeProfit != null || stopLoss != null)
		{
			StartProtection(takeProfit: takeProfit, stopLoss: stopLoss);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var adxValue = _adx.Process(candle);

		if (!_adx.IsFormed || !adxValue.IsFinal)
			return;

		if (adxValue is not AverageDirectionalIndexValue typed)
			return;

		if (typed.Dx.Plus is not decimal plusDi || typed.Dx.Minus is not decimal minusDi)
			return;

		UpdateHistory(_plusDiHistory, plusDi);
		UpdateHistory(_minusDiHistory, minusDi);

		var currentShift = Shift;
		var minCount = currentShift + 2;

		if (_plusDiHistory.Count < minCount || _minusDiHistory.Count < minCount)
			return;

		var currentIndex = _plusDiHistory.Count - 1 - currentShift;
		var previousIndex = currentIndex - 1;

		if (previousIndex < 0)
			return;

		var shiftedPlus = _plusDiHistory[currentIndex];
		var shiftedPlusPrev = _plusDiHistory[previousIndex];
		var shiftedMinus = _minusDiHistory[currentIndex];
		var shiftedMinusPrev = _minusDiHistory[previousIndex];

		var longSignal = shiftedPlus > LevelPlus && shiftedPlusPrev < LevelPlus;
		var shortSignal = shiftedMinus > LevelMinus && shiftedMinusPrev < LevelMinus;

		if (Position == 0)
		{
			if (longSignal)
			{
				// Enter a long position when +DI breaks above the configured level.
				BuyMarket();
			}
			else if (shortSignal)
			{
				// Enter a short position when -DI breaks above the configured level.
				SellMarket();
			}
		}
	}

	private void UpdateHistory(List<decimal> history, decimal value)
	{
		history.Add(value);

		var maxLength = Shift + 2;

		while (history.Count > maxLength)
		{
			// Keep only the amount of history required for the configured shift.
			history.RemoveAt(0);
		}
	}
}