Открыть на GitHub

Стратегия Stopreversal Trailing

Stopreversal Trailing воспроизводит эксперт MT5 Exp_Stopreversal.mq5. Используется пользовательский индикатор Stopreversal, который строит динамическую линию трейлинг-стопа вокруг выбранной цены свечи. Когда цена пробивает линию сверху, стратегия рассматривает это как бычий разворот, при необходимости закрывает короткую позицию и открывает новую длинную. Пробой сверху вниз формирует зеркальное медвежье действие. Обработка сигнала может быть отложена на заданное количество закрытых баров, чтобы полностью повторить поведение оригинального советника.

Детали

  • Логика входа: реакция на стрелки индикатора Stopreversal, появляющиеся при пересечении ценой адаптивного трейлинг-стопа.
  • Длинные/короткие позиции: поддерживаются оба направления с независимыми флажками включения.
  • Логика выхода: противоположные сигналы Stopreversal могут закрывать текущие позиции; доступны защитные стоп-лосс и тейк-профит.
  • Стопы: фиксированные стоп-лосс и тейк-профит в шагах цены плюс разворот по индикатору.
  • Источник данных: любая таймфреймная серия; по умолчанию используются свечи H4 как в исходном советнике.
  • Задержка сигнала: параметр SignalBar откладывает исполнение ордера на указанное число закрытых баров (по умолчанию 1 бар).
  • Управление риском: при запуске активируется сервис защиты позиций, доступны жёсткие стопы в шагах цены инструмента.
  • Параметры индикатора: Npips задаёт расстояние между ценой и стопом, PriceMode определяет тип цены для расчётов.
  • Значения по умолчанию:
    • Volume = 1
    • StopLossSteps = 1000
    • TakeProfitSteps = 2000
    • BuyPositionOpen = true
    • SellPositionOpen = true
    • BuyPositionClose = true
    • SellPositionClose = true
    • Npips = 0.004
    • PriceMode = Close
    • SignalBar = 1

Параметры

Параметр Описание
CandleType Тип свечей, используемый для расчёта Stopreversal и торговли. По умолчанию таймфрейм 4 часа.
Volume Базовый объём ордера при открытии новой позиции.
StopLossSteps Расстояние до стоп-лосса в шагах цены; 0 — отключить.
TakeProfitSteps Расстояние до тейк-профита в шагах цены; 0 — отключить.
BuyPositionOpen Включает открытие длинных позиций при бычьем сигнале.
SellPositionOpen Включает открытие коротких позиций при медвежьем сигнале.
BuyPositionClose Закрывает активные длинные позиции при появлении медвежьего сигнала.
SellPositionClose Закрывает активные короткие позиции при появлении бычьего сигнала.
Npips Доля, на которую расширяется или сужается расстояние до трейлинг-стопа.
PriceMode Вариант используемой цены (close, open, high, low, median, typical, weighted, simple, quarter, trend-follow или Demark).
SignalBar Количество полностью закрытых свечей до реакции на сигнал — аналог параметра MT5.

Фильтры

  • Категория: разворот по тренду
  • Направление: двунаправленная торговля
  • Индикаторы: Stopreversal (трейлинг-стоп на базе ATR)
  • Стопы: фиксированные стоп-лосс и тейк-профит, опционально
  • Таймфрейм: настраиваемый (по умолчанию H4)
  • Сезонность: нет
  • Нейросети: нет
  • Дивергенция: нет
  • Сложность: средняя из-за пользовательской логики трейлинга
  • Уровень риска: регулируется расстоянием стопов и параметром трейлинга
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 indicator based trailing stop strategy.
/// </summary>
public class StopreversalTrailingStrategy : Strategy
{
	private readonly StrategyParam<int> _atrPeriod;

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _stopLossSteps;
	private readonly StrategyParam<int> _takeProfitSteps;
	private readonly StrategyParam<bool> _buyPositionOpen;
	private readonly StrategyParam<bool> _sellPositionOpen;
	private readonly StrategyParam<bool> _buyPositionClose;
	private readonly StrategyParam<bool> _sellPositionClose;
	private readonly StrategyParam<decimal> _npips;
	private readonly StrategyParam<AppliedPriceModes> _priceMode;
	private readonly StrategyParam<int> _signalBar;

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

	private AverageTrueRange _atr = null!;
	private decimal? _previousStopLevel;
	private decimal? _previousPrice;

	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Initializes a new instance of <see cref="StopreversalTrailingStrategy"/>.
	/// </summary>
	public StopreversalTrailingStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Stopreversal timeframe", "General");

		_atrPeriod = Param(nameof(AtrPeriod), 15)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR lookback for trailing stop", "Indicator");

		_stopLossSteps = Param(nameof(StopLossSteps), 10)
		.SetNotNegative()
		.SetDisplay("Stop Loss Steps", "Stop loss distance in price steps", "Risk")
		;

		_takeProfitSteps = Param(nameof(TakeProfitSteps), 20)
		.SetNotNegative()
		.SetDisplay("Take Profit Steps", "Take profit distance in price steps", "Risk")
		;

		_buyPositionOpen = Param(nameof(BuyPositionOpen), true)
		.SetDisplay("Open Long", "Allow opening long positions", "Trading");

		_sellPositionOpen = Param(nameof(SellPositionOpen), true)
		.SetDisplay("Open Short", "Allow opening short positions", "Trading");

		_buyPositionClose = Param(nameof(BuyPositionClose), true)
		.SetDisplay("Close Long", "Close long positions on sell signals", "Trading");

		_sellPositionClose = Param(nameof(SellPositionClose), true)
		.SetDisplay("Close Short", "Close short positions on buy signals", "Trading");

		_npips = Param(nameof(Npips), 0.004m)
		.SetGreaterThanZero()
		.SetDisplay("Trailing Offset", "Fractional offset applied to the stop line", "Indicator")
		;

		_priceMode = Param(nameof(PriceMode), AppliedPriceModes.Close)
		.SetDisplay("Applied Price", "Price source used by the trailing stop", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
		.SetNotNegative()
		.SetDisplay("Signal Bar", "Bar delay before acting on a signal", "Indicator")
		;
	}

	/// <summary>
	/// Candle subscription type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// ATR period used for the trailing stop calculation.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Stop loss distance measured in price steps.
	/// </summary>
	public int StopLossSteps
	{
		get => _stopLossSteps.Value;
		set => _stopLossSteps.Value = value;
	}

	/// <summary>
	/// Take profit distance measured in price steps.
	/// </summary>
	public int TakeProfitSteps
	{
		get => _takeProfitSteps.Value;
		set => _takeProfitSteps.Value = value;
	}

	/// <summary>
	/// Enable long entries.
	/// </summary>
	public bool BuyPositionOpen
	{
		get => _buyPositionOpen.Value;
		set => _buyPositionOpen.Value = value;
	}

	/// <summary>
	/// Enable short entries.
	/// </summary>
	public bool SellPositionOpen
	{
		get => _sellPositionOpen.Value;
		set => _sellPositionOpen.Value = value;
	}

	/// <summary>
	/// Close long positions on short signals.
	/// </summary>
	public bool BuyPositionClose
	{
		get => _buyPositionClose.Value;
		set => _buyPositionClose.Value = value;
	}

	/// <summary>
	/// Close short positions on long signals.
	/// </summary>
	public bool SellPositionClose
	{
		get => _sellPositionClose.Value;
		set => _sellPositionClose.Value = value;
	}

	/// <summary>
	/// Fractional offset used by the trailing stop calculation.
	/// </summary>
	public decimal Npips
	{
		get => _npips.Value;
		set => _npips.Value = value;
	}

	/// <summary>
	/// Price source used when computing the trailing level.
	/// </summary>
	public AppliedPriceModes PriceMode
	{
		get => _priceMode.Value;
		set => _priceMode.Value = value;
	}

	/// <summary>
	/// Number of bars to delay before reacting to a signal.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_signals.Clear();
		_previousStopLevel = null;
		_previousPrice = null;
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
	}

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

		_atr = new AverageTrueRange
		{
			Length = AtrPeriod
		};

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _atr);
			DrawOwnTrades(area);
		}

		// no protection
	}

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

		UpdateStops(candle);

		var price = GetAppliedPrice(candle);
		var prevStop = _previousStopLevel ?? price * (1m - Npips);
		var prevPrice = _previousPrice ?? price;
		var hasPrev = _previousStopLevel.HasValue && _previousPrice.HasValue;

		var stop = CalculateStop(price, prevPrice, prevStop);

		var buySignal = hasPrev && price > stop && prevPrice < prevStop && prevStop != 0m;
		var sellSignal = hasPrev && price < stop && prevPrice > prevStop && prevStop != 0m;

		_previousPrice = price;
		_previousStopLevel = stop;

		_signals.Add(new SignalInfo
		{
			BuySignal = buySignal,
			SellSignal = sellSignal,
			ClosePrice = candle.ClosePrice,
			Time = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime
		});

		TrimSignals();

		if (_signals.Count <= SignalBar)
		return;

		var index = _signals.Count - 1 - SignalBar;
		if (index < 0)
		return;

		var signal = _signals[index];
		var allowTrading = _atr.IsFormed;

		ExecuteSignal(signal, allowTrading);
	}

	private void ExecuteSignal(SignalInfo signal, bool allowTrading)
	{
		if (SellPositionClose && signal.BuySignal && Position < 0)
		{
			BuyMarket();
			ResetShortStops();
		}

		if (BuyPositionClose && signal.SellSignal && Position > 0)
		{
			SellMarket();
			ResetLongStops();
		}

		if (!allowTrading || Position != 0)
		return;

		if (BuyPositionOpen && signal.BuySignal)
		{
			if (Volume > 0)
			{
				BuyMarket();
				ResetShortStops();
				SetLongStops(signal.ClosePrice);
			}
		}
		else if (SellPositionOpen && signal.SellSignal)
		{
			if (Volume > 0)
			{
				SellMarket();
				ResetLongStops();
				SetShortStops(signal.ClosePrice);
			}
		}
	}

	private void UpdateStops(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop is decimal longStop && candle.LowPrice <= longStop)
			{
				SellMarket();
				ResetLongStops();
				return;
			}

			if (_longTake is decimal longTake && candle.HighPrice >= longTake)
			{
				SellMarket();
				ResetLongStops();
			}
		}
		else if (Position < 0)
		{
			if (_shortStop is decimal shortStop && candle.HighPrice >= shortStop)
			{
				BuyMarket();
				ResetShortStops();
				return;
			}

			if (_shortTake is decimal shortTake && candle.LowPrice <= shortTake)
			{
				BuyMarket();
				ResetShortStops();
			}
		}
	}

	private void TrimSignals()
	{
		var max = Math.Max(SignalBar + 5, 10);
		while (_signals.Count > max)
		{
			try { _signals.RemoveAt(0); } catch { break; }
		}
	}

	private decimal CalculateStop(decimal price, decimal prevPrice, decimal prevStop)
	{
		var offset = Npips;

		if (price == prevStop)
		return prevStop;

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

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

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

	private void SetLongStops(decimal basePrice)
	{
		var step = GetEffectiveStep();

		_longStop = StopLossSteps > 0 ? basePrice - step * StopLossSteps : null;
		_longTake = TakeProfitSteps > 0 ? basePrice + step * TakeProfitSteps : null;
	}

	private void SetShortStops(decimal basePrice)
	{
		var step = GetEffectiveStep();

		_shortStop = StopLossSteps > 0 ? basePrice + step * StopLossSteps : null;
		_shortTake = TakeProfitSteps > 0 ? basePrice - step * TakeProfitSteps : null;
	}

	private void ResetLongStops()
	{
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortStops()
	{
		_shortStop = null;
		_shortTake = null;
	}

	private decimal GetEffectiveStep()
	{
		var step = Security?.PriceStep;
		if (step is decimal s && s > 0)
		return s;

		return 0.0001m;
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		var open = candle.OpenPrice;
		var close = candle.ClosePrice;
		var high = candle.HighPrice;
		var low = candle.LowPrice;

		return PriceMode switch
		{
			AppliedPriceModes.Close => close,
			AppliedPriceModes.Open => open,
			AppliedPriceModes.High => high,
			AppliedPriceModes.Low => low,
			AppliedPriceModes.Median => (high + low) / 2m,
			AppliedPriceModes.Typical => (close + high + low) / 3m,
			AppliedPriceModes.Weighted => (2m * close + high + low) / 4m,
			AppliedPriceModes.Simple => (open + close) / 2m,
			AppliedPriceModes.Quarter => (open + close + high + low) / 4m,
			AppliedPriceModes.TrendFollow0 => close > open ? high : close < open ? low : close,
			AppliedPriceModes.TrendFollow1 => close > open ? (high + close) / 2m : close < open ? (low + close) / 2m : close,
			AppliedPriceModes.Demark => CalculateDemarkPrice(open, high, low, close),
			_ => close
		};
	}

	private static decimal CalculateDemarkPrice(decimal open, decimal high, decimal low, decimal close)
	{
		var result = high + low + close;

		if (close < open)
		result = (result + low) / 2m;
		else if (close > open)
		result = (result + high) / 2m;
		else
		result = (result + close) / 2m;

		return ((result - low) + (result - high)) / 2m;
	}

	private sealed class SignalInfo
	{
		public bool BuySignal { get; init; }
		public bool SellSignal { get; init; }
		public decimal ClosePrice { get; init; }
		public DateTimeOffset Time { get; init; }
	}

	/// <summary>
	/// Available price calculation modes.
	/// </summary>
	public enum AppliedPriceModes
	{
		/// <summary>
		/// Closing price.
		/// </summary>
		Close,

		/// <summary>
		/// Opening price.
		/// </summary>
		Open,

		/// <summary>
		/// Highest price.
		/// </summary>
		High,

		/// <summary>
		/// Lowest price.
		/// </summary>
		Low,

		/// <summary>
		/// Median price (high + low) / 2.
		/// </summary>
		Median,

		/// <summary>
		/// Typical price (close + high + low) / 3.
		/// </summary>
		Typical,

		/// <summary>
		/// Weighted close price (2 * close + high + low) / 4.
		/// </summary>
		Weighted,

		/// <summary>
		/// Simple average of open and close.
		/// </summary>
		Simple,

		/// <summary>
		/// Average of open, close, high and low.
		/// </summary>
		Quarter,

		/// <summary>
		/// Trend follow price variant 0.
		/// </summary>
		TrendFollow0,

		/// <summary>
		/// Trend follow price variant 1.
		/// </summary>
		TrendFollow1,

		/// <summary>
		/// Demark price formula.
		/// </summary>
		Demark
	}
}