Стратегия YTG ADX Level Cross
Эта стратегия переносит эксперт Yuriy Tokman _ADX.mq5 на высокоуровневый API StockSharp. Она отслеживает индикатор Average Directional Index и реагирует, когда линии +DI или -DI прорывают заданные уровни. Ордера открываются только по одному, что полностью повторяет исходный MQL-алгоритм, а защитные стоп-лосс и тейк-профит в пунктах выставляются автоматически.
Краткое описание
- Режим рынка: рассчитана на трендовые участки или импульсные пробои, когда всплеск DI подтверждает силу движения.
- Направление: может открывать как длинные, так и короткие позиции, но никогда не удерживает обе одновременно.
- Таймфрейм: задаётся параметром
CandleType (по умолчанию часовые свечи).
- Данные: расчёт ADX/DI выполняется по завершённым свечам индикатора
AverageDirectionalIndex.
Логика торговли
- Подписаться на выбранную серию свечей и создать индикатор ADX с периодом
AdxPeriod.
- Для каждой завершённой свечи сохранять значения +DI и -DI, оставляя в буфере ровно тот объём истории, который требуется параметру
Shift. Значение 1, как в оригинале, анализирует предыдущую закрытую свечу.
- Вход в лонг: когда смещённое значение +DI превышает
LevelPlus, а предыдущее значение было ниже порога, при отсутствии открытой позиции отправляется рыночная покупка.
- Вход в шорт: когда смещённое значение -DI превышает
LevelMinus, а предыдущее значение было ниже уровня, при нулевой позиции выполняется рыночная продажа.
- Выход осуществляется только за счёт защитных заявок, выставленных через
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);
}
}
}
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()