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
- Die Strategie abonniert eine konfigurierbare Kerzenserie (standardmäßig 1 Minute) und bewertet nur abgeschlossene Kerzen.
- 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.
- 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.
- 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
- Strategieparameter im Designer oder über Code konfigurieren (Eröffnungs-/Schlusszeiten, Richtung, Volumen, Risikoabstände).
- Strategie dem gewünschten Wertpapier und der Datenquelle zuordnen.
- Sicherstellen, dass die Kerzenserie dieselbe Zeitzone wie der beabsichtigte Zeitplan verwendet.
- 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
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class time_ea_strategy(Strategy):
TYPE_BUY = 0
TYPE_SELL = 1
def __init__(self):
super(time_ea_strategy, self).__init__()
self._open_time = self.Param("OpenTime", TimeSpan(1, 0, 0))
self._close_time = self.Param("CloseTime", TimeSpan.Zero)
self._opened_type = self.Param("OpenedType", self.TYPE_BUY)
self._order_volume = self.Param("OrderVolume", 0.1)
self._stop_loss_points = self.Param("StopLossPoints", 0)
self._take_profit_points = self.Param("TakeProfitPoints", 0)
self._min_spread_multiplier = self.Param("MinSpreadMultiplier", 2)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1)))
self._last_entry_date = None
self._last_close_date = None
self._entry_price = 0.0
self._stop_price = 0.0
self._take_profit_price = 0.0
@property
def OpenTime(self):
return self._open_time.Value
@property
def CloseTime(self):
return self._close_time.Value
@property
def OpenedType(self):
return self._opened_type.Value
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def MinSpreadMultiplier(self):
return self._min_spread_multiplier.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(time_ea_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
candle_date = candle.CloseTime.Date
if self._contains_time(candle, self.OpenTime) and self._last_entry_date != candle_date:
self._last_entry_date = candle_date
self._handle_open(candle)
if self._contains_time(candle, self.CloseTime) and self._last_close_date != candle_date:
self._last_close_date = candle_date
pos = float(self.Position)
if pos != 0:
if pos > 0:
self.SellMarket(pos)
elif pos < 0:
self.BuyMarket(abs(pos))
self._reset_risk_levels()
return
self._manage_risk(candle)
def _handle_open(self, candle):
pos = float(self.Position)
if self.OpenedType == self.TYPE_BUY:
if pos < 0:
self.BuyMarket(abs(pos))
self._reset_risk_levels()
if float(self.Position) == 0 and float(self.OrderVolume) > 0:
self.BuyMarket(float(self.OrderVolume))
self._set_risk_levels(float(candle.ClosePrice), True)
else:
if pos > 0:
self.SellMarket(pos)
self._reset_risk_levels()
if float(self.Position) == 0 and float(self.OrderVolume) > 0:
self.SellMarket(float(self.OrderVolume))
self._set_risk_levels(float(candle.ClosePrice), False)
def _manage_risk(self, candle):
pos = float(self.Position)
if pos > 0:
if self._stop_price > 0 and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(pos)
self._reset_risk_levels()
return
if self._take_profit_price > 0 and float(candle.HighPrice) >= self._take_profit_price:
self.SellMarket(pos)
self._reset_risk_levels()
elif pos < 0:
if self._stop_price > 0 and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(abs(pos))
self._reset_risk_levels()
return
if self._take_profit_price > 0 and float(candle.LowPrice) <= self._take_profit_price:
self.BuyMarket(abs(pos))
self._reset_risk_levels()
def _set_risk_levels(self, close_price, is_long):
self._entry_price = close_price
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
if step <= 0:
step = 1.0
min_distance = max(self.MinSpreadMultiplier, 0) * step
stop_dist = max(self.StopLossPoints * step, min_distance) if self.StopLossPoints > 0 else 0.0
take_dist = max(self.TakeProfitPoints * step, min_distance) if self.TakeProfitPoints > 0 else 0.0
if is_long:
self._stop_price = close_price - stop_dist if stop_dist > 0 else 0.0
self._take_profit_price = close_price + take_dist if take_dist > 0 else 0.0
else:
self._stop_price = close_price + stop_dist if stop_dist > 0 else 0.0
self._take_profit_price = close_price - take_dist if take_dist > 0 else 0.0
def _reset_risk_levels(self):
self._entry_price = 0.0
self._stop_price = 0.0
self._take_profit_price = 0.0
def _contains_time(self, candle, target):
open_time = candle.OpenTime
close_time = candle.CloseTime
open_span = open_time.TimeOfDay
close_span = close_time.TimeOfDay
crosses_midnight = close_time.Date > open_time.Date or close_span < open_span
if not crosses_midnight:
return target >= open_span and target <= close_span
start_min = open_span.TotalMinutes
end_min = close_span.TotalMinutes + 1440.0
target_min = target.TotalMinutes
if target_min < start_min:
target_min += 1440.0
return target_min >= start_min and target_min <= end_min
def OnReseted(self):
super(time_ea_strategy, self).OnReseted()
self._last_entry_date = None
self._last_close_date = None
self._reset_risk_levels()
def CreateClone(self):
return time_ea_strategy()