PriceChannel Signal v2 is a trend-following breakout system built around a modified Donchian channel. The original MQL5 expert advisor watches for transitions in the channel trend, optional re-entry conditions when price pushes back through the bands, and protective exit levels derived from the same range. The StockSharp port keeps the original behaviour: it trades a single position at a time, reacts only on completed candles and can be restricted to an intraday window.
Trading logic
Donchian channel high and low are calculated over the configured ChannelPeriod.
The raw range is shifted by two multipliers:
Risk Factor – compresses the entry bands towards the channel median.
Exit Level – builds a second pair of inner bands that trigger exits.
A trend state is maintained:
When the close breaks above the upper entry band the trend becomes bullish.
When the close breaks below the lower entry band the trend becomes bearish.
Otherwise the previous trend is kept.
Signals generated from that state:
Long entry – trend flips from bearish to bullish.
Short entry – trend flips from bullish to bearish.
Long re-entry – optional, price closes back above the upper band while the trend is already bullish.
Short re-entry – optional, price closes back below the lower band while the trend is already bearish.
Long exit – optional, price closes below the bullish exit band after being above it on the previous bar.
Short exit – optional, price closes above the bearish exit band after being below it on the previous bar.
Only one order per bar and per direction is allowed. The strategy refuses to open a new position if another one is already active.
If the intraday time filter is enabled, all of the signals above are ignored outside the configured window.
Parameters
Parameter
Description
ChannelPeriod
Donchian lookback length used to calculate the price channel and exit bands.
RiskFactor
Shift of the entry bands (0–10). Lower values widen the bands, higher values tighten them.
ExitLevel
Shift of the exit bands. Must be larger than RiskFactor to stay inside the entry range.
UseReEntry
Enables re-entry trades when price pushes back through the active band.
UseExitSignals
Enables exit logic based on the inner protective bands.
CandleType
Timeframe used to build candles and run the indicators.
UseTimeControl
Toggles the intraday trading window.
StartHour / StartMinute
Inclusive beginning of the trading window when time control is active.
EndHour / EndMinute
Exclusive end of the trading window when time control is active.
Entry and exit rules
Enter long: trend flips to bullish or re-entry condition fires, current position is flat, and the bar is inside the allowed time window.
Enter short: trend flips to bearish or short re-entry condition fires, current position is flat, and the bar is inside the allowed time window.
Exit long:UseExitSignals is enabled and the close falls below the exit band after being above it on the previous bar.
Exit short:UseExitSignals is enabled and the close rises above the exit band after being below it on the previous bar.
Additional notes
The strategy works with market orders and does not pyramid positions.
Indicator values are processed only on finished candles to avoid intrabar repainting.
Volume defaults to 1 contract if not provided explicitly.
Time control follows the original EA behaviour: the end time is exclusive, and wrapping across midnight is supported.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Price Channel Signal v2 strategy that reacts to Donchian channel breakouts.
/// </summary>
public class PriceChannelSignalV2Strategy : Strategy
{
private readonly StrategyParam<int> _channelPeriod;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _highHistory = new();
private readonly Queue<decimal> _lowHistory = new();
private int _previousTrend;
private decimal? _previousClose;
/// <summary>
/// Channel lookback length.
/// </summary>
public int ChannelPeriod
{
get => _channelPeriod.Value;
set => _channelPeriod.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize a new instance of <see cref="PriceChannelSignalV2Strategy"/>.
/// </summary>
public PriceChannelSignalV2Strategy()
{
_channelPeriod = Param(nameof(ChannelPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Channel Period", "Donchian lookback used for Price Channel", "Price Channel");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for Price Channel", "General");
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_previousTrend = 0;
_previousClose = null;
_highHistory.Clear();
_lowHistory.Clear();
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;
if (_highHistory.Count < ChannelPeriod)
{
EnqueueCandle(candle);
return;
}
var highs = _highHistory.ToArray();
var lows = _lowHistory.ToArray();
var channelHigh = GetMax(highs);
var channelLow = GetMin(lows);
var range = channelHigh - channelLow;
if (range <= 0m)
{
_previousClose = candle.ClosePrice;
EnqueueCandle(candle);
return;
}
var mid = (channelHigh + channelLow) / 2m;
// Update trend state based on channel breakout
var trend = _previousTrend;
if (candle.ClosePrice > channelHigh + range * 0.05m)
trend = 1;
else if (candle.ClosePrice < channelLow - range * 0.05m)
trend = -1;
var volume = Volume;
if (volume <= 0)
volume = 1;
// Trend reversal signals
var changedPosition = false;
if (trend > 0 && _previousTrend <= 0)
{
if (Position <= 0)
{
BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
changedPosition = true;
}
}
else if (trend < 0 && _previousTrend >= 0)
{
if (Position >= 0)
{
SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
changedPosition = true;
}
}
// Exit on mid-line cross
if (!changedPosition && Position > 0 && _previousClose is decimal pc1 && pc1 >= mid && candle.ClosePrice < mid)
{
SellMarket(Math.Abs(Position));
}
else if (!changedPosition && Position < 0 && _previousClose is decimal pc2 && pc2 <= mid && candle.ClosePrice > mid)
{
BuyMarket(Math.Abs(Position));
}
_previousTrend = trend;
_previousClose = candle.ClosePrice;
EnqueueCandle(candle);
}
private void EnqueueCandle(ICandleMessage candle)
{
_highHistory.Enqueue(candle.HighPrice);
_lowHistory.Enqueue(candle.LowPrice);
while (_highHistory.Count > ChannelPeriod)
_highHistory.Dequeue();
while (_lowHistory.Count > ChannelPeriod)
_lowHistory.Dequeue();
}
private static decimal GetMax(IEnumerable<decimal> values)
{
var max = decimal.MinValue;
foreach (var value in values)
{
if (value > max)
max = value;
}
return max;
}
private static decimal GetMin(IEnumerable<decimal> values)
{
var min = decimal.MaxValue;
foreach (var value in values)
{
if (value < min)
min = value;
}
return min;
}
/// <inheritdoc />
protected override void OnReseted()
{
_previousTrend = 0;
_previousClose = null;
_highHistory.Clear();
_lowHistory.Clear();
base.OnReseted();
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class price_channel_signal_v2_strategy(Strategy):
def __init__(self):
super(price_channel_signal_v2_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60)))
self._channel_period = self.Param("ChannelPeriod", 20)
self._high_history = []
self._low_history = []
self._prev_trend = 0
self._prev_close = 0.0
self._has_prev_close = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def ChannelPeriod(self):
return self._channel_period.Value
@ChannelPeriod.setter
def ChannelPeriod(self, value):
self._channel_period.Value = value
def OnReseted(self):
super(price_channel_signal_v2_strategy, self).OnReseted()
self._high_history = []
self._low_history = []
self._prev_trend = 0
self._prev_close = 0.0
self._has_prev_close = False
def OnStarted2(self, time):
super(price_channel_signal_v2_strategy, self).OnStarted2(time)
self._high_history = []
self._low_history = []
self._prev_trend = 0
self._prev_close = 0.0
self._has_prev_close = False
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
period = self.ChannelPeriod
if len(self._high_history) < period:
self._high_history.append(high)
self._low_history.append(low)
self._prev_close = close
self._has_prev_close = True
return
channel_high = max(self._high_history)
channel_low = min(self._low_history)
ch_range = channel_high - channel_low
if ch_range <= 0:
self._prev_close = close
self._high_history.append(high)
self._low_history.append(low)
while len(self._high_history) > period:
self._high_history.pop(0)
while len(self._low_history) > period:
self._low_history.pop(0)
return
mid = (channel_high + channel_low) / 2.0
trend = self._prev_trend
if close > channel_high + ch_range * 0.05:
trend = 1
elif close < channel_low - ch_range * 0.05:
trend = -1
changed_position = False
if trend > 0 and self._prev_trend <= 0:
if self.Position <= 0:
self.BuyMarket()
changed_position = True
elif trend < 0 and self._prev_trend >= 0:
if self.Position >= 0:
self.SellMarket()
changed_position = True
# Exit on mid-line cross
if not changed_position and self._has_prev_close:
if self.Position > 0 and self._prev_close >= mid and close < mid:
self.SellMarket()
elif self.Position < 0 and self._prev_close <= mid and close > mid:
self.BuyMarket()
self._prev_trend = trend
self._prev_close = close
self._has_prev_close = True
self._high_history.append(high)
self._low_history.append(low)
while len(self._high_history) > period:
self._high_history.pop(0)
while len(self._low_history) > period:
self._low_history.pop(0)
def CreateClone(self):
return price_channel_signal_v2_strategy()