Auf GitHub ansehen

Strategie Pending Orders Nach Zeit

Diese Strategie recreiert den klassischen MetaTrader Expert "Pending orders by time" für StockSharp. Sie läuft nach einem diskreten Zeitplan: Jeden Tag platziert sie symmetrische Stop-Orders rund um den Markt, wenn eine neue Sessionsstunde beginnt, und löscht alle Orders plus offenen Positionen zu einer angegebenen Schlussstunde. Die Implementierung behält die ursprünglichen pip-basierten Eingaben bei, konvertiert sie in native Kurseinheiten und verwendet die High-Level-API zur Risikoverwaltung.

Funktionsweise

  1. Zeitbasierter Trigger – Wenn eine Kerze empfangen wird, die zur konfigurierten Eröffnungsstunde endet, sendet die Strategie einen Buy Stop über dem Ask und einen Sell Stop unter dem Bid. Beide Orders werden um den Parameter Distance (pips) verschoben, der in Kurseinheiten umgerechnet wird.
  2. SchutzbefehleStartProtection fügt automatisch Stop-Loss- und Take-Profit-Schutz mit den in den Parametern definierten Pip-Abständen hinzu. ManageRisk fungiert auch als Absicherung und schließt jede Restposition, wenn eine abgeschlossene Kerze zeigt, dass die Schwellenwerte überschritten wurden.
  3. Sitzungsschluss – Wenn die Schlussstunde ankommt, storniert die Strategie alle verbleibenden Pending Orders und schließt offene Trades unabhängig von Gewinn oder Verlust zwangsweise. Dies reproduziert das Verhalten des ursprünglichen Experts, am Ende der Sitzung zurückzusetzen.
  4. Stellengerechte Pip-Größe – Der Pip-Multiplikator emuliert die MetaTrader-Implementierung, indem der Preisschritt für Symbole mit drei oder fünf Dezimalstellen (z.B. JPY oder 5-stellige FX-Paare) mit zehn multipliziert wird. Dies hält Legacy-Eingaben konsistent über Broker hinweg.

Der Standard-Kerzentyp sind 30-Minuten-Balken, um unter der ursprünglichen Beschränkung von Perioden kürzer als H1 zu bleiben. Jeder andere Zeitrahmen kann verwendet werden, solange die resultierenden Stunden-Zeitstempel den gewünschten Sessionsstunden entsprechen.

Parameter

Name Beschreibung Standard
Opening Hour Stunde (0-23), zu der die Strategie das Paar Stop-Orders platziert. 9
Closing Hour Stunde (0-23), zu der alle Orders storniert und Positionen geschlossen werden. 2
Distance (pips) Versatz in Pips zwischen dem aktuellen Kurs und den ausstehenden Stop-Einstiegen. 20
Stop Loss (pips) Pip-Abstand für den Schutz-Stop, sobald eine Position offen ist. 20
Take Profit (pips) Pip-Abstand für das Gewinnziel, sobald eine Position offen ist. 500
Order Volume Menge für jede ausstehende Stop-Order. 0.1
Candle Type Zeitrahmen, der den Stundenplan steuert. 30-Minuten-Zeitrahmen

Alle Parameter können optimiert werden. Pip-basierte Eingaben werden intern mit dem Preisschritt des Instruments konvertiert, sodass sie zwischen FX-Symbolen mit unterschiedlicher Dezimalpräzision portabel bleiben.

Täglicher Arbeitsablauf

  1. Bei jedem Kerzenschluss prüft die Strategie, ob der Stop-Loss- oder Take-Profit-Abstand erreicht wurde. Wenn ja, schließt sie die aktive Position zum Marktpreis.
  2. Wenn die Schlussstunde erreicht wird, storniert sie alle ungefüllten Pending Orders und schließt die Position, um sicherzustellen, dass das Buch vor der nächsten Sitzung flach ist.
  3. Wenn die Eröffnungsstunde erreicht wird (und die Strategie flach ist), storniert sie vorsichtshalber alte Orders und sendet einen neuen Sell Stop unter dem Bid und einen Buy Stop über dem Ask. Die Orders sind um den Spread gespiegelt, damit jeder Ausbruch erfasst werden kann.
  4. Während der gesamten Sitzung hält der von StartProtection erstellte Plattformschutz einen Stop-Loss und Take-Profit angehängt, der sofort reagiert, wenn die Intrabar-Kursaction die Schwellenwerte erreicht.

Verwendungshinweise

  • Verwenden Sie Instrumente, deren Tick-Größe einen einzelnen "Punkt" darstellt, damit die Pip-Anpassung den ursprünglichen Expert widerspiegelt. Exotische Tick-Größen können manuelle Abstimmung der Abstandsparameter erfordern.
  • Die Logik nimmt einen Handelszyklus pro Tag an. Wenn Sie Intraday-Daten mit mehreren Öffnungs-/Schließungs-Übereinstimmungen verwenden, passen Sie die Stunden entsprechend an.
  • Da alle Aktionen beim Kerzenschluss stattfinden, wählen Sie eine Kerzengröße, die Ihrer gewünschten Bewertungshäufigkeit entspricht. Zum Beispiel bieten Stundenkerzen die gleiche Kadenz wie die MetaTrader-Version.
  • Die Strategie platziert neue Pending Orders nur wenn die Position flach ist, um Überexponierung zu vermeiden, wenn ein Ausbruchstrade während der nächsten Eröffnungsstunde noch aktiv ist.

Unterschiede zur MQL-Version

  • Schutzausstiege werden über StartProtection plus explizite Prüfungen verwaltet und nutzen die High-Level-API von StockSharp anstatt direkter Stop-Loss-Zuweisung am Pending-Order-Ticket.
  • Bid/Ask-Preise werden aus Security.BestBid und Security.BestAsk gelesen. Wenn diese Kurse nicht verfügbar sind, wird der Kerzenschluss als Fallback-Referenz verwendet.
  • Marktorders werden verwendet, um Positionen zur Schlussstunde der Einfachheit halber zu liquidieren und brokerspezifische Verhaltensweisen zu vermeiden.
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>
/// Places simulated symmetric stop entries at scheduled hours and manages them with daily resets.
/// </summary>
public class PendingOrdersByTimeStrategy : Strategy
{
	private readonly StrategyParam<int> _openingHour;
	private readonly StrategyParam<int> _closingHour;
	private readonly StrategyParam<decimal> _distancePips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal? _pendingBuyPrice;
	private decimal? _pendingSellPrice;
	private decimal? _entryPrice;

	public int OpeningHour
	{
		get => _openingHour.Value;
		set => _openingHour.Value = value;
	}

	public int ClosingHour
	{
		get => _closingHour.Value;
		set => _closingHour.Value = value;
	}

	public decimal DistancePips
	{
		get => _distancePips.Value;
		set => _distancePips.Value = value;
	}

	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public PendingOrdersByTimeStrategy()
	{
		_openingHour = Param(nameof(OpeningHour), 2)
			.SetDisplay("Opening Hour", "Hour to activate pending orders", "Schedule")
			.SetRange(0, 23);

		_closingHour = Param(nameof(ClosingHour), 22)
			.SetDisplay("Closing Hour", "Hour to cancel orders and flat positions", "Schedule")
			.SetRange(0, 23);

		_distancePips = Param(nameof(DistancePips), 500m)
			.SetDisplay("Distance (pips)", "Offset for entry stop orders", "Orders")
			.SetGreaterThanZero();

		_stopLossPips = Param(nameof(StopLossPips), 500m)
			.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk")
			.SetGreaterThanZero();

		_takeProfitPips = Param(nameof(TakeProfitPips), 2000m)
			.SetDisplay("Take Profit (pips)", "Profit target distance", "Risk")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Working timeframe for the schedule", "General");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_pipSize = 0m;
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
		_entryPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_pipSize = CalculatePipSize();

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

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

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0.01m;

		if (step <= 0m)
			return 0.01m;

		return step;
	}

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

		var hour = candle.OpenTime.Hour;

		// Check pending stop entries
		CheckPendingEntries(candle);

		// Manage existing position
		ManageRisk(candle);

		if (hour == ClosingHour)
		{
			// Closing hour: cancel pending and exit any open trades.
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
			ExitPosition();
		}

		if (hour == OpeningHour && hour != ClosingHour && Position == 0m && !_pendingBuyPrice.HasValue)
		{
			// Opening hour: set up new pending entries.
			SetupPendingEntries(candle.ClosePrice);
		}
	}

	private void CheckPendingEntries(ICandleMessage candle)
	{
		if (_pendingBuyPrice is decimal buyPrice && candle.HighPrice >= buyPrice && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();
			BuyMarket();
			_entryPrice = buyPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
			return;
		}

		if (_pendingSellPrice is decimal sellPrice && candle.LowPrice <= sellPrice && Position >= 0)
		{
			if (Position > 0)
				SellMarket();
			SellMarket();
			_entryPrice = sellPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
		}
	}

	private void ManageRisk(ICandleMessage candle)
	{
		if (_pipSize <= 0m || _entryPrice is not decimal entry)
			return;

		var takeProfitDistance = TakeProfitPips * _pipSize;
		var stopLossDistance = StopLossPips * _pipSize;

		if (Position > 0m)
		{
			if (takeProfitDistance > 0m && candle.HighPrice - entry >= takeProfitDistance)
			{
				SellMarket();
				_entryPrice = null;
				return;
			}

			if (stopLossDistance > 0m && entry - candle.LowPrice >= stopLossDistance)
			{
				SellMarket();
				_entryPrice = null;
			}
		}
		else if (Position < 0m)
		{
			if (takeProfitDistance > 0m && entry - candle.LowPrice >= takeProfitDistance)
			{
				BuyMarket();
				_entryPrice = null;
				return;
			}

			if (stopLossDistance > 0m && candle.HighPrice - entry >= stopLossDistance)
			{
				BuyMarket();
				_entryPrice = null;
			}
		}
	}

	private void ExitPosition()
	{
		if (Position > 0m)
			SellMarket();
		else if (Position < 0m)
			BuyMarket();
		_entryPrice = null;
	}

	private void SetupPendingEntries(decimal referencePrice)
	{
		if (_pipSize <= 0m)
			return;

		var distance = DistancePips * _pipSize;
		if (distance <= 0m)
			return;

		_pendingBuyPrice = referencePrice + distance;
		_pendingSellPrice = referencePrice - distance;
	}
}