Auf GitHub ansehen

TimeEA-Strategie

Die TimeEA-Strategie repliziert den ursprünglichen MetaTrader-Expertenberater "TimeEA" in StockSharp. Sie verwaltet eine einzelne Position ausschließlich basierend auf der Tageszeit: Sie öffnet zu einem konfigurierten Zeitpunkt, hält die Position in einer fixen Richtung und schließt sie entweder zu einer geplanten Abschlusszeit oder sobald optionale Stop-Loss/Take-Profit-Level überschritten werden.

Im Gegensatz zu indikatorbasierten Systemen konzentriert sich diese Implementierung auf diszipliniertes Sitzungsmanagement. Sie gewährleistet nur einen Einstieg pro Handelstag, bereinigt entgegengesetzte Exposition vor dem Öffnen und erzwingt konfigurierbare Mindestabstände für Schutzaufträge, um die Stop-Level-Beschränkungen eines Brokers nachzuahmen.

Funktionsweise

  1. Die Strategie abonniert eine konfigurierbare Kerzenserie (standardmäßig 1 Minute) und bewertet nur abgeschlossene Kerzen.
  2. Wenn der Schluss einer Kerze die konfigurierte Eröffnungszeit kreuzt, führt die Strategie aus:
    • Schließt jede entgegengesetzte Position, die möglicherweise noch offen ist.
    • Platziert einen Marktauftrag in der gewählten Richtung (Kauf oder Verkauf) mit dem angegebenen Volumen.
    • Zeichnet Stop-Loss- und Take-Profit-Preise in Punkten (Preisschritte) vom Einstieg auf, wobei der Mindestabstands-Multiplikator angewendet wird.
  3. Während der Sitzung überwacht die Strategie Kerzen:
    • Wenn eine Kerze das gespeicherte Stop-Loss- oder Take-Profit-Level berührt, wird die Position sofort geschlossen.
    • Wenn die Kerze das Schlusszeit-Fenster kreuzt, wird die Position unabhängig von Gewinn oder Verlust abgeflacht.
  4. Nach dem Schließen des Trades (durch Stop, Ziel oder Zeitplan) bleibt die Strategie flach bis zum nächsten Handelstag.

Dieser Ablauf reproduziert das "einmal täglich öffnen"-Verhalten der MetaTrader-Version, die sich auf TimeCurrent() und Time[0]-Vergleiche stützte.

Parameter

Name Beschreibung
Open Time Tageszeit für das Öffnen des Trades. Akzeptiert HH:MM:SS.
Close Time Tageszeit für das Abflachen aller Positionen. Kann am gleichen Tag sein oder in den nächsten Tag übergehen.
Position Type Richtung der Position (Buy oder Sell).
Order Volume Menge beim Senden des Marktauftrags.
Stop Loss (points) Abstand in Preisschritten für den Schutz-Stop. Auf 0 setzen, um zu deaktivieren.
Take Profit (points) Abstand in Preisschritten für das Gewinnziel. Auf 0 setzen, um zu deaktivieren.
Minimum Distance Multiplier Minimaler Offset für Stop und Ziel (in Preisschritten) zur Emulation des ursprünglichen Stop-Level-Checks gegen den Spread.
Candle Type Datenserie zur Erkennung von Zeitgrenzen. Standard sind 1-Minuten-Kerzen.

Praktische Hinweise

  • Einzelner Einstieg pro Tag – Sobald die Eröffnungszeit ausgelöst wird, wird die Strategie nicht wieder einsteigen bis zum nächsten Kalendertag, auch wenn die Position früh gestoppt wurde.
  • Mitternacht-Unterstützung – Sowohl Eröffnungs- als auch Schlusszeiten können vor oder nach Mitternacht festgelegt werden. Der Helfer respektiert Sitzungen, die über 00:00 Uhr hinausgehen.
  • Volumenhandling – Marktaufträge respektieren den Order Volume-Parameter; an die Kontraktgröße des ausgewählten Instruments anpassen.
  • Stop-Level-Emulation – Der Mindestabstands-Multiplikator stellt sicher, dass Stops/Ziele mindestens eine definierte Anzahl von Punkten vom Einstieg entfernt sind, was die ursprüngliche "Spread × Multiplikator"-Regel widerspiegelt.
  • Datenanforderungen – Die Strategie ist auf konsistente Kerzen für das Timing angewiesen. Börsenlokale Zeitrahmen verwenden, um Zeitzonendrift zu vermeiden.
  • Risikomanagement – Stops und Ziele werden intern gepflegt; es werden keine serverseitigen OCO-Aufträge erstellt. Wenn eine Kerze die Schwellen kreuzt, gibt die Strategie einen Marktauftrag zum Ausstieg aus.

Anwendungsfälle

  • Automatisierung sitzungsbasierter Einträge (z.B. Eröffnen von Positionen zur London- oder New-York-Eröffnung).
  • Ausführung von Richtungsvoreingenommenheits-Strategien, bei denen die Richtung im Voraus bekannt ist, aber die Ausführung einem genauen Zeitplan folgen muss.
  • Emulation von MetaTrader-Stil-Zeitauslösern innerhalb der StockSharp High-Level-API ohne manuelle Timer.

Einschränkungen

  • Slippage wird implizit durch Marktaufträge behandelt; es gibt keinen separaten Abweichungsparameter wie in MetaTrader.
  • Der Mindestabstands-Multiplikator liest keine dynamischen Spreads; er erzwingt ein statisches Polster in Preisschritten.
  • Die Strategie geht davon aus, dass pro Instanz nur ein Instrument/Wertpapier gehandelt wird.

Erste Schritte

  1. Strategieparameter im Designer oder über Code konfigurieren (Eröffnungs-/Schlusszeiten, Richtung, Volumen, Risikoabstände).
  2. Strategie dem gewünschten Wertpapier und der Datenquelle zuordnen.
  3. Sicherstellen, dass die Kerzenserie dieselbe Zeitzone wie der beabsichtigte Zeitplan verwendet.
  4. Strategie ausführen und das Handelsprotokoll überwachen; visuelle Overlays können über DrawCandles und DrawOwnTrades bei Bedarf aktiviert werden.

Die Logik ist vollständig in CS/TimeEaStrategy.cs enthalten, mit ausführlichen Inline-Kommentaren, die jede Phase des Workflows erklären.

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>
/// Time-based strategy that opens a single directional position at the configured time
/// and closes it at another time or when optional stop/target levels are hit.
/// </summary>
public class TimeEaStrategy : Strategy
{
	private readonly StrategyParam<TimeSpan> _openTime;
	private readonly StrategyParam<TimeSpan> _closeTime;
	private readonly StrategyParam<TimeEaPositionTypes> _openedType;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _minSpreadMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private DateTime? _lastEntryDate;
	private DateTime? _lastCloseDate;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;

	/// <summary>
	/// Time of day to open the position.
	/// </summary>
	public TimeSpan OpenTime
	{
		get => _openTime.Value;
		set => _openTime.Value = value;
	}

	/// <summary>
	/// Time of day to close the position.
	/// </summary>
	public TimeSpan CloseTime
	{
		get => _closeTime.Value;
		set => _closeTime.Value = value;
	}

	/// <summary>
	/// Direction of the position opened at the scheduled time.
	/// </summary>
	public TimeEaPositionTypes OpenedType
	{
		get => _openedType.Value;
		set => _openedType.Value = value;
	}

	/// <summary>
	/// Market order volume for opening trades.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

	/// <summary>
	/// Minimal distance multiplier applied to stops and targets.
	/// </summary>
	public int MinSpreadMultiplier
	{
		get => _minSpreadMultiplier.Value;
		set => _minSpreadMultiplier.Value = value;
	}

	/// <summary>
	/// Candle type used to evaluate time windows.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="TimeEaStrategy"/>.
	/// </summary>
	public TimeEaStrategy()
	{
		_openTime = Param(nameof(OpenTime), new TimeSpan(1, 0, 0))
			.SetDisplay("Open Time", "Time to enter the market", "Scheduling");

		_closeTime = Param(nameof(CloseTime), TimeSpan.Zero)
			.SetDisplay("Close Time", "Time to exit the market", "Scheduling");

		_openedType = Param(nameof(OpenedType), TimeEaPositionTypes.Buy)
			.SetDisplay("Position Type", "Direction to maintain", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Quantity for market orders", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance in price steps", "Risk");

		_minSpreadMultiplier = Param(nameof(MinSpreadMultiplier), 2)
			.SetNotNegative()
			.SetDisplay("Minimum Distance Multiplier", "Minimal offset applied to stops", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
			.SetDisplay("Candle Type", "Candles used for scheduling", "General");
	}

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

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

		_lastEntryDate = null;
		_lastCloseDate = null;
		ResetRiskLevels();
	}

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Use finished candles to evaluate the time windows.
		if (candle.State != CandleStates.Finished)
			return;

		var candleDate = candle.CloseTime.Date;

		if (ContainsTime(candle, OpenTime) && _lastEntryDate != candleDate)
		{
			_lastEntryDate = candleDate;
			HandleOpen(candle);
		}

		if (ContainsTime(candle, CloseTime) && _lastCloseDate != candleDate)
		{
			_lastCloseDate = candleDate;

			if (Position != 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
			return;
		}

		ManageRisk(candle);
	}

	private void HandleOpen(ICandleMessage candle)
	{
		// Close opposite exposure before opening a new position.
		if (OpenedType == TimeEaPositionTypes.Buy)
		{
			if (Position < 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}

			if (Position == 0 && OrderVolume > 0)
			{
				BuyMarket(OrderVolume);
				SetRiskLevels(candle.ClosePrice, true);
			}
		}
		else
		{
			if (Position > 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}

			if (Position == 0 && OrderVolume > 0)
			{
				SellMarket(OrderVolume);
				SetRiskLevels(candle.ClosePrice, false);
			}
		}
	}

	private void ManageRisk(ICandleMessage candle)
	{
		// Monitor active position for stop loss and take profit.
		if (Position > 0)
		{
			if (_stopPrice > 0m && candle.LowPrice <= _stopPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice > 0m && candle.HighPrice >= _stopPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
		}
	}

	private void SetRiskLevels(decimal closePrice, bool isLong)
	{
		_entryPrice = closePrice;

		var step = Security?.PriceStep ?? 1m;
		var minDistance = Math.Max(MinSpreadMultiplier, 0) * step;
		var stopDistance = StopLossPoints > 0 ? Math.Max(StopLossPoints * step, minDistance) : 0m;
		var takeDistance = TakeProfitPoints > 0 ? Math.Max(TakeProfitPoints * step, minDistance) : 0m;

		// Calculate price levels in the same direction logic as the original Expert Advisor.
		if (isLong)
		{
			_stopPrice = stopDistance > 0m ? closePrice - stopDistance : 0m;
			_takeProfitPrice = takeDistance > 0m ? closePrice + takeDistance : 0m;
		}
		else
		{
			_stopPrice = stopDistance > 0m ? closePrice + stopDistance : 0m;
			_takeProfitPrice = takeDistance > 0m ? closePrice - takeDistance : 0m;
		}
	}

	private void ResetRiskLevels()
	{
		_entryPrice = 0m;
		_stopPrice = 0m;
		_takeProfitPrice = 0m;
	}

	private static bool ContainsTime(ICandleMessage candle, TimeSpan target)
	{
		var openTime = candle.OpenTime;
		var closeTime = candle.CloseTime;

		var openSpan = openTime.TimeOfDay;
		var closeSpan = closeTime.TimeOfDay;

		var crossesMidnight = closeTime.Date > openTime.Date || closeSpan < openSpan;

		if (!crossesMidnight)
			return target >= openSpan && target <= closeSpan;

		var startMinutes = openSpan.TotalMinutes;
		var endMinutes = closeSpan.TotalMinutes + TimeSpan.FromDays(1).TotalMinutes;
		var targetMinutes = target.TotalMinutes;

		if (targetMinutes < startMinutes)
			targetMinutes += TimeSpan.FromDays(1).TotalMinutes;

		return targetMinutes >= startMinutes && targetMinutes <= endMinutes;
	}

	/// <summary>
	/// Supported position directions.
	/// </summary>
	public enum TimeEaPositionTypes
	{
		/// <summary>
		/// Open a long position.
		/// </summary>
		Buy,

		/// <summary>
		/// Open a short position.
		/// </summary>
		Sell
	}
}