Estratégia Ytg ADX Cruzamento de Nível
Esta estratégia porta o assessor especialista _ADX.mq5 de Yuriy Tokman para a API de alto nível do StockSharp. Ela monitora o Average Directional Index e reage quando os componentes +DI ou -DI ultrapassam limites configuráveis. As ordens são abertas apenas uma de cada vez, espelhando a lógica MQL original, e os níveis de stop-loss e take-profit de proteção expressos em pontos de preço são aplicados automaticamente.
Visão geral
- Regime de mercado: Funciona em movimentos tendenciais ou fortemente direcionais onde picos de DI acompanham rompimentos.
- Direção: Abre posições compradas ou vendidas, mas nunca ambas simultaneamente.
- Período: Controlado pelo parâmetro
CandleType (padrão velas de 1 hora).
- Dados: Usa velas finalizadas para calcular valores ADX/DI do indicador
AverageDirectionalIndex.
Lógica de trading
- Assinar a série de velas selecionada e construir o indicador ADX com o
AdxPeriod configurado.
- Para cada vela finalizada, coletar os valores +DI e -DI e manter apenas a quantidade de histórico exigida pelo parâmetro
Shift. Um Shift de 1, idêntico ao padrão MQL, avalia a vela fechada anterior.
- Entrada comprada: Ativada quando o valor +DI deslocado sobe acima de
LevelPlus enquanto seu valor anterior estava abaixo do mesmo limiar. A estratégia verifica que não há posição atualmente aberta antes de comprar a mercado.
- Entrada vendida: Ativada quando o valor -DI deslocado sobe acima de
LevelMinus enquanto seu valor anterior estava abaixo desse nível. Uma venda a mercado é enviada apenas se não houver posição ativa.
- As saídas são tratadas exclusivamente por ordens de proteção iniciadas através de
StartProtection: um take-profit e stop-loss fixos medidos em pontos de preço, equivalentes a TP e SL do código original.
Esta implementação evita intencionalmente o aumento médio em posições, reentradas enquanto as operações estão ativas, ou filtros adicionais, correspondendo ao comportamento leve do EA fonte.
Parâmetros
| Parâmetro |
Padrão |
Descrição |
CandleType |
Período de 1 hora |
Período da assinatura de velas usada para o cálculo do ADX. |
AdxPeriod |
28 |
Comprimento do Average Directional Index e seus cálculos de DI. |
LevelPlus |
5 |
Limiar que a série +DI deve exceder para abrir uma posição comprada. |
LevelMinus |
5 |
Limiar que a série -DI deve exceder para abrir uma posição vendida. |
Shift |
1 |
Número de velas fechadas a analisar retrospectivamente ao avaliar o cruzamento de DI (1 = vela anterior). |
TakeProfitPoints |
500 |
Distância em pontos de preço para a ordem de take-profit. Multiplicada internamente pelo tamanho de tick do instrumento. |
StopLossPoints |
500 |
Distância em pontos de preço para a ordem de stop-loss de proteção. |
TradeVolume |
0.1 |
Volume base para novas ordens a mercado, correspondendo à configuração Lots no especialista MQL. |
Gestão de risco
StartProtection converte os valores de take-profit e stop-loss baseados em pontos em distâncias de preço absolutas usando o PriceStep do instrumento.
- Nenhum trailing stop ou lógica de ponto de equilíbrio é aplicado; as saídas ocorrem exclusivamente através das ordens de proteção configuradas.
Notas e dicas
- Limites de DI extremamente baixos podem levar a operações de vai-e-vem frequentes, enquanto níveis mais altos esperam por impulsos direcionais mais fortes.
- O parâmetro
Shift pode ser aumentado quando é necessária confirmação de velas anteriores, por exemplo em períodos maiores para filtrar o ruído intrábarra.
- Como a estratégia opera apenas uma posição por vez, interferências manuais ou operações externas na mesma conta devem ser evitadas para prevenir conflitos com o rastreamento interno de posição.
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()