Ver no GitHub

Stopreversal Tm Strategy

Overview

The Stopreversal Tm strategy is a direct translation of the original MetaTrader 5 expert advisor Exp_Stopreversal_Tm.mq5. The trading idea follows the Stopreversal custom indicator, which maintains a dynamic trailing stop around price and generates reversal alerts whenever price crosses that trailing boundary. The strategy operates on a single instrument and a single candle feed and is designed for trend reversal trading with a user-defined session filter.

Signal Generation

The Stopreversal indicator computes a reference price from the selected applied price mode and then adjusts a trailing stop level by Sensitivity (the nPips parameter). Whenever the new applied price crosses above the trailing stop while the previous bar was below it, a bullish signal is produced. Conversely, a bearish signal appears when the new price drops below the trailing stop after being above it. Each bullish signal simultaneously requests closing of existing short positions and opening of a new long, while each bearish signal closes longs and opens shorts.

To reproduce the behaviour of the original MetaTrader implementation, the strategy can delay the execution of signals by several completed bars (Signal Bar Delay). This replicates the SignalBar input from the expert advisor and prevents trading on the still-forming candle.

Session Filter and Position Handling

The expert advisor allowed trading only within a specified time window. The converted strategy keeps the same logic: when the Use Time Filter flag is enabled, orders are allowed only inside the session configured by Start Hour/Minute and End Hour/Minute. If the current time leaves the permitted window, any open position is flattened immediately. Signal-driven exits remain active even when the session is disabled.

The strategy works on net positions. A closing action is always executed before an opposite entry, guaranteeing that the direction changes without overlapping exposures.

Parameters

  • Allow Buy Entries / Allow Sell Entries – enable or disable opening new long or short positions when the corresponding signal is received.
  • Allow Long Exits / Allow Short Exits – control whether opposite signals are allowed to close existing positions.
  • Use Time Filter – toggles the trading session window.
  • Start Hour / Start Minute / End Hour / End Minute – define the inclusive start and exclusive end of the trading window. The time filter supports overnight sessions where the end time is earlier than the start time.
  • Sensitivity (nPips) – relative distance (expressed as a multiplier, e.g., 0.004 = 0.4%) used to move the trailing stop closer or further from price.
  • Signal Bar Delay (SignalBar) – number of completed candles to wait before acting on a signal. 0 executes immediately on the closing candle, 1 reproduces the default behaviour of acting on the previous bar.
  • Candle Type – timeframe of the candle subscription used for indicator calculations.
  • Applied Price – choice of price series (close, open, median, trend-following modes, Demark price, etc.) that feeds the trailing stop calculation.

Implementation Notes

  • The indicator is implemented directly inside the strategy without relying on external buffers, ensuring that the nPips trailing stop logic matches the original MQL5 code.
  • Session management and signal sequencing follow the original expert, including the priority of closing existing exposure before opening new trades.
  • The conversion focuses on the high-level StockSharp API: candle subscriptions, delayed signal queue, and market orders (BuyMarket / SellMarket). Money management features tied to MetaTrader account metrics were omitted because StockSharp strategies already operate with explicit position sizes.
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Stopreversal trailing stop strategy with a configurable trading session filter.
/// </summary>
public class StopreversalTmStrategy : Strategy
{
	/// <summary>
	/// Available price sources for the Stopreversal trailing stop.
	/// </summary>
	public enum StopreversalAppliedPrices
	{
		Close = 1,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Simple,
		Quarter,
		TrendFollow0,
		TrendFollow1,
		Demark
	}

	private readonly StrategyParam<bool> _allowBuyEntry;
	private readonly StrategyParam<bool> _allowSellEntry;
	private readonly StrategyParam<bool> _allowBuyExit;
	private readonly StrategyParam<bool> _allowSellExit;
	private readonly StrategyParam<bool> _useTimeFilter;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _startMinute;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _endMinute;
	private readonly StrategyParam<decimal> _nPips;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<StopreversalAppliedPrices> _appliedPrice;

	private readonly List<SignalInfo> _signalQueue = new();

	private decimal? _previousAppliedPrice;
	private decimal? _previousStopLevel;

	public StopreversalTmStrategy()
	{
		_allowBuyEntry = Param(nameof(AllowBuyEntry), true)
			.SetDisplay("Allow Buy Entries", "Enable opening long positions on bullish signals", "Signals")
			;

		_allowSellEntry = Param(nameof(AllowSellEntry), true)
			.SetDisplay("Allow Sell Entries", "Enable opening short positions on bearish signals", "Signals")
			;

		_allowBuyExit = Param(nameof(AllowBuyExit), true)
			.SetDisplay("Allow Long Exits", "Close existing long positions when a sell signal arrives", "Signals")
			;

		_allowSellExit = Param(nameof(AllowSellExit), true)
			.SetDisplay("Allow Short Exits", "Close existing short positions when a buy signal arrives", "Signals")
			;

		_useTimeFilter = Param(nameof(UseTimeFilter), false)
			.SetDisplay("Use Time Filter", "Restrict trading to the configured session", "Session");

		_startHour = Param(nameof(StartHour), 0)
			.SetRange(0, 23)
			.SetDisplay("Start Hour", "Session start hour (0-23)", "Session")
			;

		_startMinute = Param(nameof(StartMinute), 0)
			.SetRange(0, 59)
			.SetDisplay("Start Minute", "Session start minute (0-59)", "Session")
			;

		_endHour = Param(nameof(EndHour), 23)
			.SetRange(0, 23)
			.SetDisplay("End Hour", "Session end hour (0-23)", "Session")
			;

		_endMinute = Param(nameof(EndMinute), 59)
			.SetRange(0, 59)
			.SetDisplay("End Minute", "Session end minute (0-59)", "Session")
			;

		_nPips = Param(nameof(Npips), 0.004m)
			.SetGreaterThanZero()
			.SetDisplay("Sensitivity", "Relative offset used to build the trailing stop", "Indicator")
			;

		_signalBar = Param(nameof(SignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Signal Bar Delay", "Number of completed bars to wait before acting", "Indicator")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");

		_appliedPrice = Param(nameof(AppliedPrice), StopreversalAppliedPrices.Close)
			.SetDisplay("Applied Price", "Price source for the trailing stop", "Indicator")
			;
	}

	public bool AllowBuyEntry { get => _allowBuyEntry.Value; set => _allowBuyEntry.Value = value; }
	public bool AllowSellEntry { get => _allowSellEntry.Value; set => _allowSellEntry.Value = value; }
	public bool AllowBuyExit { get => _allowBuyExit.Value; set => _allowBuyExit.Value = value; }
	public bool AllowSellExit { get => _allowSellExit.Value; set => _allowSellExit.Value = value; }
	public bool UseTimeFilter { get => _useTimeFilter.Value; set => _useTimeFilter.Value = value; }
	public int StartHour { get => _startHour.Value; set => _startHour.Value = value; }
	public int StartMinute { get => _startMinute.Value; set => _startMinute.Value = value; }
	public int EndHour { get => _endHour.Value; set => _endHour.Value = value; }
	public int EndMinute { get => _endMinute.Value; set => _endMinute.Value = value; }
	public decimal Npips { get => _nPips.Value; set => _nPips.Value = value; }
	public int SignalBar { get => _signalBar.Value; set => _signalBar.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public StopreversalAppliedPrices AppliedPrice { get => _appliedPrice.Value; set => _appliedPrice.Value = value; }

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_previousAppliedPrice = null;
		_previousStopLevel = null;
		_signalQueue.Clear();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_previousAppliedPrice = null;
		_previousStopLevel = null;
		_signalQueue.Clear();

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

		// no protection needed
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var price = GetAppliedPrice(candle);

		if (_previousAppliedPrice is null || _previousStopLevel is null)
		{
			_previousAppliedPrice = price;
			_previousStopLevel = price;
			EnqueueSignal(new SignalInfo(false, false, false, false), candle.CloseTime);
			return;
		}

		var prevPrice = _previousAppliedPrice.Value;
		var prevStop = _previousStopLevel.Value;

		var trailingStop = CalculateTrailingStop(price, prevPrice, prevStop);

		var buySignal = price > trailingStop && prevPrice < prevStop;
		var sellSignal = price < trailingStop && prevPrice > prevStop;

		_previousStopLevel = trailingStop;
		_previousAppliedPrice = price;

		var action = new SignalInfo(
			buySignal && AllowBuyEntry,
			sellSignal && AllowSellEntry,
			sellSignal && AllowBuyExit,
			buySignal && AllowSellExit
		);

		EnqueueSignal(action, candle.CloseTime);
	}

	private void EnqueueSignal(SignalInfo signal, DateTime currentTime)
	{
		_signalQueue.Add(signal);

		while (_signalQueue.Count > SignalBar)
		{
			var action = _signalQueue[0];
			try { _signalQueue.RemoveAt(0); } catch { }
			HandleSignal(action, currentTime);
		}
	}

	private void HandleSignal(SignalInfo signal, DateTime currentTime)
	{
		var inWindow = !UseTimeFilter || IsWithinTradingWindow(currentTime);

		if (UseTimeFilter && !inWindow && Position != 0)
		{
			if (Position > 0)
				SellMarket();
			else
				BuyMarket();
		}

		if (signal.CloseLong && Position > 0)
			SellMarket();

		if (signal.CloseShort && Position < 0)
			BuyMarket();

		if (!UseTimeFilter || inWindow)
		{
			if (signal.OpenLong && Position <= 0)
				BuyMarket();

			if (signal.OpenShort && Position >= 0)
				SellMarket();
		}
	}

	private decimal CalculateTrailingStop(decimal price, decimal prevPrice, decimal prevStop)
	{
		var shift = Npips;

		if (price == prevStop)
			return prevStop;

		if (prevPrice < prevStop && price < prevStop)
			return Math.Min(prevStop, price * (1 + shift));

		if (prevPrice > prevStop && price > prevStop)
			return Math.Max(prevStop, price * (1 - shift));

		return price > prevStop
			? price * (1 - shift)
			: price * (1 + shift);
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		return AppliedPrice switch
		{
			StopreversalAppliedPrices.Close => candle.ClosePrice,
			StopreversalAppliedPrices.Open => candle.OpenPrice,
			StopreversalAppliedPrices.High => candle.HighPrice,
			StopreversalAppliedPrices.Low => candle.LowPrice,
			StopreversalAppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			StopreversalAppliedPrices.Typical => (candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 3m,
			StopreversalAppliedPrices.Weighted => (2m * candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
			StopreversalAppliedPrices.Simple => (candle.OpenPrice + candle.ClosePrice) / 2m,
			StopreversalAppliedPrices.Quarter => (candle.OpenPrice + candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
			StopreversalAppliedPrices.TrendFollow0 => candle.ClosePrice > candle.OpenPrice
				? candle.HighPrice
				: candle.ClosePrice < candle.OpenPrice
					? candle.LowPrice
					: candle.ClosePrice,
			StopreversalAppliedPrices.TrendFollow1 => candle.ClosePrice > candle.OpenPrice
				? (candle.HighPrice + candle.ClosePrice) / 2m
				: candle.ClosePrice < candle.OpenPrice
					? (candle.LowPrice + candle.ClosePrice) / 2m
					: candle.ClosePrice,
			StopreversalAppliedPrices.Demark => CalculateDemarkPrice(candle),
			_ => candle.ClosePrice
		};
	}

	private static decimal CalculateDemarkPrice(ICandleMessage candle)
	{
		var result = candle.HighPrice + candle.LowPrice + candle.ClosePrice;

		if (candle.ClosePrice < candle.OpenPrice)
			result = (result + candle.LowPrice) / 2m;
		else if (candle.ClosePrice > candle.OpenPrice)
			result = (result + candle.HighPrice) / 2m;
		else
			result = (result + candle.ClosePrice) / 2m;

		return ((result - candle.LowPrice) + (result - candle.HighPrice)) / 2m;
	}

	private bool IsWithinTradingWindow(DateTime time)
	{
		var start = new TimeSpan(StartHour, StartMinute, 0);
		var end = new TimeSpan(EndHour, EndMinute, 0);
		var current = time.TimeOfDay;

		if (start == end)
			return false;

		if (start < end)
			return current >= start && current < end;

		return current >= start || current < end;
	}

	private readonly struct SignalInfo
	{
		public SignalInfo(bool openLong, bool openShort, bool closeLong, bool closeShort)
		{
			OpenLong = openLong;
			OpenShort = openShort;
			CloseLong = closeLong;
			CloseShort = closeShort;
		}

		public bool OpenLong { get; }
		public bool OpenShort { get; }
		public bool CloseLong { get; }
		public bool CloseShort { get; }
	}
}