Auf GitHub ansehen

Stopreversal Tm-Strategie

Übersicht

Die Stopreversal Tm-Strategie ist eine direkte Übersetzung des ursprünglichen MetaTrader 5 Expert Advisors Exp_Stopreversal_Tm.mq5. Die Handelsidee folgt dem benutzerdefinierten Indikator Stopreversal, der einen dynamischen Trailing Stop um den Preis herum aufrechthält und Umkehralarme erzeugt, wenn der Preis diese Trailing-Grenze kreuzt. Die Strategie operiert auf einem einzelnen Instrument und einem einzelnen Kerzen-Feed und ist für den Trendumkehr-Handel mit einem benutzerdefinierten Sitzungsfilter ausgelegt.

Signalgenerierung

Der Stopreversal-Indikator berechnet einen Referenzpreis aus dem gewählten angewendeten Preismodus und passt dann ein Trailing-Stop-Level um Sensitivity (den nPips-Parameter) an. Wenn der neue angewendete Preis über den Trailing Stop steigt, während sich der vorherige Balken darunter befand, wird ein bullisches Signal erzeugt. Umgekehrt erscheint ein bärisches Signal, wenn der neue Preis unter den Trailing Stop fällt, nachdem er darüber war. Jedes bullische Signal fordert gleichzeitig das Schließen bestehender Short-Positionen und das Öffnen eines neuen Longs, während jedes bärische Signal Longs schließt und Shorts öffnet.

Um das Verhalten der ursprünglichen MetaTrader-Implementierung zu reproduzieren, kann die Strategie die Ausführung von Signalen um mehrere abgeschlossene Balken verzögern (Signal Bar Delay). Dies repliziert den SignalBar-Eingang des Expert Advisors und verhindert den Handel auf der noch entstehenden Kerze.

Sitzungsfilter und Positionshandling

Der Expert Advisor erlaubte den Handel nur innerhalb eines bestimmten Zeitfensters. Die konvertierte Strategie behält die gleiche Logik: Wenn das Use Time Filter-Flag aktiviert ist, sind Aufträge nur innerhalb der durch Start Hour/Minute und End Hour/Minute konfigurierten Sitzung erlaubt. Wenn die aktuelle Zeit das erlaubte Fenster verlässt, wird jede offene Position sofort geschlossen. Signalgesteuerte Ausstiege bleiben aktiv, auch wenn die Sitzung deaktiviert ist.

Die Strategie arbeitet mit Netto-Positionen. Eine Schließaktion wird immer vor einem entgegengesetzten Einstieg ausgeführt, wodurch sichergestellt wird, dass die Richtung ohne überlappende Expositionen wechselt.

Parameter

  • Allow Buy Entries / Allow Sell Entries – Öffnung neuer Long- oder Short-Positionen aktivieren oder deaktivieren, wenn das entsprechende Signal empfangen wird.
  • Allow Long Exits / Allow Short Exits – steuert, ob entgegengesetzte Signale bestehende Positionen schließen dürfen.
  • Use Time Filter – schaltet das Handelssitzungs-Fenster ein.
  • Start Hour / Start Minute / End Hour / End Minute – definiert den inklusiven Start und das exklusive Ende des Handelsfensters. Der Zeitfilter unterstützt Übernacht-Sitzungen, bei denen die Endzeit früher als die Startzeit liegt.
  • Sensitivity (nPips) – relativer Abstand (ausgedrückt als Multiplikator, z.B. 0.004 = 0.4%), der verwendet wird, um den Trailing Stop näher oder weiter vom Preis zu bewegen.
  • Signal Bar Delay (SignalBar) – Anzahl abgeschlossener Kerzen, die gewartet werden soll, bevor auf ein Signal reagiert wird. 0 führt sofort bei der Schlusskerze aus, 1 reproduziert das Standardverhalten, auf dem vorherigen Balken zu handeln.
  • Candle Type – Zeitrahmen der Kerzen-Subskription für Indikatorberechnungen.
  • Applied Price – Wahl der Preisserie (Schluss, Eröffnung, Medianpreis, Trendfolgemodi, Demark-Preis usw.), die die Trailing-Stop-Berechnung speist.

Implementierungshinweise

  • Der Indikator ist direkt in der Strategie implementiert, ohne sich auf externe Buffer zu verlassen, sodass die nPips-Trailing-Stop-Logik dem ursprünglichen MQL5-Code entspricht.
  • Sitzungsverwaltung und Signalsequenzierung folgen dem ursprünglichen Experten, einschließlich der Priorität, bestehende Exposition zu schließen, bevor neue Trades eröffnet werden.
  • Die Konvertierung konzentriert sich auf die High-Level-StockSharp-API: Kerzen-Subskriptionen, verzögerte Signal-Warteschlange und Marktaufträge (BuyMarket / SellMarket). Money-Management-Funktionen, die an MetaTrader-Kontometriken gebunden waren, wurden weggelassen, da StockSharp-Strategien bereits mit expliziten Positionsgrößen arbeiten.
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; }
	}
}