Ver no GitHub

Estratégia Ytg ADX Cruzamento de Nível

Esta estratégia porta o assessor especialista _ADX.mq5 de Yuriy Tokman para a API de alto nível do StockSharp. Ela monitora o Average Directional Index e reage quando os componentes +DI ou -DI ultrapassam limites configuráveis. As ordens são abertas apenas uma de cada vez, espelhando a lógica MQL original, e os níveis de stop-loss e take-profit de proteção expressos em pontos de preço são aplicados automaticamente.

Visão geral

  • Regime de mercado: Funciona em movimentos tendenciais ou fortemente direcionais onde picos de DI acompanham rompimentos.
  • Direção: Abre posições compradas ou vendidas, mas nunca ambas simultaneamente.
  • Período: Controlado pelo parâmetro CandleType (padrão velas de 1 hora).
  • Dados: Usa velas finalizadas para calcular valores ADX/DI do indicador AverageDirectionalIndex.

Lógica de trading

  1. Assinar a série de velas selecionada e construir o indicador ADX com o AdxPeriod configurado.
  2. Para cada vela finalizada, coletar os valores +DI e -DI e manter apenas a quantidade de histórico exigida pelo parâmetro Shift. Um Shift de 1, idêntico ao padrão MQL, avalia a vela fechada anterior.
  3. Entrada comprada: Ativada quando o valor +DI deslocado sobe acima de LevelPlus enquanto seu valor anterior estava abaixo do mesmo limiar. A estratégia verifica que não há posição atualmente aberta antes de comprar a mercado.
  4. Entrada vendida: Ativada quando o valor -DI deslocado sobe acima de LevelMinus enquanto seu valor anterior estava abaixo desse nível. Uma venda a mercado é enviada apenas se não houver posição ativa.
  5. As saídas são tratadas exclusivamente por ordens de proteção iniciadas através de StartProtection: um take-profit e stop-loss fixos medidos em pontos de preço, equivalentes a TP e SL do código original.

Esta implementação evita intencionalmente o aumento médio em posições, reentradas enquanto as operações estão ativas, ou filtros adicionais, correspondendo ao comportamento leve do EA fonte.

Parâmetros

Parâmetro Padrão Descrição
CandleType Período de 1 hora Período da assinatura de velas usada para o cálculo do ADX.
AdxPeriod 28 Comprimento do Average Directional Index e seus cálculos de DI.
LevelPlus 5 Limiar que a série +DI deve exceder para abrir uma posição comprada.
LevelMinus 5 Limiar que a série -DI deve exceder para abrir uma posição vendida.
Shift 1 Número de velas fechadas a analisar retrospectivamente ao avaliar o cruzamento de DI (1 = vela anterior).
TakeProfitPoints 500 Distância em pontos de preço para a ordem de take-profit. Multiplicada internamente pelo tamanho de tick do instrumento.
StopLossPoints 500 Distância em pontos de preço para a ordem de stop-loss de proteção.
TradeVolume 0.1 Volume base para novas ordens a mercado, correspondendo à configuração Lots no especialista MQL.

Gestão de risco

  • StartProtection converte os valores de take-profit e stop-loss baseados em pontos em distâncias de preço absolutas usando o PriceStep do instrumento.
  • Nenhum trailing stop ou lógica de ponto de equilíbrio é aplicado; as saídas ocorrem exclusivamente através das ordens de proteção configuradas.

Notas e dicas

  • Limites de DI extremamente baixos podem levar a operações de vai-e-vem frequentes, enquanto níveis mais altos esperam por impulsos direcionais mais fortes.
  • O parâmetro Shift pode ser aumentado quando é necessária confirmação de velas anteriores, por exemplo em períodos maiores para filtrar o ruído intrábarra.
  • Como a estratégia opera apenas uma posição por vez, interferências manuais ou operações externas na mesma conta devem ser evitadas para prevenir conflitos com o rastreamento interno de posição.
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);
		}
	}
}