ADX Expert Strategy — это конвертация оригинального эксперта MetaTrader 4 "ADX Expert" (MQL-скрипт 20315). Стратегия ищет пересечения линий Directional Index (+DI и -DI), пока значение Average Directional Index (ADX) остаётся ниже заданного порога, что говорит о флэтовом рынке. Как и в исходном советнике, одновременно может быть открыта только одна позиция.
Логика торговли
Стратегия подписывается на выбранный тип свечей (по умолчанию 15-минутные) и рассчитывает ADX с указанным периодом.
Покупка открывается, когда:
+DI пересекает -DI снизу вверх;
ADX остаётся ниже порога (20 по умолчанию), указывая на слабый тренд;
текущий спред не превышает значение MaxSpreadPoints;
позиция отсутствует.
Продажа открывается при:
пересечении +DI ниже -DI;
ADX всё ещё ниже порога;
соблюдении фильтра по спреду и нулевой позиции.
Через StartProtection выставляются стоп-лосс и тейк-профит в ценовых пунктах (price points), что полностью повторяет механику MQL-версии. Значение 0 отключает соответствующий уровень.
Новые сигналы игнорируются, пока активная позиция не будет закрыта защитными ордерами.
Параметры
Параметр
Описание
Значение по умолчанию
TradeVolume
Объём каждого рыночного ордера.
0.1
AdxPeriod
Период расчёта ADX.
14
AdxThreshold
Максимальное значение ADX для допуска сделки.
20
MaxSpreadPoints
Допустимый максимум спреда в пунктах. 0 — отключить фильтр.
20
StopLossPoints
Размер стоп-лосса в пунктах.
200
TakeProfitPoints
Размер тейк-профита в пунктах.
400
CandleType
Тип свечей для расчётов (по умолчанию 15-минутные).
Таймфрейм 15 минут
Дополнительные замечания
Для контроля спреда необходимы обновления стакана заявок, чтобы получать лучшие цены Bid и Ask.
Все комментарии и сообщения журнала в коде написаны на английском языке в соответствии с требованиями репозитория.
Стратегия предоставляется исключительно в образовательных целях. Перед реальной торговлей протестируйте её на исторических данных и в демо-режиме.
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>
/// ADX crossover strategy translated from the original MQL expert.
/// Opens a single position when DI lines cross while ADX remains weak.
/// </summary>
public class AdxExpertStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxThreshold;
private readonly StrategyParam<decimal> _maxSpreadPoints;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private AverageDirectionalIndex _adx = null!;
private decimal _previousPlusDi;
private decimal _previousMinusDi;
private bool _hasPreviousDi;
/// <summary>
/// Trading volume for every market order.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// ADX calculation period.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// Maximum ADX level that still allows new trades.
/// </summary>
public decimal AdxThreshold
{
get => _adxThreshold.Value;
set => _adxThreshold.Value = value;
}
/// <summary>
/// Maximum allowed bid-ask spread measured in price points.
/// </summary>
public decimal MaxSpreadPoints
{
get => _maxSpreadPoints.Value;
set => _maxSpreadPoints.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in price points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="AdxExpertStrategy"/>.
/// </summary>
public AdxExpertStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Trade volume", "Order volume used for entries", "Risk management")
.SetOptimize(0.1m, 1m, 0.1m);
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ADX period", "Smoothing length for the ADX indicator", "Indicators")
.SetOptimize(7, 28, 7);
_adxThreshold = Param(nameof(AdxThreshold), 20m)
.SetGreaterThanZero()
.SetDisplay("ADX threshold", "Upper ADX limit that allows trades", "Signals")
.SetOptimize(15m, 35m, 5m);
_maxSpreadPoints = Param(nameof(MaxSpreadPoints), 20m)
.SetNotNegative()
.SetDisplay("Max spread (points)", "Maximum allowed bid-ask spread in points", "Risk management")
.SetOptimize(5m, 40m, 5m);
_stopLossPoints = Param(nameof(StopLossPoints), 200m)
.SetNotNegative()
.SetDisplay("Stop loss (points)", "Protective stop distance in price points", "Risk management")
.SetOptimize(100m, 400m, 50m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400m)
.SetNotNegative()
.SetDisplay("Take profit (points)", "Target distance in price points", "Risk management")
.SetOptimize(200m, 600m, 100m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle type", "Type of candles used for ADX", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousPlusDi = 0m;
_previousMinusDi = 0m;
_hasPreviousDi = false;
_entryPrice = 0m;
}
private decimal _entryPrice;
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_adx = new AverageDirectionalIndex { Length = AdxPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
IIndicatorValue adxResult;
try
{
adxResult = _adx.Process(candle);
}
catch (IndexOutOfRangeException)
{
return;
}
if (adxResult.IsEmpty || !_adx.IsFormed)
return;
if (adxResult is not AverageDirectionalIndexValue adxData)
return;
var plusDi = adxData.Dx.Plus ?? 0m;
var minusDi = adxData.Dx.Minus ?? 0m;
if (adxData.MovingAverage is not decimal currentAdx)
{
_previousPlusDi = plusDi;
_previousMinusDi = minusDi;
_hasPreviousDi = true;
return;
}
if (!_hasPreviousDi)
{
_previousPlusDi = plusDi;
_previousMinusDi = minusDi;
_hasPreviousDi = true;
return;
}
// Manage open position SL/TP
if (Position != 0)
{
var step = Security?.PriceStep ?? 1m;
if (Position > 0)
{
if (StopLossPoints > 0m && candle.LowPrice <= _entryPrice - StopLossPoints * step)
{
SellMarket(Position);
goto updateDi;
}
if (TakeProfitPoints > 0m && candle.HighPrice >= _entryPrice + TakeProfitPoints * step)
{
SellMarket(Position);
goto updateDi;
}
}
else
{
var vol = Math.Abs(Position);
if (StopLossPoints > 0m && candle.HighPrice >= _entryPrice + StopLossPoints * step)
{
BuyMarket(vol);
goto updateDi;
}
if (TakeProfitPoints > 0m && candle.LowPrice <= _entryPrice - TakeProfitPoints * step)
{
BuyMarket(vol);
goto updateDi;
}
}
}
var bullishCross = _previousPlusDi <= _previousMinusDi && plusDi > minusDi;
var bearishCross = _previousPlusDi >= _previousMinusDi && plusDi < minusDi;
if (currentAdx < AdxThreshold && Position == 0)
{
if (bullishCross)
{
BuyMarket(TradeVolume);
_entryPrice = candle.ClosePrice;
}
else if (bearishCross)
{
SellMarket(TradeVolume);
_entryPrice = candle.ClosePrice;
}
}
updateDi:
_previousPlusDi = plusDi;
_previousMinusDi = minusDi;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import AverageDirectionalIndex, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan
class adx_expert_strategy(Strategy):
def __init__(self):
super(adx_expert_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 0.1)
self._adx_period = self.Param("AdxPeriod", 14)
self._adx_threshold = self.Param("AdxThreshold", 20.0)
self._stop_loss_points = self.Param("StopLossPoints", 200.0)
self._take_profit_points = self.Param("TakeProfitPoints", 400.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2)))
self._adx = None
self._prev_plus_di = 0.0
self._prev_minus_di = 0.0
self._has_prev_di = False
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(adx_expert_strategy, self).OnStarted2(time)
self._adx = AverageDirectionalIndex()
self._adx.Length = self._adx_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
try:
civ = CandleIndicatorValue(self._adx, candle)
civ.IsFinal = True
adx_result = self._adx.Process(civ)
except Exception:
return
if adx_result.IsEmpty or not self._adx.IsFormed:
return
plus_di = float(adx_result.Dx.Plus) if adx_result.Dx.Plus is not None else 0.0
minus_di = float(adx_result.Dx.Minus) if adx_result.Dx.Minus is not None else 0.0
current_adx_val = adx_result.MovingAverage
if current_adx_val is None:
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
self._has_prev_di = True
return
current_adx = float(current_adx_val)
if not self._has_prev_di:
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
self._has_prev_di = True
return
# Manage SL/TP
if self.Position != 0:
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self.Position > 0:
if self._stop_loss_points.Value > 0 and float(candle.LowPrice) <= self._entry_price - self._stop_loss_points.Value * step:
self.SellMarket(self.Position)
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
return
if self._take_profit_points.Value > 0 and float(candle.HighPrice) >= self._entry_price + self._take_profit_points.Value * step:
self.SellMarket(self.Position)
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
return
else:
vol = abs(self.Position)
if self._stop_loss_points.Value > 0 and float(candle.HighPrice) >= self._entry_price + self._stop_loss_points.Value * step:
self.BuyMarket(vol)
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
return
if self._take_profit_points.Value > 0 and float(candle.LowPrice) <= self._entry_price - self._take_profit_points.Value * step:
self.BuyMarket(vol)
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
return
bullish_cross = self._prev_plus_di <= self._prev_minus_di and plus_di > minus_di
bearish_cross = self._prev_plus_di >= self._prev_minus_di and plus_di < minus_di
if current_adx < self._adx_threshold.Value and self.Position == 0:
if bullish_cross:
self.BuyMarket(self._trade_volume.Value)
self._entry_price = float(candle.ClosePrice)
elif bearish_cross:
self.SellMarket(self._trade_volume.Value)
self._entry_price = float(candle.ClosePrice)
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
def OnReseted(self):
super(adx_expert_strategy, self).OnReseted()
self._prev_plus_di = 0.0
self._prev_minus_di = 0.0
self._has_prev_di = False
self._entry_price = 0.0
def CreateClone(self):
return adx_expert_strategy()