GitHub で見る

PriceChannel Signal v2 Strategy

Overview

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

  1. Donchian channel high and low are calculated over the configured ChannelPeriod.
  2. 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.
  3. 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.
  4. 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.
  5. Only one order per bar and per direction is allowed. The strategy refuses to open a new position if another one is already active.
  6. 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();
	}
}