Auf GitHub ansehen

Channel EA Limits Strategy

Overview

  • Origin: converted from the MetaTrader 5 expert ChannelEA1.mq5.
  • Purpose: monitor an intraday price channel between two user-defined hours and queue limit orders at the end of that window.
  • Approach: the strategy keeps track of the highest and lowest prices observed during the session and places symmetric limit orders to trade potential reversals back toward the opposite side of the channel.

The strategy is suitable for symbols that exhibit mean reversion once a daily range is established. By design it works on netting accounts: a filled sell limit order will close an existing long before opening a new short and vice versa.

Parameters

Name Default Description
BeginHour 1 Hour (0-23) when the intraday range tracking starts. The strategy cancels outstanding orders and closes positions at this time.
EndHour 10 Hour (0-23) when the accumulated range is evaluated and new limit orders are placed. Supports overnight sessions: if BeginHour > EndHour, the session spans midnight.
OrderVolume 1 Volume applied to every pending order.
CandleType 1 hour time frame Candle series used to build the channel. You can switch to any time frame supported by StockSharp.

Trading Logic

  1. Session handling
    • The strategy derives the session start and end timestamps from the BeginHour and EndHour parameters using the candle timestamps. When BeginHour > EndHour the end is moved to the next day.
    • At the first finished candle whose close time reaches the start boundary, the strategy cancels all active orders, closes the open position, and resets the session statistics.
  2. Channel construction
    • Only candles whose open time lies inside the session window contribute to the range. The strategy keeps the running maximum high and minimum low for the session and counts the number of contributing candles.
    • At least two finished candles are required to form a valid range, mirroring the behaviour of the original MQL5 expert (n > 2 condition).
  3. Order placement at session end
    • When a finished candle crosses the end boundary, the strategy checks that the range has been formed and that the low is strictly below the high.
    • It then places two pending orders:
      • BuyLimit at the recorded session low with OrderVolume volume.
      • SellLimit at the recorded session high with the same volume.
    • Orders stay active until the next session starts. Because the strategy runs on a netting account, these orders serve both as entries and exits: for example, the SellLimit closes an existing long at the session high before establishing a new short.
  4. Next session preparation
    • On the next start boundary the strategy closes any remaining position and removes leftover pending orders before measuring the new channel.

Additional Notes

  • No explicit stop-loss is set. Risk management must be controlled through position sizing, manual overrides, or external protective logic.
  • The logic uses finished candles only (CandleStates.Finished) to stay aligned with the original EA behaviour.
  • Ensure that the data feed and server time zone match your expectations, because session boundaries are evaluated in exchange/local time.
  • When optimising, consider both the trading hours and the candle duration; the strategy is sensitive to the combination because the recorded range depends on the selected time frame.
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;
	}
}