Открыть на GitHub

Стратегия YTG ADX Level Cross

Эта стратегия переносит эксперт Yuriy Tokman _ADX.mq5 на высокоуровневый API StockSharp. Она отслеживает индикатор Average Directional Index и реагирует, когда линии +DI или -DI прорывают заданные уровни. Ордера открываются только по одному, что полностью повторяет исходный MQL-алгоритм, а защитные стоп-лосс и тейк-профит в пунктах выставляются автоматически.

Краткое описание

  • Режим рынка: рассчитана на трендовые участки или импульсные пробои, когда всплеск DI подтверждает силу движения.
  • Направление: может открывать как длинные, так и короткие позиции, но никогда не удерживает обе одновременно.
  • Таймфрейм: задаётся параметром CandleType (по умолчанию часовые свечи).
  • Данные: расчёт ADX/DI выполняется по завершённым свечам индикатора AverageDirectionalIndex.

Логика торговли

  1. Подписаться на выбранную серию свечей и создать индикатор ADX с периодом AdxPeriod.
  2. Для каждой завершённой свечи сохранять значения +DI и -DI, оставляя в буфере ровно тот объём истории, который требуется параметру Shift. Значение 1, как в оригинале, анализирует предыдущую закрытую свечу.
  3. Вход в лонг: когда смещённое значение +DI превышает LevelPlus, а предыдущее значение было ниже порога, при отсутствии открытой позиции отправляется рыночная покупка.
  4. Вход в шорт: когда смещённое значение -DI превышает LevelMinus, а предыдущее значение было ниже уровня, при нулевой позиции выполняется рыночная продажа.
  5. Выход осуществляется только за счёт защитных заявок, выставленных через StartProtection: фиксированный тейк-профит и стоп-лосс в пунктах, аналогичные параметрам TP и SL оригинального советника.

Дополнительно стратегия не усредняется, не переоткрывает сделки во время активной позиции и не применяет фильтры — поведение полностью соответствует лёгкой логике исходной версии.

Параметры

Параметр Значение по умолчанию Описание
CandleType Часовой таймфрейм Таймфрейм подписки, по которому рассчитывается ADX.
AdxPeriod 28 Длина индикатора ADX и линий Directional Movement.
LevelPlus 5 Порог, который должна пробить линия +DI для открытия длинной позиции.
LevelMinus 5 Порог, который должна пробить линия -DI для открытия короткой позиции.
Shift 1 Количество закрытых свечей, на которых проверяется пробой (1 = предыдущая свеча).
TakeProfitPoints 500 Размер тейк-профита в пунктах. Внутри умножается на шаг цены инструмента.
StopLossPoints 500 Размер стоп-лосса в пунктах.
TradeVolume 0.1 Базовый объём рыночных ордеров, соответствует параметру Lots в MQL-версии.

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

  • Метод StartProtection пересчитывает значения стопа и тейка из пунктов в абсолютные цены, используя PriceStep инструмента.
  • Дополнительные механизмы (трейлинг, перенос в безубыток) не используются — выход всегда происходит по заранее заданным уровням.

Рекомендации

  • Слишком маленькие пороги DI приводят к частым «пилящим» сделкам, большие значения позволяют ждать по-настоящему сильных импульсов.
  • Параметр Shift можно увеличить, если требуется подтверждение на более ранних свечах, например при торговле на старших таймфреймах.
  • Так как стратегия работает только с одной позицией, не рекомендуется вмешиваться вручную или запускать параллельные алгоритмы на том же счёте, чтобы не нарушить внутренний контроль позиции.
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);
		}
	}
}