Ver no GitHub

Estratégia de testador de índices

Visão geral

A Indices Tester Strategy é uma versão direta do MetaTrader 5 consultor especialista "Indices Tester". O sistema concentra-se na negociação de índices intradiários, onde uma única posição longa é aberta durante uma janela de negociação muito estreita. As decisões de negociação dependem puramente de filtros de tempo e limites operacionais:

  • Um único fluxo de velas configurável aciona o relógio interno da estratégia.
  • Novas posições só podem ser abertas entre os horários de início e término da sessão configurados.
  • É permitido um número fixo de negociações por dia, evitando reentradas repetidas.
  • Todas as posições abertas são fechadas à força num momento de liquidação definido.
  • A estratégia opera apenas no lado comprado, espelhando o consultor especialista original.

Esta implementação usa o StockSharp API de alto nível, assina dados de velas com SubscribeCandles e lida com decisões de negociação no retorno de chamada ProcessCandle. Não são necessários indicadores, mantendo a lógica enxuta e focada no tempo e nos controles de risco.

Lógica de negociação

  1. Reinicialização diária – a estratégia acompanha o dia de negociação atual. Quando um novo dia começa, todos os contadores são zerados, permitindo uma nova margem de negociação para aquele dia.
  2. Janela de entrada – somente velas com tempo de fechamento estritamente dentro do intervalo [SessionStart, SessionEnd) podem acionar entradas. Isso reproduz as verificações TimeStart e TimeEnd do código original.
  3. Limites de posições e negociações – as entradas serão ignoradas se o número de negociações já abertas durante o dia atual atingir DailyTradeLimit ou se o número de posições abertas simultaneamente exceder MaxOpenPositions.
  4. Envio de ordem – quando todas as condições estão alinhadas, a estratégia envia uma ordem de compra de mercado para TradeVolume unidades. O contador de negociações do dia é incrementado imediatamente após o envio da ordem.
  5. Saída forçada – se uma vela fechar após CloseTime e houver uma posição longa ativa, a estratégia fecha a posição com uma ordem de venda a mercado. Isso reflete a lógica do temporizador ClosePos() da implementação MQL.

A combinação do contador de negociações e do limitador de posições garante que o sistema se comporte como um simples agendador de negociação única por dia por padrão, ao mesmo tempo que permite o ajuste de parâmetros para atividades mais frequentes.

Parâmetros

Nome Descrição
CandleType Série de velas primárias que aciona o relógio estratégico (o padrão é velas de 1 minuto).
SessionStart Hora do dia em que novas negociações podem começar.
SessionEnd Hora do dia em que novas negociações não são mais permitidas.
CloseTime Hora do dia em que qualquer posição aberta restante é liquidada.
DailyTradeLimit Número máximo de entradas permitidas por dia antes da suspensão da negociação.
MaxOpenPositions Número máximo de posições longas abertas simultaneamente (contadas em unidades de negociação).
TradeVolume Volume de ordens de mercado utilizado para cada entrada.

Notas e diferenças

  • StockSharp não expõe tabelas de sessão MetaTrader, portanto, a conversão depende do tempo de troca dos carimbos de data e hora da vela junto com a proteção IsFormedAndOnlineAndAllowTrading().
  • O consultor especialista original usava temporizadores de segundo nível; esta porta aproveita o fechamento de velas para impulsionar o tempo de entrada e as saídas forçadas, o que é suficiente para janelas de negociação de nível minuto.
  • As contagens de negociação são redefinidas no início de cada dia de negociação detectada a partir dos horários de fechamento das velas, mantendo o comportamento consistente em diferentes fusos horários, desde que a origem da vela corresponda à bolsa desejada.

Dicas de uso

  • Certifique-se de que o CandleType configurado corresponda ao mercado que está sendo negociado para que os filtros de tempo se alinhem com a sessão desejada.
  • Aumente DailyTradeLimit se forem necessárias várias tentativas por dia, por exemplo, ao executar em intervalos de tempo mais curtos.
  • Defina MaxOpenPositions acima de 1 somente quando o escalonamento parcial em posições for desejado; caso contrário, mantenha o padrão para imitar exatamente o 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);
	}
}