Ver en GitHub

Estrategia del probador de índices

Descripción general

La Estrategia de Probador de Índices es una adaptación directa del asesor experto MetaTrader 5 "Probador de Índices". El sistema se centra en el comercio de índices intradía, donde se abre una única posición larga durante una ventana de negociación muy estrecha. Las decisiones comerciales se basan únicamente en filtros de tiempo y límites operativos:

  • Un único flujo de velas configurable impulsa el reloj interno de la estrategia.
  • Sólo se podrán abrir nuevas posiciones entre las horas de inicio y fin de sesión configuradas.
  • Se permite un número fijo de operaciones por día, lo que evita reingresos repetidos.
  • Todas las posiciones abiertas se cierran forzosamente en un momento de liquidación definido.
  • La estrategia opera únicamente en el lado largo, reflejando al asesor experto original.

Esta implementación utiliza StockSharp API de alto nivel, se suscribe a datos de velas con SubscribeCandles y maneja decisiones comerciales en la devolución de llamada ProcessCandle. No se requieren indicadores, lo que mantiene la lógica ágil y centrada en los controles de tiempo y riesgo.

Lógica de trading

  1. Restablecimiento diario: la estrategia realiza un seguimiento del día de negociación actual. Cuando comienza un nuevo día, todos los contadores se reinician, lo que permite una nueva asignación comercial para ese día.
  2. Ventana de entrada: solo las velas con un tiempo de cierre estrictamente dentro del intervalo [SessionStart, SessionEnd) pueden activar entradas. Esto reproduce las comprobaciones TimeStart y TimeEnd del código original.
  3. Límites de posiciones y operaciones: las entradas se omiten si la cantidad de operaciones ya abiertas durante el día actual ha alcanzado DailyTradeLimit, o si la cantidad de posiciones abiertas simultáneamente excede MaxOpenPositions.
  4. Envío de orden: cuando todas las condiciones se alinean, la estrategia envía una orden de compra de mercado por TradeVolume unidades. El contador de operaciones del día se incrementa inmediatamente después del envío de la orden.
  5. Salida forzada: si una vela se cierra después de CloseTime y hay una posición larga activa, la estrategia cierra la posición con una orden de venta de mercado. Esto refleja la lógica del temporizador ClosePos() de la implementación MQL.

La combinación del contador de operaciones y el limitador de posición garantiza que el sistema se comporte como un simple programador de operaciones por día de forma predeterminada y, al mismo tiempo, permite el ajuste de parámetros para una actividad más frecuente.

Parámetros

Nombre Descripción
CandleType Serie de velas primarias que impulsan el reloj de estrategia (por defecto, velas de 1 minuto).
SessionStart Hora del día en la que se permite iniciar nuevas operaciones.
SessionEnd Hora del día en la que ya no se permiten nuevas operaciones.
CloseTime Hora del día en que se liquida cualquier posición abierta restante.
DailyTradeLimit Número máximo de entradas permitidas por día antes de que se suspenda la negociación.
MaxOpenPositions Número máximo de posiciones largas abiertas simultáneamente (contadas en unidades comerciales).
TradeVolume Volumen de orden de mercado utilizado para cada entrada.

Notas y diferencias

  • StockSharp no expone tablas de sesión MetaTrader, por lo que la conversión se basa en el tiempo de intercambio de las marcas de tiempo de las velas junto con la protección IsFormedAndOnlineAndAllowTrading().
  • El asesor experto original utilizó cronómetros de segundo nivel; este puerto aprovecha los cierres de velas para impulsar tanto el momento de entrada como las salidas forzadas, lo cual es suficiente para ventanas de negociación a nivel de minutos.
  • Los recuentos de operaciones se restablecen al comienzo de cada día de negociación detectado a partir de las horas de cierre de las velas, lo que mantiene el comportamiento constante en diferentes zonas horarias siempre que la fuente de la vela coincida con el intercambio deseado.

Consejos de uso

  • Asegúrese de que el CandleType configurado coincida con el mercado en el que se negocia para que los filtros de tiempo se alineen con la sesión deseada.
  • Aumente DailyTradeLimit si se requieren varios intentos por día, por ejemplo, cuando se ejecuta en períodos de tiempo más cortos.
  • Establezca MaxOpenPositions encima de 1 solo cuando se desee un escalado parcial en posiciones; de lo contrario, mantenga el valor predeterminado para imitar exactamente el script MetaTrader.
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);
	}
}