The original MetaTrader expert advisor "Dynamic Stop Loss" does not open new trades on its own. Instead it watches existing market positions and, once a new candle appears, repositions the protective stop-loss so that it stays at a fixed distance behind the latest price. The StockSharp port keeps the same behaviour: every completed bar triggers a recalculation of the protective stop for whichever side is currently open. If no position exists, the strategy simply idles until a new position is detected.
How it works
The strategy subscribes to candles defined by the Candle Type parameter (default 1-minute timeframe).
When a candle closes the close price is multiplied by the user-selected point distance. The distance is converted from MetaTrader-style points into an absolute price delta via Security.PriceStep (fallback to Security.Step, then to 1).
If a long position is open the strategy cancels any existing stop order and places a new sell stop at Close - Distance.
If a short position is open the stop is moved to Close + Distance using a buy stop order.
When the position is closed (manually or by the stop filling) the trailing order is cancelled to avoid stale protection orders.
This produces the same constantly re-anchored stop distance as the MQL version, meaning the stop can move both closer to and further from the market as candles fluctuate.
Parameters
Name
Default
Description
StopLossPoints
800
Distance between the market price and the protective stop measured in instrument points. The value is multiplied by Security.PriceStep (fallback to Security.Step, then 1) before being applied to the close price. Set to 0 to disable stop management.
CandleType
TimeFrameCandle(00:01:00)
Candle type that defines when the stop is recalculated. Choose a timeframe matching the chart used in MetaTrader.
Usage notes
The strategy expects trades to be opened by external strategies, manual operations or other components. It only manages the stop-loss.
Ensure the security metadata (PriceStep, Step, volume) is filled so that the point-to-price conversion matches the broker's tick size. Instruments quoted with fractional pips must expose the proper step.
Because the stop is recomputed on every candle close it will follow the price even when the market moves against the position. This mirrors the MetaTrader logic where OrderModify always uses the latest Bid/Ask minus/plus the configured distance.
The created stop orders always replace the previous one to keep the platform in sync with the latest protective level.
namespace StockSharp.Samples.Strategies;
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// Dynamic Stop Loss strategy: EMA trend with ATR-based dynamic stop management.
/// Enters on EMA trend direction, exits when price moves against by ATR distance.
/// </summary>
public class DynamicStopLossStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<int> _signalCooldownCandles;
private decimal _entryPrice;
private decimal _stopPrice;
private bool _prevAboveEma;
private bool _hasPrevSignal;
private int _candlesSinceTrade;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }
public DynamicStopLossStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_emaPeriod = Param(nameof(EmaPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "EMA trend period", "Indicators");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR period for stop distance", "Indicators");
_atrMultiplier = Param(nameof(AtrMultiplier), 1.5m)
.SetDisplay("ATR Multiplier", "ATR multiplier for stop distance", "Risk");
_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 12)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between entries", "Trading");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0m;
_stopPrice = 0m;
_prevAboveEma = false;
_hasPrevSignal = false;
_candlesSinceTrade = SignalCooldownCandles;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entryPrice = 0;
_stopPrice = 0;
_hasPrevSignal = false;
_candlesSinceTrade = SignalCooldownCandles;
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ema, atr, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal ema, decimal atr)
{
if (candle.State != CandleStates.Finished) return;
var close = candle.ClosePrice;
var stopDist = atr * AtrMultiplier;
var aboveEma = close > ema;
if (_candlesSinceTrade < SignalCooldownCandles)
_candlesSinceTrade++;
if (Position > 0)
{
var newStop = close - stopDist;
if (newStop > _stopPrice) _stopPrice = newStop;
if (close <= _stopPrice)
{
SellMarket();
_entryPrice = 0;
_stopPrice = 0;
_candlesSinceTrade = 0;
_prevAboveEma = aboveEma;
_hasPrevSignal = true;
return;
}
}
else if (Position < 0)
{
var newStop = close + stopDist;
if (newStop < _stopPrice || _stopPrice == 0) _stopPrice = newStop;
if (close >= _stopPrice)
{
BuyMarket();
_entryPrice = 0;
_stopPrice = 0;
_candlesSinceTrade = 0;
_prevAboveEma = aboveEma;
_hasPrevSignal = true;
return;
}
}
if (_hasPrevSignal && aboveEma != _prevAboveEma && _candlesSinceTrade >= SignalCooldownCandles)
{
if (aboveEma && Position <= 0)
{
BuyMarket();
_entryPrice = close;
_stopPrice = close - stopDist;
_candlesSinceTrade = 0;
}
else if (!aboveEma && Position >= 0)
{
SellMarket();
_entryPrice = close;
_stopPrice = close + stopDist;
_candlesSinceTrade = 0;
}
}
_prevAboveEma = aboveEma;
_hasPrevSignal = true;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class dynamic_stop_loss_strategy(Strategy):
def __init__(self):
super(dynamic_stop_loss_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 100) \
.SetDisplay("EMA Period", "EMA trend period", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "ATR period for stop distance", "Indicators")
self._atr_multiplier = self.Param("AtrMultiplier", 1.5) \
.SetDisplay("ATR Multiplier", "ATR multiplier for stop distance", "Risk")
self._signal_cooldown = self.Param("SignalCooldownCandles", 12) \
.SetDisplay("Signal Cooldown", "Bars to wait between entries", "Trading")
self._ema = None
self._atr = None
self._entry_price = 0.0
self._stop_price = 0.0
self._prev_above_ema = False
self._has_prev_signal = False
self._candles_since_trade = 0
@property
def ema_period(self):
return self._ema_period.Value
@property
def atr_period(self):
return self._atr_period.Value
@property
def atr_multiplier(self):
return self._atr_multiplier.Value
@property
def signal_cooldown(self):
return self._signal_cooldown.Value
def OnReseted(self):
super(dynamic_stop_loss_strategy, self).OnReseted()
self._ema = None
self._atr = None
self._entry_price = 0.0
self._stop_price = 0.0
self._prev_above_ema = False
self._has_prev_signal = False
self._candles_since_trade = self.signal_cooldown
def OnStarted2(self, time):
super(dynamic_stop_loss_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = self.ema_period
self._atr = AverageTrueRange()
self._atr.Length = self.atr_period
self._entry_price = 0.0
self._stop_price = 0.0
self._has_prev_signal = False
self._candles_since_trade = self.signal_cooldown
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(30)))
subscription.Bind(self._ema, self._atr, self._process_candle)
subscription.Start()
def _process_candle(self, candle, ema_value, atr_value):
if candle.State != CandleStates.Finished:
return
if not self._ema.IsFormed or not self._atr.IsFormed:
return
close = float(candle.ClosePrice)
ema_val = float(ema_value)
atr_val = float(atr_value)
stop_dist = atr_val * self.atr_multiplier
above_ema = close > ema_val
if self._candles_since_trade < self.signal_cooldown:
self._candles_since_trade += 1
if self.Position > 0:
new_stop = close - stop_dist
if new_stop > self._stop_price:
self._stop_price = new_stop
if close <= self._stop_price:
self.SellMarket()
self._entry_price = 0.0
self._stop_price = 0.0
self._candles_since_trade = 0
self._prev_above_ema = above_ema
self._has_prev_signal = True
return
elif self.Position < 0:
new_stop = close + stop_dist
if new_stop < self._stop_price or self._stop_price == 0.0:
self._stop_price = new_stop
if close >= self._stop_price:
self.BuyMarket()
self._entry_price = 0.0
self._stop_price = 0.0
self._candles_since_trade = 0
self._prev_above_ema = above_ema
self._has_prev_signal = True
return
if self._has_prev_signal and above_ema != self._prev_above_ema and self._candles_since_trade >= self.signal_cooldown:
if above_ema and self.Position <= 0:
self.BuyMarket()
self._entry_price = close
self._stop_price = close - stop_dist
self._candles_since_trade = 0
elif not above_ema and self.Position >= 0:
self.SellMarket()
self._entry_price = close
self._stop_price = close + stop_dist
self._candles_since_trade = 0
self._prev_above_ema = above_ema
self._has_prev_signal = True
def CreateClone(self):
return dynamic_stop_loss_strategy()