Auf GitHub ansehen

Ytg ADX Niveau-Kreuzung Strategie

Diese Strategie portiert Yuriy Tokmans _ADX.mq5-Expert-Advisor in die StockSharp-High-Level-API. Sie überwacht den Average Directional Index und reagiert, wenn die +DI- oder -DI-Komponenten konfigurierbare Schwellenwerte überschreiten. Orders werden jeweils nur einmal eröffnet, was der ursprünglichen MQL-Logik entspricht, und schützende Stop-Loss- und Take-Profit-Level in Preispunkten werden automatisch angewendet.

Übersicht

  • Marktregime: Funktioniert bei Trend- oder stark direktionalen Bewegungen, bei denen DI-Spitzen Ausbrüche begleiten.
  • Richtung: Öffnet entweder Long- oder Short-Positionen, aber nie beide gleichzeitig.
  • Zeitrahmen: Gesteuert durch den CandleType-Parameter (Standard 1-Stunden-Kerzen).
  • Daten: Verwendet fertige Kerzen zur Berechnung von ADX/DI-Werten aus dem AverageDirectionalIndex-Indikator.

Handelslogik

  1. Die ausgewählte Kerzenserie abonnieren und den ADX-Indikator mit dem konfigurierten AdxPeriod erstellen.
  2. Für jede fertige Kerze die +DI- und -DI-Werte sammeln und nur die vom Shift-Parameter benötigte Historienmenge behalten. Ein Shift von 1, identisch mit dem MQL-Standard, wertet die vorherige geschlossene Kerze aus.
  3. Long-Einstieg: Ausgelöst, wenn der verschobene +DI-Wert über LevelPlus steigt, während sein vorheriger Wert unter dem gleichen Schwellenwert lag. Die Strategie prüft, dass aktuell keine Position offen ist, bevor sie zum Markt kauft.
  4. Short-Einstieg: Ausgelöst, wenn der verschobene -DI-Wert über LevelMinus steigt, während sein vorheriger Wert unter diesem Niveau lag. Ein Marktverkauf wird nur gesendet, wenn keine aktive Position vorhanden ist.
  5. Ausstiege werden ausschließlich durch schützende Orders gehandhabt, die über StartProtection gestartet werden: ein fester Take-Profit und Stop-Loss gemessen in Preispunkten, entsprechend TP und SL aus dem Originalcode.

Diese Implementierung vermeidet bewusst das Averaging in Positionen, Wiedereinstiege bei aktiven Trades oder zusätzliche Filter, was dem leichtgewichtigen Verhalten des Quell-EAs entspricht.

Parameter

Parameter Standard Beschreibung
CandleType 1-Stunden-Zeitrahmen Zeitrahmen des Kerzen-Abonnements für die ADX-Berechnung.
AdxPeriod 28 Länge des Average Directional Index und seiner DI-Berechnungen.
LevelPlus 5 Schwellenwert, den die +DI-Serie überschreiten muss, um eine Long-Position zu eröffnen.
LevelMinus 5 Schwellenwert, den die -DI-Serie überschreiten muss, um eine Short-Position zu eröffnen.
Shift 1 Anzahl der geschlossenen Kerzen, die bei der DI-Kreuzungsbewertung zurückgeschaut werden (1 = vorherige Kerze).
TakeProfitPoints 500 Abstand in Preispunkten für die Take-Profit-Order. Intern mit der Tick-Größe des Instruments multipliziert.
StopLossPoints 500 Abstand in Preispunkten für die schützende Stop-Loss-Order.
TradeVolume 0.1 Basisvolumen für neue Marktorders, entspricht der Lots-Einstellung im MQL-Experten.

Risikomanagement

  • StartProtection konvertiert die punktbasierten Take-Profit- und Stop-Loss-Werte in absolute Preisdistanzen unter Verwendung des Instrument-PriceStep.
  • Kein Trailing Stop oder Breakeven-Logik wird angewendet; Ausstiege erfolgen ausschließlich über die konfigurierten Schutz-Orders.

Hinweise und Tipps

  • Extrem niedrige DI-Schwellenwerte können zu häufigen Sägezahn-Trades führen, während höhere Level auf stärkere Richtungsausbrüche warten.
  • Der Shift-Parameter kann erhöht werden, wenn Bestätigung von früheren Kerzen benötigt wird, zum Beispiel auf höheren Zeitrahmen zur Filterung von Intrabar-Rauschen.
  • Da die Strategie jeweils nur eine Position handelt, sollten manuelle Eingriffe oder externe Trades auf demselben Konto vermieden werden, um Konflikte mit dem internen Positions-Tracking zu verhindern.
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);
		}
	}
}