Ver en GitHub

ChannelEA2 Strategy

Overview

ChannelEA2 Strategy replicates the MetaTrader expert "ChannelEA2" in StockSharp. The strategy builds an intraday price channel between the configured session start and end hours. When the session ends, it places stop orders above the channel high and below the channel low. Each stop order carries a protective stop loss defined by the opposite edge of the channel. The approach aims to capture breakouts after a period of consolidation during the session window.

Trading Logic

  • At the first finished candle whose open time crosses the BeginHour, the strategy resets the session.
    • All open positions are closed with market orders.
    • Any active orders, including previous stop entries or protection stops, are cancelled.
    • Session high and low are initialised using the first candle inside the new session.
  • During the session (from BeginHour until EndHour), the high and low of each finished candle update the channel boundaries.
  • On the first candle that opens after the session has ended (EndHour), the strategy calculates:
    • A buy stop order at the recorded session high plus an optional buffer measured in price steps.
    • A sell stop order at the recorded session low minus the same buffer.
    • The stop loss for the buy order is the session low, while the stop loss for the sell order is the session high.
  • If a position opens, the opposite entry order is cancelled and a protective stop is registered in the market using the stored stop level.
  • Orders remain active until the next session start, when everything is reset again.

Parameters

Name Description Default
BeginHour Hour (0-23) when the session resets and the channel starts collecting data. 1
EndHour Hour (0-23) when stop orders are scheduled. Supports overnight sessions when BeginHour > EndHour. 10
TradeVolume Volume used for each entry order. 1
CandleType Candle series used to build the channel (defaults to 1-hour candles). 1 hour
StopBufferMultiplier Multiplier of the instrument price step used as a safety buffer for entry activation and protective stops. 2

Risk Management

  • The strategy automatically calls StartProtection() to let StockSharp manage unexpected positions.
  • Protective stop orders are submitted immediately after a position appears. They are cancelled when the position returns to zero.
  • Stop prices are offset by StopBufferMultiplier * PriceStep to avoid violating exchange stop distance limits.

Additional Notes

  • The channel range freezes once the stop orders are generated; later candles do not affect the entry levels until the next session starts.
  • If the instrument has no PriceStep defined, the buffer is ignored and orders are placed at the exact channel levels.
  • Volume values are decimals, allowing fractional contracts or lots when supported by the broker.
  • The strategy draws candles and executed trades on the chart area for visual tracking.
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;
	}
}