Ver no GitHub

Estratégia TimeEA

A Estratégia TimeEA replica o consultor especialista original do MetaTrader "TimeEA" dentro do StockSharp. Ela gerencia uma única posição baseando-se exclusivamente na hora do dia: abre em um momento configurado, mantém a posição em uma direção fixa e sai ou em um horário de fechamento programado ou quando os níveis opcionais de stop-loss/take-profit são violados.

Ao contrário dos sistemas baseados em indicadores, esta implementação foca no gerenciamento disciplinado de sessão. Garante apenas uma entrada por dia de trading, limpa a exposição oposta antes de abrir e aplica distâncias mínimas configuráveis para ordens protetoras que imitam as limitações de nível de stop do corretor.

Como funciona

  1. A estratégia assina uma série de velas configurável (1 minuto por padrão) e avalia apenas as velas concluídas.
  2. Quando o fechamento de uma vela cruza o Horário de abertura configurado, a estratégia:
    • Fecha qualquer posição oposta que ainda possa estar aberta.
    • Coloca uma ordem a mercado na direção escolhida (Compra ou Venda) com o volume especificado.
    • Registra preços de stop-loss e take-profit em pontos (passos de preço) a partir da entrada, aplicando o multiplicador de distância mínima.
  3. Durante a sessão, a estratégia monitora as velas:
    • Se uma vela toca o nível de stop-loss ou take-profit armazenado, a posição é fechada imediatamente.
    • Se a vela cruzar a janela do Horário de fechamento, a posição é nivelada independentemente de lucro ou perda.
  4. Após fechar a negociação (por stop, alvo ou agenda), a estratégia permanece sem posição até o próximo dia de trading.

Este fluxo reproduz o comportamento "abrir uma vez por dia" da versão do MetaTrader que dependia de comparações TimeCurrent() e Time[0].

Parâmetros

Nome Descrição
Open Time Hora do dia para abrir a negociação. Aceita HH:MM:SS.
Close Time Hora do dia para nivelar todas as posições. Pode ser no mesmo dia ou passar para o dia seguinte.
Position Type Direção da posição (Buy ou Sell).
Order Volume Quantidade usada ao enviar a ordem a mercado.
Stop Loss (points) Distância em passos de preço para o stop protetor. Definir como 0 para desativar.
Take Profit (points) Distância em passos de preço para o alvo de lucro. Definir como 0 para desativar.
Minimum Distance Multiplier Offset mínimo aplicado tanto ao stop quanto ao alvo (em passos de preço) para emular a verificação original do nível de stop contra o spread.
Candle Type Série de dados usada para detectar limites de tempo. O padrão são velas de 1 minuto.

Notas práticas

  • Entrada única por dia – Uma vez que o horário de abertura é acionado, a estratégia não reingressará até o próximo dia de calendário mesmo que a posição tenha sido parada antecipadamente.
  • Suporte a cruzamento de meia-noite – Tanto os horários de abertura quanto de fechamento podem ser definidos antes ou depois da meia-noite. O auxiliar respeita sessões que continuam após 00:00.
  • Gerenciamento de volume – As ordens a mercado respeitam o parâmetro Order Volume; ajustar para o tamanho do contrato do instrumento selecionado.
  • Emulação de nível de stop – O multiplicador de distância mínima garante que stops/alvos fiquem pelo menos um número definido de pontos do ponto de entrada, refletindo a regra original "spread × multiplicador".
  • Requisitos de dados – A estratégia depende de velas consistentes para o timing. Usar intervalos de tempo locais da bolsa para evitar a deriva de fuso horário.
  • Gestão de risco – Stops e alvos são mantidos internamente; nenhuma ordem OCO do lado do servidor é criada. Quando uma vela cruzar os limiares, a estratégia emite uma ordem a mercado para sair.

Casos de uso

  • Automatizar entradas baseadas em sessão (p. ex., abrir posições na abertura de Londres ou Nova York).
  • Executar estratégias de viés direcional onde a direção é conhecida de antemão, mas a execução deve seguir um calendário preciso.
  • Emular gatilhos de tempo no estilo MetaTrader dentro da API de alto nível do StockSharp sem temporizadores manuais.

Limitações

  • O deslizamento é tratado implicitamente por ordens a mercado; não há um parâmetro de desvio separado como no MetaTrader.
  • O multiplicador de distância mínima não lê spreads dinâmicos; aplica um amortecedor estático expresso em passos de preço.
  • A estratégia assume que apenas um instrumento/segurança é negociado por instância.

Primeiros passos

  1. Configurar os parâmetros da estratégia no Designer ou via código (horários de abertura/fechamento, direção, volume, distâncias de risco).
  2. Anexar a estratégia ao instrumento e à fonte de dados desejados.
  3. Garantir que a série de velas use o mesmo fuso horário que o calendário pretendido.
  4. Executar a estratégia e monitorar o registro de negociações; sobreposições visuais podem ser habilitadas via DrawCandles e DrawOwnTrades se desejado.

A lógica está completamente contida em CS/TimeEaStrategy.cs com extensos comentários em linha explicando cada estágio do fluxo de trabalho.

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
	}
}