ADX Expert Strategy
Overview
The ADX Expert Strategy is a direct conversion of the original MetaTrader 4 expert advisor "ADX Expert" (MQL script 20315). The expert looks for crosses between the positive and negative Directional Index (DI) lines while the Average Directional Index (ADX) remains below a specified threshold, indicating that the market is ranging. Only one position can be open at a time, just like in the source expert.
Trading Logic
- The strategy subscribes to the selected candle series (15-minute candles by default) and calculates the Average Directional Index with the configured period.
- A buy order is placed when:
- The +DI line crosses above the -DI line.
- The ADX value stays below the defined threshold (default 20), signaling a weak trend.
- The current spread is below the
MaxSpreadPoints filter.
- No position is currently open.
- A sell order is placed when:
- The +DI line crosses below the -DI line.
- The ADX value is still lower than the allowed threshold.
- The spread requirement and flat-position condition are satisfied.
- Protective stop-loss and take-profit levels are assigned through
StartProtection, mirroring the fixed stop and target from the MQL version. They are expressed in price points (price steps) and can be disabled by setting the values to zero.
The strategy relies on a single position workflow: new signals are ignored until the current position is closed by its protective orders.
Parameters
| Parameter |
Description |
Default |
TradeVolume |
Order size used for every market order. |
0.1 |
AdxPeriod |
Period for ADX calculation. |
14 |
AdxThreshold |
Maximum ADX value that still allows a trade. |
20 |
MaxSpreadPoints |
Maximum allowed spread in price points. Set to 0 to disable the filter. |
20 |
StopLossPoints |
Stop-loss distance in price points. |
200 |
TakeProfitPoints |
Take-profit distance in price points. |
400 |
CandleType |
Candle type for indicator calculations (15-minute candles by default). |
15-minute time frame |
Additional Notes
- The spread filter requires order book updates to read best bid and ask prices. Ensure that your data provider supplies this information.
- All comments and logs are written in English for clarity, complying with repository guidelines.
- The strategy is intended for educational purposes. Test it thoroughly in a simulated environment before deploying it to live trading.
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()