Открыть на GitHub

Стратегия Stopreversal Tm

Общее описание

Stopreversal Tm — это прямая конвертация эксперта MetaTrader 5 Exp_Stopreversal_Tm.mq5. В основе лежит индикатор Stopreversal: он рассчитывает плавающий трейлинг-стоп от выбранного типа цены и формирует сигналы разворота при пересечении уровня ценой. Стратегия работает с одним инструментом и одним потоком свечей, сохраняя поддержу пользовательского торгового сеанса из оригинальной версии.

Формирование сигналов

Индикатор Stopreversal получает опорную цену из выбранного режима (close, open, медианная цена, trend-following, Demark и т.д.), после чего смещает трейлинг на величину Sensitivity (nPips). Если новая цена оказывается выше трейлинга, а предыдущая свеча была ниже, возникает сигнал на покупку. Если цена опускается ниже трейлинга, а предыдущая свеча была выше, появляется сигнал на продажу. Бычий сигнал одновременно инициирует закрытие шортов и открытие лонга, медвежий — закрытие лонгов и открытие шорта.

Чтобы воспроизвести поведение советника, ввод SignalBar преобразован в параметр Signal Bar Delay: он позволяет дождаться заданного числа завершённых свечей перед исполнением сигнала и тем самым исключает работу по ещё формирующемуся бару.

Торговая сессия и управление позицией

Как и в оригинале, можно ограничить торговлю временным окном. Параметр Use Time Filter включает или отключает проверку, а Start Hour/Minute и End Hour/Minute задают начало и конец сеанса. При выходе из допустимого интервала открытая позиция немедленно закрывается. Даже при отключённом фильтре сигналы на закрытие продолжают обрабатываться.

Стратегия работает в неттинговой логике: при смене направления сначала закрывается текущая позиция, затем выполняется противоположная сделка. Это исключает одновременное удержание разнонаправленных позиций.

Параметры

  • Allow Buy Entries / Allow Sell Entries – разрешить или запретить открытие лонгов/шортов по соответствующим сигналам.
  • Allow Long Exits / Allow Short Exits – определить, можно ли закрывать позицию по встречному сигналу.
  • Use Time Filter – включить фильтр торгового времени.
  • Start Hour / Start Minute / End Hour / End Minute – параметры временного окна (начало включительно, конец исключительно), поддерживается переход через полночь.
  • Sensitivity (nPips) – коэффициент, определяющий расстояние трейлинга от цены (например, 0.004 соответствует 0.4%).
  • Signal Bar Delay (SignalBar) – количество завершённых баров до исполнения сигнала (0 – сразу, 1 – на предыдущей свече, как в исходнике).
  • Candle Type – тип (таймфрейм) свечей для расчёта.
  • Applied Price – выбор ценового ряда для индикатора (close, open, median, trend-following, Demark и др.).

Особенности реализации

  • Логика вычисления индикатора перенесена внутрь стратегии и полностью повторяет MQL5-формулу nPips без использования внешних буферов.
  • Последовательность операций (сначала закрытие, затем открытие) и работа с торговыми сессиями сохранены из оригинала.
  • Применяется высокоуровневый API StockSharp: подписка на свечи, очередь задержанных сигналов и рыночные приказы BuyMarket / SellMarket. Функции управления капиталом из MetaTrader не перенесены, так как в StockSharp объём позиций задаётся напрямую.
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; }
	}
}