Ver en GitHub

Estrategia TimeEA

La Estrategia TimeEA replica el asesor experto original de MetaTrader "TimeEA" dentro de StockSharp. Gestiona una sola posición basándose exclusivamente en la hora del día: abre a un momento configurado, mantiene la posición en una dirección fija y sale ya sea en un tiempo de cierre programado o una vez que se superan los niveles opcionales de stop-loss / take-profit.

A diferencia de los sistemas basados en indicadores, esta implementación se centra en la gestión disciplinada de sesiones. Asegura solo una entrada por día de trading, limpia la exposición opuesta antes de abrir, y aplica distancias mínimas configurables para órdenes protectoras que imitan las limitaciones de nivel de stop del bróker.

Cómo funciona

  1. La estrategia se suscribe a una serie de velas configurable (1 minuto por defecto) y evalúa solo las velas completadas.
  2. Cuando el cierre de una vela cruza el Tiempo de apertura configurado, la estrategia:
    • Cierra cualquier posición opuesta que todavía pueda estar abierta.
    • Coloca una orden de mercado en la dirección elegida (Compra o Venta) con el volumen especificado.
    • Registra precios de stop-loss y take-profit en puntos (pasos de precio) desde la entrada, aplicando el multiplicador de distancia mínima.
  3. Durante la sesión la estrategia monitorea las velas:
    • Si una vela toca el nivel de stop-loss o take-profit almacenado, la posición se cierra inmediatamente.
    • Si la vela cruza la ventana del Tiempo de cierre, la posición se aplana independientemente de la ganancia o pérdida.
  4. Después de cerrar la operación (por stop, objetivo o programación) la estrategia permanece plana hasta el próximo día de trading.

Este flujo reproduce el comportamiento "abrir una vez por día" de la versión de MetaTrader que se basaba en comparaciones TimeCurrent() y Time[0].

Parámetros

Nombre Descripción
Open Time Hora del día para abrir la operación. Acepta HH:MM:SS.
Close Time Hora del día para aplanar todas las posiciones. Puede ser el mismo día o pasar al día siguiente.
Position Type Dirección de la posición (Buy o Sell).
Order Volume Cantidad usada al enviar la orden de mercado.
Stop Loss (points) Distancia en pasos de precio para el stop protector. Establecer en 0 para deshabilitar.
Take Profit (points) Distancia en pasos de precio para el objetivo de ganancia. Establecer en 0 para deshabilitar.
Minimum Distance Multiplier Offset mínimo aplicado tanto al stop como al objetivo (en pasos de precio) para emular la verificación original del nivel de stop contra el spread.
Candle Type Serie de datos usada para detectar los límites de tiempo. Por defecto son velas de 1 minuto.

Notas prácticas

  • Entrada única por día – Una vez que el tiempo de apertura se dispara, la estrategia no volverá a entrar hasta el siguiente día de calendario incluso si la posición fue stoped tempranamente.
  • Soporte de cruce de medianoche – Tanto los tiempos de apertura como de cierre pueden establecerse antes o después de la medianoche. El helper respeta sesiones que continúan después de las 00:00.
  • Manejo de volumen – Las órdenes de mercado respetan el parámetro Order Volume; ajustar al tamaño del contrato del instrumento seleccionado.
  • Emulación de nivel de stop – El multiplicador de distancia mínima asegura que los stops/objetivos estén al menos un número definido de puntos lejos de la entrada, reflejando la regla original "spread × multiplicador".
  • Requisitos de datos – La estrategia depende de velas consistentes para el timing. Usar marcos temporales locales del mercado para evitar la deriva de zona horaria.
  • Gestión de riesgos – Los stops y objetivos se mantienen internamente; no se crean órdenes OCO del lado del servidor. Cuando una vela cruza los umbrales, la estrategia emite una orden de mercado para salir.

Casos de uso

  • Automatizar entradas basadas en sesión (p. ej., abrir posiciones en la apertura de Londres o Nueva York).
  • Ejecutar estrategias de sesgo direccional donde la dirección se conoce de antemano pero la ejecución debe seguir un calendario preciso.
  • Emular disparadores de tiempo al estilo MetaTrader dentro de la API de alto nivel de StockSharp sin temporizadores manuales.

Limitaciones

  • El deslizamiento se maneja implícitamente por órdenes de mercado; no hay un parámetro de desviación separado como en MetaTrader.
  • El multiplicador de distancia mínima no lee spreads dinámicos; aplica un cojín estático expresado en pasos de precio.
  • La estrategia asume que solo se negocia un instrumento/instrumento por instancia.

Primeros pasos

  1. Configurar los parámetros de la estrategia en Designer o vía código (tiempos de apertura/cierre, dirección, volumen, distancias de riesgo).
  2. Adjuntar la estrategia al instrumento y fuente de datos deseados.
  3. Asegurarse de que la serie de velas use la misma zona horaria que el horario previsto.
  4. Ejecutar la estrategia y monitorear el registro de operaciones; las superposiciones visuales pueden habilitarse vía DrawCandles y DrawOwnTrades si se desea.

La lógica está completamente contenida en CS/TimeEaStrategy.cs con extensos comentarios en línea que explican cada etapa del flujo de trabajo.

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