GitHub で見る

Indices Tester Strategy

Overview

The Indices Tester Strategy is a direct port of the MetaTrader 5 expert advisor "Indices Tester". The system focuses on intraday index trading where a single long position is opened during a very narrow trading window. Trading decisions rely purely on time filters and operational limits:

  • A single configurable candle stream drives the internal clock of the strategy.
  • New positions can only be opened between the configured session start and end times.
  • A fixed number of trades is allowed per day, preventing repeated re-entries.
  • All open positions are forcibly closed at a defined liquidation time.
  • The strategy operates on the long side only, mirroring the original expert advisor.

This implementation uses the high-level StockSharp API, subscribes to candle data with SubscribeCandles, and handles trading decisions in the ProcessCandle callback. No indicators are required, keeping the logic lean and focused on timing and risk controls.

Trading Logic

  1. Daily reset – the strategy keeps track of the current trading day. When a new day starts all counters are reset, allowing a fresh trade allowance for that day.
  2. Entry window – only candles with a close time strictly inside the [SessionStart, SessionEnd) interval can trigger entries. This reproduces the TimeStart and TimeEnd checks from the original code.
  3. Position and trade limits – entries are skipped if the number of trades already opened during the current day has reached DailyTradeLimit, or if the number of simultaneously open positions exceeds MaxOpenPositions.
  4. Order submission – when all conditions align the strategy submits a market buy order for TradeVolume units. The counter of trades for the day is incremented immediately after order submission.
  5. Forced exit – if a candle closes after CloseTime and there is an active long position, the strategy closes the position with a market sell order. This mirrors the ClosePos() timer logic from the MQL implementation.

The combination of the trade counter and position limiter guarantees that the system behaves as a simple single-trade-per-day scheduler by default while still allowing parameter tuning for more frequent activity.

Parameters

Name Description
CandleType Primary candle series driving the strategy clock (defaults to 1-minute candles).
SessionStart Time of day when new trades are allowed to start.
SessionEnd Time of day when new trades are no longer allowed.
CloseTime Time of day when any remaining open position is liquidated.
DailyTradeLimit Maximum number of entries allowed per day before trading is suspended.
MaxOpenPositions Maximum number of simultaneously open long positions (counted in trade units).
TradeVolume Market order volume used for each entry.

Notes and Differences

  • StockSharp does not expose MetaTrader session tables, so the conversion relies on the exchange time from candle timestamps together with the IsFormedAndOnlineAndAllowTrading() guard.
  • The original expert advisor used second-level timers; this port leverages candle closures to drive both entry timing and forced exits, which is sufficient for minute-level trading windows.
  • Trade counts are reset at the beginning of each trading day detected from candle close times, keeping behaviour consistent across different time zones as long as the candle source matches the desired exchange.

Usage Tips

  • Ensure the configured CandleType matches the market being traded so that the time filters align with the desired session.
  • Increase DailyTradeLimit if multiple attempts per day are required, for example when running on shorter time frames.
  • Set MaxOpenPositions above 1 only when partial scaling into positions is desired; otherwise keep the default to mimic the MetaTrader script exactly.
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);
	}
}