Estrategia Ytg ADX Cruce de Nivel
Esta estrategia porta el asesor experto _ADX.mq5 de Yuriy Tokman a la API de alto nivel de StockSharp. Monitorea el Average Directional Index y reacciona cuando los componentes +DI o -DI superan umbrales configurables. Las órdenes se abren solo de una en una, reflejando la lógica MQL original, y los niveles de stop-loss y take-profit de protección expresados en puntos de precio se aplican automáticamente.
Descripción general
- Régimen de mercado: Funciona en movimientos tendenciales o fuertemente direccionales donde los picos de DI acompañan rupturas.
- Dirección: Abre posiciones largas o cortas, pero nunca ambas simultáneamente.
- Marco temporal: Controlado por el parámetro
CandleType (por defecto velas de 1 hora).
- Datos: Usa velas finalizadas para calcular los valores ADX/DI del indicador
AverageDirectionalIndex.
Lógica de trading
- Suscribirse a la serie de velas seleccionada y construir el indicador ADX con el
AdxPeriod configurado.
- Para cada vela finalizada, recopilar los valores +DI y -DI y mantener solo la cantidad de historial requerida por el parámetro
Shift. Un Shift de 1, idéntico al predeterminado de MQL, evalúa la vela cerrada anterior.
- Entrada larga: Activada cuando el valor +DI desplazado sube por encima de
LevelPlus mientras su valor anterior estaba por debajo del mismo umbral. La estrategia verifica que no haya posición abierta actualmente antes de comprar a mercado.
- Entrada corta: Activada cuando el valor -DI desplazado sube por encima de
LevelMinus mientras su valor anterior estaba por debajo de ese nivel. Se envía una venta a mercado solo si no hay posición activa.
- Las salidas son manejadas exclusivamente por órdenes de protección iniciadas a través de
StartProtection: un take-profit y stop-loss fijos medidos en puntos de precio, equivalentes a TP y SL del código original.
Esta implementación evita intencionalmente el promediado en posiciones, reentradas mientras las operaciones están activas, o filtros adicionales, coincidiendo con el comportamiento ligero del EA fuente.
Parámetros
| Parámetro |
Predeterminado |
Descripción |
CandleType |
Marco temporal de 1 hora |
Marco temporal de la suscripción de velas usada para el cálculo de ADX. |
AdxPeriod |
28 |
Longitud del Average Directional Index y sus cálculos de DI. |
LevelPlus |
5 |
Umbral que la serie +DI debe superar para abrir una posición larga. |
LevelMinus |
5 |
Umbral que la serie -DI debe superar para abrir una posición corta. |
Shift |
1 |
Número de velas cerradas a mirar atrás al evaluar el cruce de DI (1 = vela anterior). |
TakeProfitPoints |
500 |
Distancia en puntos de precio para la orden de take-profit. Multiplicada internamente por el tamaño de tick del instrumento. |
StopLossPoints |
500 |
Distancia en puntos de precio para la orden de stop-loss de protección. |
TradeVolume |
0.1 |
Volumen base para nuevas órdenes de mercado, coincidiendo con el ajuste Lots en el experto MQL. |
Gestión de riesgos
StartProtection convierte los valores de take-profit y stop-loss basados en puntos en distancias de precio absolutas usando el PriceStep del instrumento.
- No se aplica trailing stop ni lógica de punto de equilibrio; las salidas ocurren únicamente a través de las órdenes de protección configuradas.
Notas y consejos
- Los umbrales de DI extremadamente bajos pueden llevar a operaciones de vaivén frecuentes, mientras que niveles más altos esperan impulsos direccionales más fuertes.
- El parámetro
Shift puede aumentarse cuando se necesita confirmación de velas anteriores, por ejemplo en marcos temporales más altos para filtrar el ruido intrábarra.
- Debido a que la estrategia opera solo una posición a la vez, se deben evitar las interferencias manuales o las operaciones externas en la misma cuenta para prevenir conflictos con el seguimiento interno de posición.
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()