Ver en GitHub

Dynamic Stop Loss

Overview

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

  1. The strategy subscribes to candles defined by the Candle Type parameter (default 1-minute timeframe).
  2. 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).
  3. If a long position is open the strategy cancels any existing stop order and places a new sell stop at Close - Distance.
  4. If a short position is open the stop is moved to Close + Distance using a buy stop order.
  5. 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;
	}
}