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
- Die ausgewählte Kerzenserie abonnieren und den ADX-Indikator mit dem konfigurierten
AdxPeriod erstellen.
- 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.
- 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.
- 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.
- 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);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import AverageDirectionalIndex, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class ytg_adx_level_cross_strategy(Strategy):
def __init__(self):
super(ytg_adx_level_cross_strategy, self).__init__()
self._adx_period = self.Param("AdxPeriod", 14)
self._level_plus = self.Param("LevelPlus", 15)
self._level_minus = self.Param("LevelMinus", 15)
self._shift = self.Param("Shift", 1)
self._take_profit_points = self.Param("TakeProfitPoints", 500.0)
self._stop_loss_points = self.Param("StopLossPoints", 500.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._adx = None
self._plus_di_history = []
self._minus_di_history = []
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def AdxPeriod(self):
return self._adx_period.Value
@property
def LevelPlus(self):
return self._level_plus.Value
@property
def LevelMinus(self):
return self._level_minus.Value
@property
def Shift(self):
return self._shift.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
def OnStarted2(self, time):
super(ytg_adx_level_cross_strategy, self).OnStarted2(time)
self._adx = AverageDirectionalIndex()
self._adx.Length = self.AdxPeriod
self._plus_di_history = []
self._minus_di_history = []
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
tp_unit = None
sl_unit = None
if self.TakeProfitPoints > 0:
tp_unit = Unit(self.TakeProfitPoints * step, UnitTypes.Absolute)
if self.StopLossPoints > 0:
sl_unit = Unit(self.StopLossPoints * step, UnitTypes.Absolute)
if tp_unit is not None or sl_unit is not None:
self.StartProtection(tp_unit, sl_unit)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
civ = CandleIndicatorValue(self._adx, candle)
civ.IsFinal = True
adx_val = self._adx.Process(civ)
if not self._adx.IsFormed:
return
if not adx_val.IsFinal:
return
plus_di = None
minus_di = None
try:
dx = adx_val.Dx
if dx is not None:
plus_di = dx.Plus
minus_di = dx.Minus
except Exception:
return
if plus_di is None or minus_di is None:
return
plus_di = float(plus_di)
minus_di = float(minus_di)
self._update_history(self._plus_di_history, plus_di)
self._update_history(self._minus_di_history, minus_di)
current_shift = self.Shift
min_count = current_shift + 2
if len(self._plus_di_history) < min_count or len(self._minus_di_history) < min_count:
return
current_idx = len(self._plus_di_history) - 1 - current_shift
prev_idx = current_idx - 1
if prev_idx < 0:
return
shifted_plus = self._plus_di_history[current_idx]
shifted_plus_prev = self._plus_di_history[prev_idx]
shifted_minus = self._minus_di_history[current_idx]
shifted_minus_prev = self._minus_di_history[prev_idx]
long_signal = shifted_plus > self.LevelPlus and shifted_plus_prev < self.LevelPlus
short_signal = shifted_minus > self.LevelMinus and shifted_minus_prev < self.LevelMinus
if self.Position == 0:
if long_signal:
self.BuyMarket()
elif short_signal:
self.SellMarket()
def _update_history(self, history, value):
history.append(value)
max_length = self.Shift + 2
while len(history) > max_length:
history.pop(0)
def OnReseted(self):
super(ytg_adx_level_cross_strategy, self).OnReseted()
self._plus_di_history = []
self._minus_di_history = []
def CreateClone(self):
return ytg_adx_level_cross_strategy()