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
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.
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).
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.
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan
class channel_ea_limits_strategy(Strategy):
def __init__(self):
super(channel_ea_limits_strategy, self).__init__()
self._begin_hour = self.Param("BeginHour", 1)
self._end_hour = self.Param("EndHour", 10)
self._order_volume = self.Param("OrderVolume", 1.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._session_high = None
self._session_low = None
self._bars_in_session = 0
self._prev_candle_close = None
self._orders_placed = False
self._needs_session_reset = False
self._trade_taken = False
self._session_start = None
self._session_end = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(channel_ea_limits_strategy, self).OnStarted2(time)
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _calc_session_start(self, close_time):
begin = self._begin_hour.Value
end = self._end_hour.Value
day = close_time.Date
start = day.AddHours(begin)
if begin <= end:
if close_time < start:
start = start.AddDays(-1)
else:
if close_time.TimeOfDay.TotalHours < begin:
start = start.AddDays(-1)
return start
def _calc_session_end(self, session_start):
end_hour = self._end_hour.Value
begin_hour = self._begin_hour.Value
day = session_start.Date
end = day.AddHours(end_hour)
if end_hour <= begin_hour or end <= session_start:
end = end.AddDays(1)
return end
def _reset_session(self):
self._session_high = None
self._session_low = None
self._bars_in_session = 0
self._orders_placed = False
self._needs_session_reset = True
self._trade_taken = False
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close_time = candle.CloseTime
session_start = self._calc_session_start(close_time)
if self._session_start is None or self._session_start != session_start:
self._session_start = session_start
self._session_end = self._calc_session_end(session_start)
self._reset_session()
if self._needs_session_reset:
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(abs(self.Position))
self._needs_session_reset = False
open_time = candle.OpenTime
if open_time >= self._session_start and open_time < self._session_end:
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self._session_high is None or h > self._session_high:
self._session_high = h
if self._session_low is None or lo < self._session_low:
self._session_low = lo
self._bars_in_session += 1
if self._orders_placed and not self._trade_taken and self._bars_in_session >= 2:
if self._session_low is not None and self._session_high is not None and self._session_low < self._session_high:
if self.Position == 0:
if float(candle.LowPrice) <= self._session_low:
self.BuyMarket(self._order_volume.Value)
self._trade_taken = True
elif float(candle.HighPrice) >= self._session_high:
self.SellMarket(self._order_volume.Value)
self._trade_taken = True
if not self._orders_placed and self._prev_candle_close is not None:
if self._prev_candle_close < self._session_end and close_time >= self._session_end:
if self._bars_in_session >= 2 and self._session_low is not None and self._session_high is not None and self._session_low < self._session_high:
self._orders_placed = True
self._prev_candle_close = close_time
def OnReseted(self):
super(channel_ea_limits_strategy, self).OnReseted()
self._session_high = None
self._session_low = None
self._bars_in_session = 0
self._prev_candle_close = None
self._orders_placed = False
self._needs_session_reset = False
self._trade_taken = False
self._session_start = None
self._session_end = None
def CreateClone(self):
return channel_ea_limits_strategy()