Ver en GitHub

Estrategia Canal EA con Órdenes Límite

Descripción general

  • Origen: convertida del expert de MetaTrader 5 ChannelEA1.mq5.
  • Propósito: monitorear un canal de precio intradía entre dos horas definidas por el usuario y colocar órdenes límite al final de esa ventana.
  • Enfoque: la estrategia realiza un seguimiento de los precios más altos y más bajos observados durante la sesión y coloca órdenes límite simétricas para operar posibles reversiones de vuelta al lado opuesto del canal.

La estrategia es adecuada para instrumentos que exhiben reversión a la media una vez que se establece un rango diario. Por diseño funciona en cuentas de compensación: una orden de venta límite ejecutada cerrará una posición larga existente antes de abrir una nueva corta y viceversa.

Parámetros

Nombre Por defecto Descripción
BeginHour 1 Hora (0-23) cuando comienza el seguimiento del rango intradía. La estrategia cancela órdenes pendientes y cierra posiciones en este momento.
EndHour 10 Hora (0-23) cuando se evalúa el rango acumulado y se colocan nuevas órdenes límite. Soporta sesiones nocturnas: si BeginHour > EndHour, la sesión abarca la medianoche.
OrderVolume 1 Volumen aplicado a cada orden pendiente.
CandleType Marco temporal de 1 hora Serie de velas utilizada para construir el canal. Puede cambiar a cualquier marco temporal soportado por StockSharp.

Lógica de trading

  1. Manejo de sesión
    • La estrategia deriva los timestamps de inicio y fin de sesión de los parámetros BeginHour y EndHour usando los timestamps de las velas. Cuando BeginHour > EndHour, el fin se traslada al día siguiente.
    • En la primera vela terminada cuyo tiempo de cierre alcanza el límite de inicio, la estrategia cancela todas las órdenes activas, cierra la posición abierta y restablece las estadísticas de sesión.
  2. Construcción del canal
    • Solo las velas cuyo tiempo de apertura se encuentra dentro de la ventana de sesión contribuyen al rango. La estrategia mantiene el máximo y mínimo corrientes para la sesión y cuenta el número de velas contribuyentes.
    • Se requieren al menos dos velas terminadas para formar un rango válido, replicando el comportamiento del expert MQL5 original (condición n > 2).
  3. Colocación de órdenes al final de sesión
    • Cuando una vela terminada cruza el límite de fin, la estrategia verifica que el rango se ha formado y que el mínimo está estrictamente por debajo del máximo.
    • Luego coloca dos órdenes pendientes:
      • BuyLimit en el mínimo de sesión registrado con volumen OrderVolume.
      • SellLimit en el máximo de sesión registrado con el mismo volumen.
    • Las órdenes permanecen activas hasta que comienza la próxima sesión. Dado que la estrategia corre en cuenta de compensación, estas órdenes sirven tanto como entradas como salidas: por ejemplo, el SellLimit cierra una posición larga existente en el máximo de sesión antes de establecer una nueva corta.
  4. Preparación de la próxima sesión
    • En el próximo límite de inicio, la estrategia cierra cualquier posición restante y elimina las órdenes pendientes sobrantes antes de medir el nuevo canal.

Notas adicionales

  • No se establece stop-loss explícito. La gestión de riesgos debe controlarse a través del dimensionamiento de posición, anulaciones manuales o lógica protectora externa.
  • La lógica usa solo velas terminadas (CandleStates.Finished) para mantenerse alineada con el comportamiento del EA original.
  • Asegúrese de que la zona horaria del feed de datos y del servidor coincida con sus expectativas, porque los límites de sesión se evalúan en tiempo de bolsa/local.
  • Al optimizar, considere tanto las horas de trading como la duración de la vela; la estrategia es sensible a la combinación porque el rango registrado depende del marco temporal seleccionado.
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>
/// Channel trading strategy that places limit orders at the end of the monitored session.
/// </summary>
public class ChannelEaLimitsStrategy : Strategy
{
	private readonly StrategyParam<int> _beginHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private DateTimeOffset _sessionStart;
	private DateTimeOffset _sessionEnd;
	private decimal _sessionHigh;
	private decimal _sessionLow;
	private int _barsInSession;
	private DateTimeOffset? _prevCandleClose;
	private bool _ordersPlaced;
	private bool _needsSessionReset;
	private bool _tradeTaken;

	/// <summary>
	/// Initializes a new instance of the <see cref="ChannelEaLimitsStrategy"/> class.
	/// </summary>
	public ChannelEaLimitsStrategy()
	{
		_beginHour = Param(nameof(BeginHour), 1)
			.SetDisplay("Begin Hour", "Hour when session tracking starts (0-23)", "Session")
			.SetRange(0, 23);

		_endHour = Param(nameof(EndHour), 10)
			.SetDisplay("End Hour", "Hour when limit orders are placed (0-23)", "Session")
			.SetRange(0, 23);

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetDisplay("Order Volume", "Volume for each limit order", "Trading")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to build the session channel", "General");
	}

	/// <summary>
	/// Hour when session tracking starts.
	/// </summary>
	public int BeginHour
	{
		get => _beginHour.Value;
		set => _beginHour.Value = value;
	}

	/// <summary>
	/// Hour when the strategy places new pending orders.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Volume per limit order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Working candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_sessionStart = DateTimeOffset.MinValue;
		_sessionEnd = DateTimeOffset.MinValue;
		_sessionHigh = decimal.MinValue;
		_sessionLow = decimal.MaxValue;
		_barsInSession = 0;
		_prevCandleClose = null;
		_ordersPlaced = false;
		_needsSessionReset = false;
		_tradeTaken = false;
	}

	/// <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)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var closeTime = candle.CloseTime;
		var sessionStart = CalculateSessionStart(closeTime);

		if (_sessionStart != sessionStart)
		{
			_sessionStart = sessionStart;
			_sessionEnd = CalculateSessionEnd(_sessionStart);
			ResetSessionState();
		}

		if (_needsSessionReset)
		{
			// Close any open position at session reset
			if (Position > 0)
				SellMarket(Position);
			else if (Position < 0)
				BuyMarket(-Position);
			_needsSessionReset = false;
		}

		if (candle.OpenTime >= _sessionStart && candle.OpenTime < _sessionEnd)
		{
			var high = candle.HighPrice;
			var low = candle.LowPrice;

			if (_sessionHigh == decimal.MinValue || high > _sessionHigh)
				_sessionHigh = high;

			if (_sessionLow == decimal.MaxValue || low < _sessionLow)
				_sessionLow = low;

			_barsInSession++;
		}

		// After session ends, trade breakouts of the channel
		if (_ordersPlaced && !_tradeTaken && _barsInSession >= 2 && _sessionLow < _sessionHigh)
		{
			if (Position == 0)
			{
				// Buy when price touches session low, sell when it touches session high
				if (candle.LowPrice <= _sessionLow)
				{
					BuyMarket(OrderVolume);
					_tradeTaken = true;
				}
				else if (candle.HighPrice >= _sessionHigh)
				{
					SellMarket(OrderVolume);
					_tradeTaken = true;
				}
			}
		}

		if (!_ordersPlaced && _prevCandleClose.HasValue)
		{
			var previousClose = _prevCandleClose.Value;

			if (previousClose < _sessionEnd && closeTime >= _sessionEnd)
			{
				if (_barsInSession >= 2 && _sessionLow < _sessionHigh)
				{
					_ordersPlaced = true;
				}
			}
		}

		_prevCandleClose = closeTime;
	}

	private void ResetSessionState()
	{
		_sessionHigh = decimal.MinValue;
		_sessionLow = decimal.MaxValue;
		_barsInSession = 0;
		_ordersPlaced = false;
		_needsSessionReset = true;
		_tradeTaken = false;
	}

	private DateTimeOffset CalculateSessionStart(DateTimeOffset time)
	{
		var offset = time.Offset;
		var day = new DateTimeOffset(time.Date, offset);
		var start = day.AddHours(BeginHour);
		var startHour = TimeSpan.FromHours(BeginHour);

		if (BeginHour <= EndHour)
		{
			if (time < start)
				start = start.AddDays(-1);
		}
		else
		{
			if (time.TimeOfDay < startHour)
				start = start.AddDays(-1);
		}

		return start;
	}

	private DateTimeOffset CalculateSessionEnd(DateTimeOffset sessionStart)
	{
		var offset = sessionStart.Offset;
		var day = new DateTimeOffset(sessionStart.Date, offset);
		var end = day.AddHours(EndHour);

		if (EndHour <= BeginHour || end <= sessionStart)
			end = end.AddDays(1);

		return end;
	}
}