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
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// MA Cross + DMI Strategy.
/// Uses MA crossover confirmed by DMI directional alignment.
/// Buys when fast MA crosses above slow MA and DI+ > DI-.
/// Sells when fast MA crosses below slow MA and DI- > DI+.
/// </summary>
public class MaCrossDmiStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleTypeParam;
private readonly StrategyParam<int> _ma1Length;
private readonly StrategyParam<int> _ma2Length;
private readonly StrategyParam<int> _dmiLength;
private readonly StrategyParam<int> _cooldownBars;
private ExponentialMovingAverage _ma1;
private ExponentialMovingAverage _ma2;
private DirectionalIndex _dmi;
private decimal _prevMa1;
private decimal _prevMa2;
private int _cooldownRemaining;
public MaCrossDmiStrategy()
{
_candleTypeParam = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle type", "Candle type for strategy calculation.", "General");
_ma1Length = Param(nameof(Ma1Length), 10)
.SetGreaterThanZero()
.SetDisplay("MA1 Length", "Fast moving average period", "Moving Average");
_ma2Length = Param(nameof(Ma2Length), 20)
.SetGreaterThanZero()
.SetDisplay("MA2 Length", "Slow moving average period", "Moving Average");
_dmiLength = Param(nameof(DmiLength), 14)
.SetGreaterThanZero()
.SetDisplay("DMI Length", "DMI period", "DMI");
_cooldownBars = Param(nameof(CooldownBars), 10)
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Risk");
}
public DataType CandleType
{
get => _candleTypeParam.Value;
set => _candleTypeParam.Value = value;
}
public int Ma1Length
{
get => _ma1Length.Value;
set => _ma1Length.Value = value;
}
public int Ma2Length
{
get => _ma2Length.Value;
set => _ma2Length.Value = value;
}
public int DmiLength
{
get => _dmiLength.Value;
set => _dmiLength.Value = value;
}
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ma1 = null;
_ma2 = null;
_dmi = null;
_prevMa1 = 0;
_prevMa2 = 0;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ma1 = new ExponentialMovingAverage { Length = Ma1Length };
_ma2 = new ExponentialMovingAverage { Length = Ma2Length };
_dmi = new DirectionalIndex { Length = DmiLength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_ma1, _ma2, _dmi, OnProcess)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ma1);
DrawIndicator(area, _ma2);
DrawOwnTrades(area);
}
}
private void OnProcess(ICandleMessage candle, IIndicatorValue ma1Value, IIndicatorValue ma2Value, IIndicatorValue dmiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_ma1.IsFormed || !_ma2.IsFormed || !_dmi.IsFormed)
return;
if (ma1Value.IsEmpty || ma2Value.IsEmpty || dmiValue.IsEmpty)
return;
var ma1Price = ma1Value.ToDecimal();
var ma2Price = ma2Value.ToDecimal();
var dmiData = (DirectionalIndexValue)dmiValue;
if (dmiData.Plus is not decimal diPlus || dmiData.Minus is not decimal diMinus)
{
_prevMa1 = ma1Price;
_prevMa2 = ma2Price;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_prevMa1 = ma1Price;
_prevMa2 = ma2Price;
return;
}
if (_cooldownRemaining > 0)
{
_cooldownRemaining--;
_prevMa1 = ma1Price;
_prevMa2 = ma2Price;
return;
}
if (_prevMa1 == 0 || _prevMa2 == 0)
{
_prevMa1 = ma1Price;
_prevMa2 = ma2Price;
return;
}
// MA crossover detection
var maCrossUp = ma1Price > ma2Price && _prevMa1 <= _prevMa2;
var maCrossDown = ma1Price < ma2Price && _prevMa1 >= _prevMa2;
// Buy: MA cross up + DI+ > DI- (bullish direction)
if (maCrossUp && diPlus > diMinus && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
_cooldownRemaining = CooldownBars;
}
// Sell: MA cross down + DI- > DI+ (bearish direction)
else if (maCrossDown && diMinus > diPlus && Position >= 0)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(Volume);
_cooldownRemaining = CooldownBars;
}
// Exit long on MA cross down (without DMI requirement)
else if (Position > 0 && maCrossDown)
{
SellMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
// Exit short on MA cross up (without DMI requirement)
else if (Position < 0 && maCrossUp)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
_prevMa1 = ma1Price;
_prevMa2 = ma2Price;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, DirectionalIndex, IndicatorHelper
from StockSharp.Algo.Strategies import Strategy
class ma_cross_dmi_strategy(Strategy):
"""MA Cross + DMI Strategy. MA crossover confirmed by DMI direction."""
def __init__(self):
super(ma_cross_dmi_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle type", "Candle type for strategy calculation.", "General")
self._ma1_length = self.Param("Ma1Length", 10) \
.SetDisplay("MA1 Length", "Fast moving average period", "Moving Average")
self._ma2_length = self.Param("Ma2Length", 20) \
.SetDisplay("MA2 Length", "Slow moving average period", "Moving Average")
self._dmi_length = self.Param("DmiLength", 14) \
.SetDisplay("DMI Length", "DMI period", "DMI")
self._cooldown_bars = self.Param("CooldownBars", 10) \
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Risk")
self._ma1 = None
self._ma2 = None
self._dmi = None
self._prev_ma1 = 0.0
self._prev_ma2 = 0.0
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(ma_cross_dmi_strategy, self).OnReseted()
self._ma1 = None
self._ma2 = None
self._dmi = None
self._prev_ma1 = 0.0
self._prev_ma2 = 0.0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(ma_cross_dmi_strategy, self).OnStarted2(time)
self._ma1 = ExponentialMovingAverage()
self._ma1.Length = int(self._ma1_length.Value)
self._ma2 = ExponentialMovingAverage()
self._ma2.Length = int(self._ma2_length.Value)
self._dmi = DirectionalIndex()
self._dmi.Length = int(self._dmi_length.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(self._ma1, self._ma2, self._dmi, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ma1)
self.DrawIndicator(area, self._ma2)
self.DrawOwnTrades(area)
def _on_process(self, candle, ma1_value, ma2_value, dmi_value):
if candle.State != CandleStates.Finished:
return
if not self._ma1.IsFormed or not self._ma2.IsFormed or not self._dmi.IsFormed:
return
if ma1_value.IsEmpty or ma2_value.IsEmpty or dmi_value.IsEmpty:
return
ma1 = float(IndicatorHelper.ToDecimal(ma1_value))
ma2 = float(IndicatorHelper.ToDecimal(ma2_value))
if dmi_value.Plus is None or dmi_value.Minus is None:
self._prev_ma1 = ma1
self._prev_ma2 = ma2
return
di_plus = float(dmi_value.Plus)
di_minus = float(dmi_value.Minus)
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_ma1 = ma1
self._prev_ma2 = ma2
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._prev_ma1 = ma1
self._prev_ma2 = ma2
return
if self._prev_ma1 == 0.0 or self._prev_ma2 == 0.0:
self._prev_ma1 = ma1
self._prev_ma2 = ma2
return
cooldown = int(self._cooldown_bars.Value)
ma_cross_up = ma1 > ma2 and self._prev_ma1 <= self._prev_ma2
ma_cross_down = ma1 < ma2 and self._prev_ma1 >= self._prev_ma2
if ma_cross_up and di_plus > di_minus and self.Position <= 0:
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self.BuyMarket(self.Volume)
self._cooldown_remaining = cooldown
elif ma_cross_down and di_minus > di_plus and self.Position >= 0:
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self.SellMarket(self.Volume)
self._cooldown_remaining = cooldown
elif self.Position > 0 and ma_cross_down:
self.SellMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
elif self.Position < 0 and ma_cross_up:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
self._prev_ma1 = ma1
self._prev_ma2 = ma2
def CreateClone(self):
return ma_cross_dmi_strategy()