Ver no GitHub

Estratégia Canal EA com Ordens a Limite

Visão geral

  • Origem: convertida do expert do MetaTrader 5 ChannelEA1.mq5.
  • Propósito: monitorar um canal de preço intradiário entre duas horas definidas pelo usuário e enfileirar ordens a limite ao final dessa janela.
  • Abordagem: a estratégia rastreia os preços mais altos e mais baixos observados durante a sessão e coloca ordens a limite simétricas para negociar possíveis reversões de volta ao lado oposto do canal.

A estratégia é adequada para instrumentos que exibem reversão à média uma vez que um range diário é estabelecido. Por design ela funciona em contas de compensação: uma ordem de venda a limite executada fechará uma posição comprada existente antes de abrir uma nova vendida e vice-versa.

Parâmetros

Nome Padrão Descrição
BeginHour 1 Hora (0-23) quando o rastreamento do range intradiário começa. A estratégia cancela ordens pendentes e fecha posições neste momento.
EndHour 10 Hora (0-23) quando o range acumulado é avaliado e novas ordens a limite são colocadas. Suporta sessões overnight: se BeginHour > EndHour, a sessão abrange a meia-noite.
OrderVolume 1 Volume aplicado a cada ordem pendente.
CandleType Período de 1 hora Série de candles usada para construir o canal. Você pode mudar para qualquer período suportado pelo StockSharp.

Lógica de trading

  1. Tratamento de sessão
    • A estratégia deriva os timestamps de início e fim da sessão dos parâmetros BeginHour e EndHour usando os timestamps dos candles. Quando BeginHour > EndHour, o fim é movido para o dia seguinte.
    • No primeiro candle terminado cujo tempo de fechamento atinge o limite de início, a estratégia cancela todas as ordens ativas, fecha a posição aberta e redefine as estatísticas da sessão.
  2. Construção do canal
    • Apenas candles cujo tempo de abertura está dentro da janela de sessão contribuem para o range. A estratégia mantém o máximo corrente e o mínimo corrente para a sessão e conta o número de candles contribuintes.
    • São necessários pelo menos dois candles terminados para formar um range válido, refletindo o comportamento do expert MQL5 original (condição n > 2).
  3. Colocação de ordens ao final da sessão
    • Quando um candle terminado cruza o limite de fim, a estratégia verifica que o range foi formado e que a mínima está estritamente abaixo da máxima.
    • Em seguida coloca duas ordens pendentes:
      • BuyLimit na mínima de sessão registrada com volume OrderVolume.
      • SellLimit na máxima de sessão registrada com o mesmo volume.
    • As ordens permanecem ativas até que a próxima sessão comece. Como a estratégia roda em conta de compensação, essas ordens servem tanto como entradas quanto saídas: por exemplo, o SellLimit fecha uma posição comprada existente na máxima de sessão antes de estabelecer uma nova vendida.
  4. Preparação da próxima sessão
    • No próximo limite de início, a estratégia fecha quaisquer posições restantes e remove ordens pendentes sobrantes antes de medir o novo canal.

Notas adicionais

  • Nenhum stop-loss explícito é definido. O gerenciamento de risco deve ser controlado através do dimensionamento de posição, substituições manuais ou lógica protetora externa.
  • A lógica usa apenas candles terminados (CandleStates.Finished) para se manter alinhada com o comportamento do EA original.
  • Certifique-se de que o fuso horário do feed de dados e do servidor corresponde às suas expectativas, porque os limites de sessão são avaliados no horário da bolsa/local.
  • Ao otimizar, considere tanto as horas de trading quanto a duração do candle; a estratégia é sensível à combinação porque o range registrado depende do período selecionado.
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;
	}
}