MACD + DMI Strategy
Combines the Moving Average Convergence Divergence with the Directional Movement Index to trade only when trend strength is confirmed. The system waits for a MACD crossover and checks that the dominant directional line exceeds the opposite line while the ADX is above a key level.
The strategy is designed for both long and short positions. By pairing momentum and trend filters, it aims to avoid whipsaws in sideways markets. Protective stops based on volatility keep risk contained.
Details
- Entry Criteria:
- Long: MACD line crosses above signal, +DI > -DI, and ADX above the key level.
- Short: MACD line crosses below signal, -DI > +DI, and ADX above the key level.
- Exit Criteria:
- Reverse signal or volatility stop hit.
- Indicators:
- MACD (fast 12, slow 26, signal 9)
- Directional Movement Index (length 14, ADX smoothing 14)
- Stops: Uses built-in stop-loss and take-profit via StartProtection.
- Default Values:
Ma1Length= 10Ma2Length= 20DmiLength= 14AdxSmoothing= 14KeyLevel= 20
- Filters:
- Trend following
- Works on multiple timeframes
- Indicators: MACD, DMI
- Stops: Yes
- Complexity: Moderate
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// MACD + DMI strategy.
/// The MACD line is the difference of Ma1Length and Ma2Length EMAs. A long opens when it crosses above its signal line
/// while +DI is above -DI and ADX is above KeyLevel; a short opens on the mirrored setup. The reverse signal flips the
/// position, and percent stop-loss and take-profit protection limit each trade.
/// </summary>
public class MacdDmiStrategy : Strategy
{
private readonly StrategyParam<int> _ma1Length;
private readonly StrategyParam<int> _ma2Length;
private readonly StrategyParam<int> _signalLength;
private readonly StrategyParam<int> _dmiLength;
private readonly StrategyParam<int> _adxSmoothing;
private readonly StrategyParam<decimal> _keyLevel;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<decimal> _takeProfitPercent;
private readonly StrategyParam<DataType> _candleType;
private SmoothedMovingAverage _adx;
private decimal? _prevMacd;
private decimal? _prevSignal;
/// <summary>
/// Fast EMA period of MACD.
/// </summary>
public int Ma1Length
{
get => _ma1Length.Value;
set => _ma1Length.Value = value;
}
/// <summary>
/// Slow EMA period of MACD.
/// </summary>
public int Ma2Length
{
get => _ma2Length.Value;
set => _ma2Length.Value = value;
}
/// <summary>
/// MACD signal line period.
/// </summary>
public int SignalLength
{
get => _signalLength.Value;
set => _signalLength.Value = value;
}
/// <summary>
/// Directional movement period.
/// </summary>
public int DmiLength
{
get => _dmiLength.Value;
set => _dmiLength.Value = value;
}
/// <summary>
/// ADX smoothing period.
/// </summary>
public int AdxSmoothing
{
get => _adxSmoothing.Value;
set => _adxSmoothing.Value = value;
}
/// <summary>
/// ADX level required for entries.
/// </summary>
public decimal KeyLevel
{
get => _keyLevel.Value;
set => _keyLevel.Value = value;
}
/// <summary>
/// Stop-loss percentage. 0 disables it.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Take-profit percentage. 0 disables it.
/// </summary>
public decimal TakeProfitPercent
{
get => _takeProfitPercent.Value;
set => _takeProfitPercent.Value = value;
}
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public MacdDmiStrategy()
{
_ma1Length = Param(nameof(Ma1Length), 10)
.SetGreaterThanZero()
.SetDisplay("MA1 Length", "Fast EMA period of MACD", "MACD");
_ma2Length = Param(nameof(Ma2Length), 20)
.SetGreaterThanZero()
.SetDisplay("MA2 Length", "Slow EMA period of MACD", "MACD");
_signalLength = Param(nameof(SignalLength), 9)
.SetGreaterThanZero()
.SetDisplay("Signal Length", "MACD signal line period", "MACD");
_dmiLength = Param(nameof(DmiLength), 14)
.SetGreaterThanZero()
.SetDisplay("DMI Length", "Directional movement period", "DMI");
_adxSmoothing = Param(nameof(AdxSmoothing), 14)
.SetGreaterThanZero()
.SetDisplay("ADX Smoothing", "ADX smoothing period", "DMI");
_keyLevel = Param(nameof(KeyLevel), 20m)
.SetDisplay("Key Level", "ADX level required for entries", "DMI");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop-loss percentage", "Risk");
_takeProfitPercent = Param(nameof(TakeProfitPercent), 4m)
.SetNotNegative()
.SetDisplay("Take Profit %", "Take-profit percentage", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle type", "Candle type for strategy calculation", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_adx = null;
_prevMacd = null;
_prevSignal = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevMacd = null;
_prevSignal = null;
var macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = Ma1Length },
LongMa = { Length = Ma2Length },
},
SignalMa = { Length = SignalLength },
};
var dmi = new DirectionalIndex { Length = DmiLength };
// ADX is the DX of the DmiLength directional lines smoothed over AdxSmoothing bars.
_adx = new SmoothedMovingAverage { Length = AdxSmoothing };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(macd, dmi, ProcessCandle)
.Start();
StartProtection(
TakeProfitPercent > 0 ? new Unit(TakeProfitPercent, UnitTypes.Percent) : new Unit(),
StopLossPercent > 0 ? new Unit(StopLossPercent, UnitTypes.Percent) : new Unit(),
useMarketOrders: true);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
{
DrawIndicator(oscillators, macd);
DrawIndicator(oscillators, dmi);
}
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue, IIndicatorValue dmiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!dmiValue.IsFormed)
return;
var dmi = (DirectionalIndexValue)dmiValue;
if (dmi.Plus is not decimal diPlus || dmi.Minus is not decimal diMinus)
return;
var diSum = diPlus + diMinus;
var dx = diSum == 0 ? 0m : 100m * Math.Abs(diPlus - diMinus) / diSum;
var adx = _adx.Process(dx, candle.ServerTime, true).ToDecimal();
if (!macdValue.IsFormed)
return;
var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
return;
var prevMacd = _prevMacd;
var prevSignal = _prevSignal;
_prevMacd = macd;
_prevSignal = signal;
if (!_adx.IsFormed || prevMacd is not decimal lastMacd || prevSignal is not decimal lastSignal)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var crossUp = lastMacd <= lastSignal && macd > signal;
var crossDown = lastMacd >= lastSignal && macd < signal;
if (crossUp && diPlus > diMinus && adx > KeyLevel && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (crossDown && diMinus > diPlus && adx > KeyLevel && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal, DirectionalIndex, SmoothedMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class macd_dmi_strategy(Strategy):
"""
MACD + DMI strategy.
The MACD line is the difference of Ma1Length and Ma2Length EMAs. A long opens when it crosses above its signal line
while +DI is above -DI and ADX is above KeyLevel; a short opens on the mirrored setup. The reverse signal flips the
position, and percent stop-loss and take-profit protection limit each trade.
"""
def __init__(self):
super(macd_dmi_strategy, self).__init__()
self._ma1_length = self.Param("Ma1Length", 10) \
.SetGreaterThanZero() \
.SetDisplay("MA1 Length", "Fast EMA period of MACD", "MACD")
self._ma2_length = self.Param("Ma2Length", 20) \
.SetGreaterThanZero() \
.SetDisplay("MA2 Length", "Slow EMA period of MACD", "MACD")
self._signal_length = self.Param("SignalLength", 9) \
.SetGreaterThanZero() \
.SetDisplay("Signal Length", "MACD signal line period", "MACD")
self._dmi_length = self.Param("DmiLength", 14) \
.SetGreaterThanZero() \
.SetDisplay("DMI Length", "Directional movement period", "DMI")
self._adx_smoothing = self.Param("AdxSmoothing", 14) \
.SetGreaterThanZero() \
.SetDisplay("ADX Smoothing", "ADX smoothing period", "DMI")
self._key_level = self.Param("KeyLevel", 20.0) \
.SetDisplay("Key Level", "ADX level required for entries", "DMI")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetNotNegative() \
.SetDisplay("Stop Loss %", "Stop-loss percentage", "Risk")
self._take_profit_percent = self.Param("TakeProfitPercent", 4.0) \
.SetNotNegative() \
.SetDisplay("Take Profit %", "Take-profit percentage", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle type", "Candle type for strategy calculation", "General")
self._adx = None
self._prev_macd = None
self._prev_signal = None
@property
def CandleType(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnReseted(self):
super(macd_dmi_strategy, self).OnReseted()
self._adx = None
self._prev_macd = None
self._prev_signal = None
def OnStarted2(self, time):
super(macd_dmi_strategy, self).OnStarted2(time)
self._prev_macd = None
self._prev_signal = None
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self._ma1_length.Value
macd.Macd.LongMa.Length = self._ma2_length.Value
macd.SignalMa.Length = self._signal_length.Value
dmi = DirectionalIndex()
dmi.Length = self._dmi_length.Value
# ADX is the DX of the DmiLength directional lines smoothed over AdxSmoothing bars.
self._adx = SmoothedMovingAverage()
self._adx.Length = self._adx_smoothing.Value
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(macd, dmi, self._process_candle).Start()
tp = float(self._take_profit_percent.Value)
sl = float(self._stop_loss_percent.Value)
self.StartProtection(
Unit(Decimal(tp), UnitTypes.Percent) if tp > 0 else Unit(),
Unit(Decimal(sl), UnitTypes.Percent) if sl > 0 else Unit(),
useMarketOrders=True)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, macd)
self.DrawIndicator(oscillators, dmi)
def _process_candle(self, candle, macd_value, dmi_value):
if candle.State != CandleStates.Finished:
return
if not dmi_value.IsFormed:
return
if dmi_value.Plus is None or dmi_value.Minus is None:
return
di_plus = float(dmi_value.Plus)
di_minus = float(dmi_value.Minus)
di_sum = di_plus + di_minus
dx = 0.0 if di_sum == 0 else 100.0 * abs(di_plus - di_minus) / di_sum
adx = float(to_decimal(process_float(self._adx, dx, candle.ServerTime, True)))
if not macd_value.IsFormed:
return
if macd_value.Macd is None or macd_value.Signal is None:
return
macd = float(macd_value.Macd)
signal = float(macd_value.Signal)
prev_macd = self._prev_macd
prev_signal = self._prev_signal
self._prev_macd = macd
self._prev_signal = signal
if not self._adx.IsFormed or prev_macd is None or prev_signal is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
key_level = float(self._key_level.Value)
cross_up = prev_macd <= prev_signal and macd > signal
cross_down = prev_macd >= prev_signal and macd < signal
if cross_up and di_plus > di_minus and adx > key_level and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif cross_down and di_minus > di_plus and adx > key_level and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
def CreateClone(self):
return macd_dmi_strategy()