DMI Winner
DMI Winner is a trend‑following strategy based on the Directional Movement Index
(DMI). It opens trades when the +DI and -DI lines cross and the Average
Directional Index (ADX) rises above a key threshold, signalling a strong trend.
An optional moving‑average filter keeps trades in the direction of the broader trend. A stop‑loss can also be enabled to cap downside risk, though by default the system relies on signal reversals for exits.
Details
- Data: Price candles.
- Entry Criteria:
- Long:
+DIcrosses above-DIANDADX>KeyLevel(with optional MA filter). - Short:
-DIcrosses above+DIANDADX>KeyLevel(with optional MA filter).
- Long:
- Exit Criteria: Opposite DI cross or stop‑loss if enabled.
- Stops: Optional stop‑loss (
UseSL). - Default Values:
DILength= 14KeyLevel= 23UseMA= TrueUseSL= False
- Filters:
- Category: Trend following
- Direction: Long & Short
- Indicators: DMI, Moving Average
- Complexity: Moderate
- Risk level: Medium
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>
/// DMI Winner strategy.
/// Goes long when +DI crosses above -DI and short when -DI crosses above +DI, both only while ADX is above KeyLevel
/// and, when UseMA is set, price is on the trade side of the moving average. The opposite DI cross closes the position
/// (and reverses it when it is a valid entry), and an optional percent stop-loss caps the risk.
/// </summary>
public class DmiWinnerStrategy : Strategy
{
private readonly StrategyParam<int> _diLength;
private readonly StrategyParam<int> _adxSmoothing;
private readonly StrategyParam<decimal> _keyLevel;
private readonly StrategyParam<bool> _useMa;
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<bool> _useSl;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private SmoothedMovingAverage _adx;
private decimal? _prevDiPlus;
private decimal? _prevDiMinus;
/// <summary>
/// Directional indicator period.
/// </summary>
public int DILength
{
get => _diLength.Value;
set => _diLength.Value = value;
}
/// <summary>
/// ADX smoothing period.
/// </summary>
public int ADXSmoothing
{
get => _adxSmoothing.Value;
set => _adxSmoothing.Value = value;
}
/// <summary>
/// ADX level a crossover needs to be traded.
/// </summary>
public decimal KeyLevel
{
get => _keyLevel.Value;
set => _keyLevel.Value = value;
}
/// <summary>
/// Enable the moving average trend filter.
/// </summary>
public bool UseMA
{
get => _useMa.Value;
set => _useMa.Value = value;
}
/// <summary>
/// Moving average period.
/// </summary>
public int MALength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
/// <summary>
/// Enable the stop-loss.
/// </summary>
public bool UseSL
{
get => _useSl.Value;
set => _useSl.Value = value;
}
/// <summary>
/// Stop-loss percentage from the entry price.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public DmiWinnerStrategy()
{
_diLength = Param(nameof(DILength), 14)
.SetGreaterThanZero()
.SetDisplay("DI Length", "Directional indicator period", "DMI");
_adxSmoothing = Param(nameof(ADXSmoothing), 13)
.SetGreaterThanZero()
.SetDisplay("ADX Smoothing", "ADX smoothing period", "DMI");
_keyLevel = Param(nameof(KeyLevel), 23m)
.SetDisplay("Key Level", "ADX level a crossover needs to be traded", "DMI");
_useMa = Param(nameof(UseMA), true)
.SetDisplay("Use MA", "Trade only on the trend side of the moving average", "Moving Average");
_maLength = Param(nameof(MALength), 50)
.SetGreaterThanZero()
.SetDisplay("MA Length", "Moving average period", "Moving Average");
_useSl = Param(nameof(UseSL), false)
.SetDisplay("Use Stop Loss", "Enable the percent stop-loss", "Risk");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Stop-loss percentage from the entry price", "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;
_prevDiPlus = null;
_prevDiMinus = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevDiPlus = null;
_prevDiMinus = null;
var dmi = new DirectionalIndex { Length = DILength };
// ADX is the DX of the DILength directional lines smoothed over ADXSmoothing bars.
_adx = new SmoothedMovingAverage { Length = ADXSmoothing };
var ma = new ExponentialMovingAverage { Length = MALength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(dmi, ma, ProcessCandle)
.Start();
if (UseSL)
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ma);
DrawOwnTrades(area);
var dmiArea = CreateChartArea();
if (dmiArea != null)
DrawIndicator(dmiArea, dmi);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue dmiValue, IIndicatorValue maValue)
{
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();
var prevPlus = _prevDiPlus;
var prevMinus = _prevDiMinus;
_prevDiPlus = diPlus;
_prevDiMinus = diMinus;
if (!_adx.IsFormed || !maValue.IsFormed || prevPlus is not decimal lastPlus || prevMinus is not decimal lastMinus)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var ma = maValue.ToDecimal();
var close = candle.ClosePrice;
var crossUp = lastPlus <= lastMinus && diPlus > diMinus;
var crossDown = lastPlus >= lastMinus && diPlus < diMinus;
var longSignal = crossUp && adx > KeyLevel && (!UseMA || close > ma);
var shortSignal = crossDown && adx > KeyLevel && (!UseMA || close < ma);
if (longSignal && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (shortSignal && 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, Unit, UnitTypes
from StockSharp.Algo.Indicators import DirectionalIndex, SmoothedMovingAverage, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class dmi_winner_strategy(Strategy):
"""
DMI Winner strategy.
Goes long when +DI crosses above -DI and short when -DI crosses above +DI, both only while ADX is above KeyLevel
and, when UseMA is set, price is on the trade side of the moving average. The opposite DI cross closes the position
(and reverses it when it is a valid entry), and an optional percent stop-loss caps the risk.
"""
def __init__(self):
super(dmi_winner_strategy, self).__init__()
self._di_length = self.Param("DILength", 14) \
.SetGreaterThanZero() \
.SetDisplay("DI Length", "Directional indicator period", "DMI")
self._adx_smoothing = self.Param("ADXSmoothing", 13) \
.SetGreaterThanZero() \
.SetDisplay("ADX Smoothing", "ADX smoothing period", "DMI")
self._key_level = self.Param("KeyLevel", 23.0) \
.SetDisplay("Key Level", "ADX level a crossover needs to be traded", "DMI")
self._use_ma = self.Param("UseMA", True) \
.SetDisplay("Use MA", "Trade only on the trend side of the moving average", "Moving Average")
self._ma_length = self.Param("MALength", 50) \
.SetGreaterThanZero() \
.SetDisplay("MA Length", "Moving average period", "Moving Average")
self._use_sl = self.Param("UseSL", False) \
.SetDisplay("Use Stop Loss", "Enable the percent stop-loss", "Risk")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss %", "Stop-loss percentage from the entry price", "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_di_plus = None
self._prev_di_minus = None
@property
def CandleType(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnReseted(self):
super(dmi_winner_strategy, self).OnReseted()
self._adx = None
self._prev_di_plus = None
self._prev_di_minus = None
def OnStarted2(self, time):
super(dmi_winner_strategy, self).OnStarted2(time)
self._prev_di_plus = None
self._prev_di_minus = None
dmi = DirectionalIndex()
dmi.Length = self._di_length.Value
# ADX is the DX of the DILength directional lines smoothed over ADXSmoothing bars.
self._adx = SmoothedMovingAverage()
self._adx.Length = self._adx_smoothing.Value
ma = ExponentialMovingAverage()
ma.Length = self._ma_length.Value
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(dmi, ma, self._process_candle).Start()
if self._use_sl.Value:
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ma)
self.DrawOwnTrades(area)
dmi_area = self.CreateChartArea()
if dmi_area is not None:
self.DrawIndicator(dmi_area, dmi)
def _process_candle(self, candle, dmi_value, ma_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)))
prev_plus = self._prev_di_plus
prev_minus = self._prev_di_minus
self._prev_di_plus = di_plus
self._prev_di_minus = di_minus
if not self._adx.IsFormed or not ma_value.IsFormed or prev_plus is None or prev_minus is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
ma = float(ma_value.GetValue[Decimal](None))
close = float(candle.ClosePrice)
key_level = float(self._key_level.Value)
use_ma = self._use_ma.Value
cross_up = prev_plus <= prev_minus and di_plus > di_minus
cross_down = prev_plus >= prev_minus and di_plus < di_minus
long_signal = cross_up and adx > key_level and (not use_ma or close > ma)
short_signal = cross_down and adx > key_level and (not use_ma or close < ma)
if long_signal and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif short_signal 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 dmi_winner_strategy()