MA Cross + DMI Strategy
Trades a crossover of fast and slow exponential moving averages only when the Directional Movement Index confirms trend strength. By waiting for +DI or -DI to dominate while ADX rises above a key level, the system filters out weak crossovers.
This strategy can enter long or short positions and exits on opposing crossovers. ADX filtering helps the method stay out of ranging periods where moving averages frequently whipsaw.
Details
- Entry Criteria:
- Long: Fast EMA crosses above slow EMA, +DI > -DI, and ADX above the key level.
- Short: Fast EMA crosses below slow EMA, -DI > +DI, and ADX above the key level.
- Exit Criteria:
- Opposite crossover or manual stop.
- Indicators:
- Two EMAs (periods 10 and 20)
- Directional Movement Index (length 14, ADX smoothing 14)
- Stops: None by default; can use StartProtection.
- Default Values:
Ma1Length= 10Ma2Length= 20DmiLength= 14AdxSmoothing= 14KeyLevel= 20
- Filters:
- Trend following
- Works on intraday to swing timeframes
- Indicators: EMA, DMI
- Stops: Optional
- Complexity: Basic
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>
/// MA Cross + DMI strategy.
/// Goes long when the fast EMA crosses above the slow EMA while +DI is above -DI and ADX is above KeyLevel, and short on
/// the mirrored setup. Any opposite EMA crossover closes the position, or reverses it when the DMI confirms.
/// </summary>
public class MaCrossDmiStrategy : Strategy
{
private readonly StrategyParam<int> _ma1Length;
private readonly StrategyParam<int> _ma2Length;
private readonly StrategyParam<int> _dmiLength;
private readonly StrategyParam<int> _adxSmoothing;
private readonly StrategyParam<decimal> _keyLevel;
private readonly StrategyParam<DataType> _candleType;
private SmoothedMovingAverage _adx;
private decimal? _prevMa1;
private decimal? _prevMa2;
/// <summary>
/// Fast EMA period.
/// </summary>
public int Ma1Length
{
get => _ma1Length.Value;
set => _ma1Length.Value = value;
}
/// <summary>
/// Slow EMA period.
/// </summary>
public int Ma2Length
{
get => _ma2Length.Value;
set => _ma2Length.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>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public MaCrossDmiStrategy()
{
_ma1Length = Param(nameof(Ma1Length), 10)
.SetGreaterThanZero()
.SetDisplay("MA1 Length", "Fast EMA period", "Moving Average");
_ma2Length = Param(nameof(Ma2Length), 20)
.SetGreaterThanZero()
.SetDisplay("MA2 Length", "Slow EMA period", "Moving Average");
_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");
_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;
_prevMa1 = null;
_prevMa2 = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevMa1 = null;
_prevMa2 = null;
var ma1 = new ExponentialMovingAverage { Length = Ma1Length };
var ma2 = new ExponentialMovingAverage { Length = Ma2Length };
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(ma1, ma2, dmi, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ma1);
DrawIndicator(area, ma2);
DrawOwnTrades(area);
var dmiArea = CreateChartArea();
if (dmiArea != null)
DrawIndicator(dmiArea, dmi);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue ma1Value, IIndicatorValue ma2Value, 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 (!ma1Value.IsFormed || !ma2Value.IsFormed)
return;
var ma1 = ma1Value.ToDecimal();
var ma2 = ma2Value.ToDecimal();
var prevMa1 = _prevMa1;
var prevMa2 = _prevMa2;
_prevMa1 = ma1;
_prevMa2 = ma2;
if (!_adx.IsFormed || prevMa1 is not decimal lastMa1 || prevMa2 is not decimal lastMa2)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var crossUp = lastMa1 <= lastMa2 && ma1 > ma2;
var crossDown = lastMa1 >= lastMa2 && ma1 < ma2;
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));
else if (Position > 0 && crossDown)
SellMarket(Position);
else if (Position < 0 && crossUp)
BuyMarket(-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
from StockSharp.Algo.Indicators import ExponentialMovingAverage, DirectionalIndex, SmoothedMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class ma_cross_dmi_strategy(Strategy):
"""
MA Cross + DMI strategy.
Goes long when the fast EMA crosses above the slow EMA while +DI is above -DI and ADX is above KeyLevel, and short on
the mirrored setup. Any opposite EMA crossover closes the position, or reverses it when the DMI confirms.
"""
def __init__(self):
super(ma_cross_dmi_strategy, self).__init__()
self._ma1_length = self.Param("Ma1Length", 10) \
.SetGreaterThanZero() \
.SetDisplay("MA1 Length", "Fast EMA period", "Moving Average")
self._ma2_length = self.Param("Ma2Length", 20) \
.SetGreaterThanZero() \
.SetDisplay("MA2 Length", "Slow EMA period", "Moving Average")
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._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle type", "Candle type for strategy calculation", "General")
self._adx = None
self._prev_ma1 = None
self._prev_ma2 = None
@property
def CandleType(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnReseted(self):
super(ma_cross_dmi_strategy, self).OnReseted()
self._adx = None
self._prev_ma1 = None
self._prev_ma2 = None
def OnStarted2(self, time):
super(ma_cross_dmi_strategy, self).OnStarted2(time)
self._prev_ma1 = None
self._prev_ma2 = None
ma1 = ExponentialMovingAverage()
ma1.Length = self._ma1_length.Value
ma2 = ExponentialMovingAverage()
ma2.Length = self._ma2_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(ma1, ma2, dmi, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ma1)
self.DrawIndicator(area, ma2)
self.DrawOwnTrades(area)
dmi_area = self.CreateChartArea()
if dmi_area is not None:
self.DrawIndicator(dmi_area, dmi)
def _process_candle(self, candle, ma1_value, ma2_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 ma1_value.IsFormed or not ma2_value.IsFormed:
return
ma1 = float(ma1_value.GetValue[Decimal](None))
ma2 = float(ma2_value.GetValue[Decimal](None))
prev_ma1 = self._prev_ma1
prev_ma2 = self._prev_ma2
self._prev_ma1 = ma1
self._prev_ma2 = ma2
if not self._adx.IsFormed or prev_ma1 is None or prev_ma2 is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
key_level = float(self._key_level.Value)
cross_up = prev_ma1 <= prev_ma2 and ma1 > ma2
cross_down = prev_ma1 >= prev_ma2 and ma1 < ma2
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))
elif self.Position > 0 and cross_down:
self.SellMarket(self.Position)
elif self.Position < 0 and cross_up:
self.BuyMarket(-self.Position)
def CreateClone(self):
return ma_cross_dmi_strategy()