Ver en GitHub

Estrategia ChannelEA2

Descripción general

La estrategia ChannelEA2 replica el experto MetaTrader "ChannelEA2" en StockSharp. La estrategia construye un canal de precios intradía entre las horas de inicio y fin de sesión configuradas. Cuando la sesión termina, coloca órdenes stop por encima del máximo del canal y por debajo del mínimo del canal. Cada orden stop lleva un stop loss protector definido por el borde opuesto del canal. El enfoque tiene como objetivo capturar rupturas después de un período de consolidación durante la ventana de sesión.

Lógica de trading

  • En la primera vela finalizada cuyo tiempo de apertura cruza el BeginHour, la estrategia reinicia la sesión.
    • Todas las posiciones abiertas se cierran con órdenes de mercado.
    • Cualquier orden activa, incluidas las anteriores entradas stop o stops de protección, se cancela.
    • Los máximos y mínimos de la sesión se inicializan usando la primera vela dentro de la nueva sesión.
  • Durante la sesión (desde BeginHour hasta EndHour), el máximo y mínimo de cada vela finalizada actualiza los límites del canal.
  • En la primera vela que se abre después de que la sesión ha terminado (EndHour), la estrategia calcula:
    • Una orden de compra stop en el máximo de sesión registrado más un buffer opcional medido en pasos de precio.
    • Una orden de venta stop en el mínimo de sesión registrado menos el mismo buffer.
    • El stop loss para la orden de compra es el mínimo de sesión, mientras que el stop loss para la orden de venta es el máximo de sesión.
  • Si se abre una posición, se cancela la orden de entrada opuesta y se registra un stop protector en el mercado usando el nivel de stop almacenado.
  • Las órdenes permanecen activas hasta el inicio de la siguiente sesión, cuando todo se reinicia de nuevo.

Parámetros

Nombre Descripción Predeterminado
BeginHour Hora (0-23) cuando la sesión se reinicia y el canal comienza a recopilar datos. 1
EndHour Hora (0-23) cuando las órdenes stop están programadas. Admite sesiones nocturnas cuando BeginHour > EndHour. 10
TradeVolume Volumen usado para cada orden de entrada. 1
CandleType Serie de velas usada para construir el canal (predeterminado velas de 1 hora). 1 hora
StopBufferMultiplier Multiplicador del paso de precio del instrumento usado como buffer de seguridad para la activación de entrada y stops de protección. 2

Gestión de riesgo

  • La estrategia llama automáticamente a StartProtection() para que StockSharp gestione posiciones inesperadas.
  • Las órdenes stop de protección se envían inmediatamente después de que aparece una posición. Se cancelan cuando la posición vuelve a cero.
  • Los precios de stop se desplazan por StopBufferMultiplier * PriceStep para evitar violar los límites de distancia de stop de la bolsa.

Notas adicionales

  • El rango del canal se congela una vez que se generan las órdenes stop; las velas posteriores no afectan los niveles de entrada hasta que comienza la siguiente sesión.
  • Si el instrumento no tiene PriceStep definido, el buffer se ignora y las órdenes se colocan en los niveles exactos del canal.
  • Los valores de volumen son decimales, permitiendo contratos o lotes fraccionarios cuando el broker lo soporta.
  • La estrategia dibuja velas y operaciones ejecutadas en el área del gráfico para seguimiento visual.
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>
/// Breakout strategy that places stop orders at the extremes of the intraday channel.
/// </summary>
public class ChannelEa2Strategy : Strategy
{
	private readonly StrategyParam<int> _beginHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _stopBufferMultiplier;

	private decimal? _sessionHigh;
	private decimal? _sessionLow;
	private bool _channelReady;
	private decimal? _entryPrice;
	private decimal? _stopLossPrice;

	/// <summary>
	/// Trading session start hour.
	/// </summary>
	public int BeginHour
	{
		get => _beginHour.Value;
		set => _beginHour.Value = value;
	}

	/// <summary>
	/// Trading session end hour.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Order volume.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Candle type used for channel detection.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Number of price steps added as a buffer to entry and protective orders.
	/// </summary>
	public decimal StopBufferMultiplier
	{
		get => _stopBufferMultiplier.Value;
		set => _stopBufferMultiplier.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ChannelEa2Strategy"/> class.
	/// </summary>
	public ChannelEa2Strategy()
	{
		_beginHour = Param(nameof(BeginHour), 1)
			.SetDisplay("Begin Hour", "Hour when the session resets", "Trading")
			
			.SetOptimize(0, 23, 1);

		_endHour = Param(nameof(EndHour), 10)
			.SetDisplay("End Hour", "Hour when breakout orders are scheduled", "Trading")
			
			.SetOptimize(0, 23, 1);

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetDisplay("Volume", "Order volume", "Trading")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for the channel", "General");

		_stopBufferMultiplier = Param(nameof(StopBufferMultiplier), 2m)
			.SetDisplay("Stop Buffer", "Price step multiplier for safety offsets", "Risk")
			.SetNotNegative();
	}

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

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

		_sessionHigh = null;
		_sessionLow = null;
		_channelReady = false;
		_entryPrice = null;
		_stopLossPrice = null;
	}

	/// <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 hour = candle.OpenTime.Hour;

		// During channel-building hours, accumulate the range.
		if (hour >= BeginHour && hour < EndHour)
		{
			if (_sessionHigh is null || candle.HighPrice > _sessionHigh)
				_sessionHigh = candle.HighPrice;
			if (_sessionLow is null || candle.LowPrice < _sessionLow)
				_sessionLow = candle.LowPrice;
			_channelReady = true;
			return;
		}

		// Outside the channel window, attempt breakout entries.
		if (!_channelReady || _sessionHigh is not decimal high || _sessionLow is not decimal low || high <= low)
			return;

		var buffer = GetPriceBuffer();

		// Manage existing positions.
		if (Position > 0)
		{
			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(Position);
				_entryPrice = null;
				_stopLossPrice = null;
			}
			return;
		}
		else if (Position < 0)
		{
			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				_entryPrice = null;
				_stopLossPrice = null;
			}
			return;
		}

		// Long breakout: price exceeds channel high.
		if (candle.HighPrice > high + buffer)
		{
			BuyMarket(TradeVolume > 0 ? TradeVolume : Volume);
			_entryPrice = candle.ClosePrice;
			_stopLossPrice = low - buffer;
			// Reset channel for next session.
			_sessionHigh = null;
			_sessionLow = null;
			_channelReady = false;
			return;
		}

		// Short breakout: price drops below channel low.
		if (candle.LowPrice < low - buffer)
		{
			SellMarket(TradeVolume > 0 ? TradeVolume : Volume);
			_entryPrice = candle.ClosePrice;
			_stopLossPrice = high + buffer;
			_sessionHigh = null;
			_sessionLow = null;
			_channelReady = false;
		}
	}

	private decimal GetPriceBuffer()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m || StopBufferMultiplier <= 0m)
			return 0m;

		return step * StopBufferMultiplier;
	}
}