YTG ADX Level Cross Strategy
This strategy ports Yuriy Tokman's _ADX.mq5 expert advisor to the StockSharp high-level API. It monitors the Average Directional Index and reacts when the +DI or -DI components surge through configurable thresholds. Orders are opened only once at a time, mirroring the original MQL logic, and protective stop-loss and take-profit levels expressed in price points are applied automatically.
Overview
- Market regime: Works on trending or strongly directional moves where DI spikes accompany breakouts.
- Direction: Opens either long or short positions, but never both simultaneously.
- Timeframe: Controlled by the
CandleType parameter (default 1-hour candles).
- Data: Uses finished candles to calculate ADX/DI values from the
AverageDirectionalIndex indicator.
Trading Logic
- Subscribe to the selected candle series and build the ADX indicator with the configured
AdxPeriod.
- For each finished candle, collect the +DI and -DI values and keep only the amount of history required by the
Shift parameter. A Shift of 1, identical to the MQL default, evaluates the previous closed candle.
- Long entry: Triggered when the shifted +DI value rises above
LevelPlus while its previous value was below the same threshold. The strategy checks that no position is currently open before buying at market.
- Short entry: Triggered when the shifted -DI value rises above
LevelMinus while its previous value was below that level. A market sell is sent only if there is no active position.
- Exits are handled exclusively by protective orders launched through
StartProtection: a fixed take-profit and stop-loss measured in price points, equivalent to TP and SL from the original code.
This implementation intentionally avoids averaging into positions, reentries while trades are active, or additional filters, matching the lightweight behaviour of the source EA.
Parameters
| Parameter |
Default |
Description |
CandleType |
1-hour time frame |
Time frame of the candle subscription used for ADX calculation. |
AdxPeriod |
28 |
Length of the Average Directional Index and its DI calculations. |
LevelPlus |
5 |
Threshold that the +DI series must exceed to open a long position. |
LevelMinus |
5 |
Threshold that the -DI series must exceed to open a short position. |
Shift |
1 |
Number of closed candles to look back when evaluating the DI crossing (1 = previous candle). |
TakeProfitPoints |
500 |
Distance in price points for the take-profit order. Multiplied by the instrument's tick size internally. |
StopLossPoints |
500 |
Distance in price points for the protective stop-loss order. |
TradeVolume |
0.1 |
Base volume for new market orders, matching the Lots setting in the MQL expert. |
Risk Management
StartProtection converts the point-based take-profit and stop-loss values into absolute price distances using the instrument's PriceStep.
- No trailing stop or breakeven logic is applied; exits occur solely through the configured protective orders.
Notes and Tips
- Extremely low DI thresholds may lead to frequent whipsaw trades, while higher levels wait for stronger directional bursts.
- The
Shift parameter can be increased when you need confirmation from earlier candles, for example on higher time frames to filter intrabar noise.
- Because the strategy trades only one position at a time, manual interference or external trades on the same account should be avoided to prevent conflicts with the internal position tracking.
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()