Auf GitHub ansehen

Index-Tester-Strategie

Überblick

Die Indices Tester Strategy ist eine direkte Portierung des MetaTrader 5 Expertenberaters „Indices Tester“. Das System konzentriert sich auf den Intraday-Indexhandel, bei dem eine einzelne Long-Position während eines sehr engen Handelsfensters eröffnet wird. Handelsentscheidungen basieren ausschließlich auf Zeitfiltern und Betriebslimits:

  • Ein einzelner konfigurierbarer Kerzenstrom steuert die interne Uhr der Strategie.
  • Neue Positionen können nur zwischen den konfigurierten Start- und Endzeiten der Sitzung eröffnet werden.
  • Pro Tag ist eine feste Anzahl von Trades zulässig, wodurch wiederholte Wiedereinstiege vermieden werden.
  • Alle offenen Positionen werden zu einem definierten Liquidationszeitpunkt zwangsweise geschlossen.
  • Die Strategie funktioniert nur auf der Long-Seite und spiegelt den ursprünglichen Expert Advisor wider.

Diese Implementierung verwendet das übergeordnete StockSharp API, abonniert Kerzendaten mit SubscribeCandles und verarbeitet Handelsentscheidungen im Callback ProcessCandle. Es sind keine Indikatoren erforderlich, sodass die Logik schlank bleibt und sich auf Timing und Risikokontrollen konzentriert.

Handelslogik

  1. Täglicher Reset – die Strategie verfolgt den aktuellen Handelstag. Wenn ein neuer Tag beginnt, werden alle Zähler zurückgesetzt, sodass für diesen Tag ein neues Handelsvolumen möglich ist.
  2. Eintrittsfenster – Nur Kerzen mit einer Schließzeit, die genau innerhalb des [SessionStart, SessionEnd)-Intervalls liegt, können Einträge auslösen. Dies reproduziert die TimeStart- und TimeEnd-Prüfungen aus dem Originalcode.
  3. Positions- und Handelslimits – Eingaben werden übersprungen, wenn die Anzahl der am aktuellen Tag bereits eröffneten Trades DailyTradeLimit erreicht hat oder wenn die Anzahl gleichzeitig offener Positionen MaxOpenPositions überschreitet.
  4. Auftragseinreichung – wenn alle Bedingungen übereinstimmen, übermittelt die Strategie einen Marktkaufauftrag für TradeVolume Einheiten. Der Handelszähler für den Tag wird unmittelbar nach der Auftragserteilung erhöht.
  5. Erzwungener Ausstieg – wenn eine Kerze nach CloseTime schließt und eine aktive Long-Position besteht, schließt die Strategie die Position mit einer Marktverkaufsorder. Dies spiegelt die ClosePos()-Timerlogik aus der MQL-Implementierung wider.

Die Kombination aus Handelszähler und Positionsbegrenzer gewährleistet, dass sich das System standardmäßig wie ein einfacher Einzelhandelsplaner pro Tag verhält und dennoch eine Parameteranpassung für häufigere Aktivitäten ermöglicht.

Parameter

Name Beschreibung
CandleType Primäre Kerzenserie, die die Strategieuhr antreibt (standardmäßig 1-Minuten-Kerzen).
SessionStart Tageszeit, zu der neue Trades beginnen dürfen.
SessionEnd Tageszeit, zu der neue Trades nicht mehr zulässig sind.
CloseTime Tageszeit, zu der alle verbleibenden offenen Positionen liquidiert werden.
DailyTradeLimit Maximal zulässige Anzahl von Einträgen pro Tag, bevor der Handel ausgesetzt wird.
MaxOpenPositions Maximale Anzahl gleichzeitig offener Long-Positionen (gezählt in Handelseinheiten).
TradeVolume Für jeden Eintrag verwendetes Market-Order-Volumen.

Hinweise und Unterschiede

  • StockSharp stellt keine MetaTrader-Sitzungstabellen zur Verfügung, daher basiert die Konvertierung auf der Austauschzeit von Kerzenzeitstempeln zusammen mit dem IsFormedAndOnlineAndAllowTrading()-Guard.
  • Der ursprüngliche Fachberater verwendete Timer der zweiten Ebene; Dieser Port nutzt Kerzenschließungen, um sowohl den Einstiegszeitpunkt als auch erzwungene Ausstiege zu steuern, was für Handelsfenster auf Minutenebene ausreicht.
  • Die Handelszahlen werden zu Beginn jedes Handelstages zurückgesetzt, der anhand der Schlusszeiten der Kerzen ermittelt wird. Dadurch bleibt das Verhalten über verschiedene Zeitzonen hinweg konsistent, solange die Quelle der Kerze mit der gewünschten Börse übereinstimmt.

Nutzungstipps

  • Stellen Sie sicher, dass der konfigurierte CandleType mit dem gehandelten Markt übereinstimmt, damit die Zeitfilter mit der gewünschten Sitzung übereinstimmen.
  • Erhöhen Sie DailyTradeLimit, wenn mehrere Versuche pro Tag erforderlich sind, beispielsweise bei kürzeren Zeitrahmen.
  • Setzen Sie MaxOpenPositions nur dann über 1, wenn eine teilweise Skalierung in Positionen gewünscht ist; Andernfalls behalten Sie die Standardeinstellung bei, um das MetaTrader-Skript genau nachzuahmen.
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 expert advisor "Indices Tester".
/// Implements a time filtered long-only session with daily trade and position limits.
/// </summary>
public class IndicesTesterStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<TimeSpan> _sessionStart;
	private readonly StrategyParam<TimeSpan> _sessionEnd;
	private readonly StrategyParam<TimeSpan> _closeTime;
	private readonly StrategyParam<int> _dailyTradeLimit;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<decimal> _tradeVolume;

	private DateTime _currentDay;
	private int _tradesOpenedToday;

	/// <summary>
	/// Candle type used to drive the strategy clock.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Session start time when new long positions may be opened.
	/// </summary>
	public TimeSpan SessionStart
	{
		get => _sessionStart.Value;
		set => _sessionStart.Value = value;
	}

	/// <summary>
	/// Session end time after which new positions are not allowed.
	/// </summary>
	public TimeSpan SessionEnd
	{
		get => _sessionEnd.Value;
		set => _sessionEnd.Value = value;
	}

	/// <summary>
	/// Time of day when all active positions are closed.
	/// </summary>
	public TimeSpan CloseTime
	{
		get => _closeTime.Value;
		set => _closeTime.Value = value;
	}

	/// <summary>
	/// Maximum number of entries that can be opened during a single trading day.
	/// </summary>
	public int DailyTradeLimit
	{
		get => _dailyTradeLimit.Value;
		set => _dailyTradeLimit.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous long positions measured in trade units.
	/// </summary>
	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	/// <summary>
	/// Order volume submitted with every market entry.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="IndicesTesterStrategy"/> class.
	/// </summary>
	public IndicesTesterStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe that drives the logic", "General");

		_sessionStart = Param(nameof(SessionStart), new TimeSpan(0, 0, 0))
			.SetDisplay("Session Start", "Time of day when entries become eligible", "Trading");

		_sessionEnd = Param(nameof(SessionEnd), new TimeSpan(23, 0, 0))
			.SetDisplay("Session End", "Time of day when new entries stop", "Trading");

		_closeTime = Param(nameof(CloseTime), new TimeSpan(23, 30, 0))
			.SetDisplay("Close Time", "Time of day used to liquidate open positions", "Risk");

		_dailyTradeLimit = Param(nameof(DailyTradeLimit), 1)
			.SetGreaterThanZero()
			.SetDisplay("Daily Trades", "Maximum number of trades per day", "Risk");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 1)
			.SetGreaterThanZero()
			.SetDisplay("Open Positions", "Maximum simultaneous long positions", "Risk");

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Market order volume for new positions", "Trading");
	}

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

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

		_currentDay = default;
		_tradesOpenedToday = 0;
	}

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

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ignore unfinished candles because the original EA worked on closed data.
		if (candle.State != CandleStates.Finished)
			return;

		var candleTime = candle.CloseTime;
		if (_currentDay != candleTime.Date)
		{
			// Reset the intraday counters on the first candle of a new session.
			_currentDay = candleTime.Date;
			_tradesOpenedToday = 0;
		}

		var timeOfDay = candleTime.TimeOfDay;

		// Liquidate open positions once the configured close time is reached.
		if (Position > 0m && timeOfDay >= CloseTime)
		{
			SellMarket(Position);
			return;
		}

		// Only evaluate entries strictly inside the trading window.
		if (timeOfDay <= SessionStart || timeOfDay >= SessionEnd)
			return;

		// Respect the daily trade allowance taken from the original EA.
		if (_tradesOpenedToday >= DailyTradeLimit)
			return;

		// Skip entries when the simultaneous position limit would be exceeded.
		if (GetOpenPositionCount() >= MaxOpenPositions)
			return;

		var volume = TradeVolume;
		if (volume <= 0m)
			return;

		// Submit the market order and immediately update the per-day trade counter.
		BuyMarket(volume);
		_tradesOpenedToday++;
	}

	private int GetOpenPositionCount()
	{
		if (Position == 0m)
			return 0;

		var volume = TradeVolume;
		if (volume <= 0m)
			return 1;

		return (int)Math.Ceiling(Math.Abs(Position) / volume);
	}
}