Auf GitHub ansehen

Tuyul Gap Ende der Woche

Überblick

Tuyul Gap End Of Week portiert den MetaTrader 5 Expertenberater TuyulGAP nach StockSharp. Die Strategie bereitet sich auf die wöchentliche Markteröffnung vor, indem sie am Freitagabend eine konfigurierbare Anzahl aktueller Kerzen scannt und ein Paar Breakout-Stop-Orders um das höchste Hoch und das niedrigste Tief platziert. Es ist nur eine Handelssitzung pro Woche zulässig; Sobald die Aufträge bereitgestellt wurden, wartet die Strategie darauf, dass der Preis eines der beiden Niveaus durchbricht. Jede offene Position, die ein sicheres Gewinnziel in der Kontowährung erreicht, wird sofort geschlossen und alle verbleibenden ausstehenden Aufträge werden am Montag storniert, um den Workflow für die nächste Woche zurückzusetzen.

Strategielogik

  • Wöchentlicher Sitzungstrigger – das Setup wird an einem konfigurierbaren Wochentag (standardmäßig Freitag) ausgeführt, wenn die Börsenuhr die konfigurierte Stunde erreicht. Während des Minutenfensters (standardmäßig 23:00–23:15 Uhr) bereitet die Strategie die Breakout-Levels einmal pro Sitzung vor.
  • Dynamische Ausbruchsniveaus – das höchste Hoch und das niedrigste Tief der vorherigen Lookback Bars fertigen Kerzen definieren die Auslösepreise. Der Kaufstopp wird einen Tick über dem Hoch platziert, der Verkaufsstopp einen Tick unter dem Tief und ahmt den MetaTrader-Punktversatz nach.
  • Pending-Order-Hygiene – wenn für die Woche bereits eine Stop-Order vorliegt, wird diese nicht neu erstellt. Die entgegengesetzte ausstehende Order bleibt aktiv, nachdem eine Seite ausgelöst wurde, sodass die Strategie in beide Richtungen der Lücke handeln kann.
  • Sicherer Gewinnausstieg – offene Positionen werden bei jeder abgeschlossenen Kerze überwacht. Wenn der nicht realisierte Gewinn für eine Position das sichere Gewinnziel (in der Portfoliowährung) erreicht, wird er unabhängig von der Richtung auf den Markt abgeflacht.
  • Wöchentliches Zurücksetzen – bei der ersten Kerze am Montag storniert die Strategie alle noch aktiven ausstehenden Aufträge und aktiviert die Sitzungsflagge wieder, sodass die Einrichtung am nächsten Freitag durchgeführt werden kann.

Parameter

  • Volumen – Ordervolumen für die Breakout-Stop-Orders.
  • Stop-Loss (Punkte) – Abstand vom Einstiegspreis, ausgedrückt in Instrumentenpunkten, der zum Platzieren eines Schutzstopps nach der Eröffnung einer Position verwendet wird. Auf 0 setzen, um den Stopp zu deaktivieren.
  • Lookback-Balken – Anzahl der überprüften fertigen Kerzen, um die wöchentlichen Höchst- und Tiefststände zu berechnen.
  • Setup Day Of Week – Tagesindex (0=Sonntag … 6=Samstag), der die wöchentliche Einrichtung auslöst. Der Standardwert von 5 behält das ursprüngliche Freitagsverhalten bei.
  • Setup-Stunde – Börsenstunde, die als Anker für die Bereitstellung der Breakout-Orders dient.
  • Setup-Minutenfenster – Anzahl der Minuten nach Setup Hour, in denen das Setup gültig bleibt. Mit dem Standardwert 15 läuft die Strategie zwischen 23:00 und 23:15 Uhr.
  • Sicheres Gewinnziel – minimaler nicht realisierter Gewinn pro Position (in Portfoliowährung), der einen sofortigen Marktausstieg auslöst.
  • Kerzentyp – Zeitrahmen, der für den High/Low-Scan und die Überwachungsschleife verwendet wird.

Zusätzliche Hinweise

  • Die Stop-Loss-Order wird erst übermittelt, nachdem eine Position eröffnet wurde, da StockSharp das direkte Anhängen eines Schutzstopps an eine ausstehende Stop-Order nicht unterstützt.
  • Volumen, Preis und Stop-Level werden mithilfe der Schritt- und Präzisionsinformationen des Wertpapiers normalisiert, die StockSharp bereitstellt.
  • Für diese Strategie gibt es keine Python-Übersetzung. In diesem Paket ist nur die C#-Implementierung enthalten.
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>
/// Port of the MetaTrader 5 expert advisor TuyulGAP.
/// Places weekly breakout stop orders around the recent high/low range and closes positions once secure profit is reached.
/// </summary>
public class TuyulGapEndOfWeekStrategy : Strategy
{
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _lookbackBars;
	private readonly StrategyParam<int> _setupDayOfWeek;
	private readonly StrategyParam<int> _setupHour;
	private readonly StrategyParam<int> _setupMinuteWindow;
	private readonly StrategyParam<decimal> _secureProfitTarget;
	private readonly StrategyParam<DataType> _candleType;

	private Highest _highestHigh;
	private Lowest _lowestLow;

	private decimal _tickSize;
	private decimal _entryPrice;
	private decimal? _virtualStopPrice;
	private decimal _prevHighest;
	private decimal _prevLowest;

	/// <summary>
	/// Initializes a new instance of the <see cref="TuyulGapEndOfWeekStrategy"/> class.
	/// </summary>
	public TuyulGapEndOfWeekStrategy()
	{

		_stopLossPoints = Param(nameof(StopLossPoints), 60)
		.SetRange(0, 5000)
		.SetDisplay("Stop Loss (points)", "Distance from entry used for protective stops", "Risk");

		_lookbackBars = Param(nameof(LookbackBars), 12)
		.SetRange(2, 500)
		.SetDisplay("Lookback Bars", "Number of finished candles inspected for highs/lows", "Setup");

		_setupDayOfWeek = Param(nameof(SetupDayOfWeek), 5)
		.SetRange(0, 6)
		.SetDisplay("Setup Day Of Week", "Day index (0=Sunday) that stages the weekly orders", "Setup");

		_setupHour = Param(nameof(SetupHour), 23)
		.SetRange(0, 23)
		.SetDisplay("Setup Hour", "Exchange hour when the weekly setup is evaluated", "Setup");

		_setupMinuteWindow = Param(nameof(SetupMinuteWindow), 15)
		.SetRange(0, 59)
		.SetDisplay("Setup Minute Window", "Minutes after the setup hour when staging is allowed", "Setup");

		_secureProfitTarget = Param(nameof(SecureProfitTarget), 5m)
		.SetRange(0m, 100000m)
		.SetDisplay("Secure Profit Target", "Unrealized profit per position that triggers an immediate exit", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe used for the high/low scan and monitoring", "Data");
	}


	/// <summary>
	/// Distance from entry used for protective stops, measured in instrument points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Number of finished candles inspected for highs and lows.
	/// </summary>
	public int LookbackBars
	{
		get => _lookbackBars.Value;
		set => _lookbackBars.Value = value;
	}

	/// <summary>
	/// Day index (0=Sunday … 6=Saturday) that stages the weekly setup.
	/// </summary>
	public int SetupDayOfWeek
	{
		get => _setupDayOfWeek.Value;
		set => _setupDayOfWeek.Value = value;
	}

	/// <summary>
	/// Exchange hour when the weekly setup is evaluated.
	/// </summary>
	public int SetupHour
	{
		get => _setupHour.Value;
		set => _setupHour.Value = value;
	}

	/// <summary>
	/// Minutes after the setup hour when staging is allowed.
	/// </summary>
	public int SetupMinuteWindow
	{
		get => _setupMinuteWindow.Value;
		set => _setupMinuteWindow.Value = value;
	}

	/// <summary>
	/// Unrealized profit per position that triggers an immediate exit.
	/// </summary>
	public decimal SecureProfitTarget
	{
		get => _secureProfitTarget.Value;
		set => _secureProfitTarget.Value = value;
	}

	/// <summary>
	/// Timeframe used for the high/low scan and monitoring.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_highestHigh = null;
		_lowestLow = null;
		_tickSize = 0m;
		_entryPrice = 0m;
		_virtualStopPrice = null;
		_prevHighest = 0m;
		_prevLowest = 0m;
	}

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

		_tickSize = Security?.PriceStep ?? 0m;

		_highestHigh = new Highest
		{
			Length = Math.Max(2, LookbackBars)
		};

		_lowestLow = new Lowest
		{
			Length = Math.Max(2, LookbackBars)
		};

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

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

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

		if (!_highestHigh.IsFormed || !_lowestLow.IsFormed)
			return;

		// Check virtual stop
		if (Position > 0m && _virtualStopPrice.HasValue && candle.LowPrice <= _virtualStopPrice.Value)
		{
			SellMarket(Math.Abs(Position));
			_virtualStopPrice = null;
			_entryPrice = 0m;
			return;
		}
		if (Position < 0m && _virtualStopPrice.HasValue && candle.HighPrice >= _virtualStopPrice.Value)
		{
			BuyMarket(Math.Abs(Position));
			_virtualStopPrice = null;
			_entryPrice = 0m;
			return;
		}

		// Close on profit
		if (Position != 0m && SecureProfitTarget > 0m && PnL >= SecureProfitTarget)
		{
			if (Position > 0m)
				SellMarket(Math.Abs(Position));
			else
				BuyMarket(Math.Abs(Position));
			_virtualStopPrice = null;
			_entryPrice = 0m;
			return;
		}

		if (Position == 0m && _prevHighest > 0m && _prevLowest > 0m)
		{
			// Breakout above previous highest
			if (candle.ClosePrice > _prevHighest)
			{
				BuyMarket(Volume);
				_entryPrice = candle.ClosePrice;
				var stopDist = GetStopLossDistance();
				if (stopDist > 0m)
					_virtualStopPrice = _entryPrice - stopDist;
			}
			// Breakout below previous lowest
			else if (candle.ClosePrice < _prevLowest)
			{
				SellMarket(Volume);
				_entryPrice = candle.ClosePrice;
				var stopDist = GetStopLossDistance();
				if (stopDist > 0m)
					_virtualStopPrice = _entryPrice + stopDist;
			}
		}

		_prevHighest = highestValue;
		_prevLowest = lowestValue;
	}

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

	private decimal GetStopLossDistance()
	{
		var tick = _tickSize > 0m ? _tickSize : Security?.PriceStep ?? 0m;
		if (tick <= 0m)
		return 0m;

		return StopLossPoints > 0 ? StopLossPoints * tick : 0m;
	}

	private decimal NormalizePrice(decimal price)
	{
		var tick = _tickSize > 0m ? _tickSize : Security?.PriceStep ?? 0m;
		if (tick <= 0m)
		return price;

		return Math.Round(price / tick, MidpointRounding.AwayFromZero) * tick;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var security = Security;
		if (security == null)
		return volume;

		if (security.VolumeStep is { } volumeStep && volumeStep > 0m)
		volume = Math.Round(volume / volumeStep, MidpointRounding.AwayFromZero) * volumeStep;

		if (security.MinVolume is { } minVolume && minVolume > 0m && volume < minVolume)
		volume = minVolume;

		if (security.MaxVolume is { } maxVolume && maxVolume > 0m && volume > maxVolume)
		volume = maxVolume;

		return volume;
	}

	private DayOfWeek GetSetupDay()
	{
		var day = SetupDayOfWeek % 7;
		if (day < 0)
		day += 7;

		return (DayOfWeek)day;
	}
}