PriceChannel Signal v2 — трендовая стратегия на основе модифицированного канала Дончиана. Оригинальный советник MQL5 отслеживает смену тренда по каналу, дополнительные сигналы на повторный вход при повторном пробое диапазона и защитные уровни выхода, построенные из того же диапазона. Порт на StockSharp полностью сохраняет логику: работает с одной позицией, анализирует только завершённые свечи и при необходимости ограничивает торговлю временным окном.
Логика работы
Верхняя и нижняя границы канала Дончиана рассчитываются на интервале ChannelPeriod.
Полученный диапазон смещается двумя коэффициентами:
Risk Factor — сдвигает входные полосы к центру диапазона.
Exit Level — формирует внутренние защитные полосы для выходов.
Поддерживается состояние тренда:
Закрытие выше верхней входной полосы переводит тренд в бычий режим.
Закрытие ниже нижней входной полосы переводит тренд в медвежий режим.
Если пробоя нет, сохраняется предыдущее состояние.
Сигналы:
Покупка — переход тренда из медвежьего в бычий.
Продажа — переход тренда из бычьего в медвежий.
Повторный вход вверх — при включённой опции цена закрывается выше верхней полосы, когда тренд уже бычий.
Повторный вход вниз — при включённой опции цена закрывается ниже нижней полосы, когда тренд уже медвежий.
Выход из лонга — при включённой опции закрытие опускается ниже защитной полосы после нахождения выше неё на предыдущей свече.
Выход из шорта — при включённой опции закрытие поднимается выше защитной полосы после нахождения ниже неё на предыдущей свече.
На одну свечу допускается не более одной заявки в каждом направлении. Новая позиция не открывается, если предыдущая ещё активна.
При включённом временном фильтре все сигналы игнорируются вне заданного торгового интервала.
Параметры
Параметр
Описание
ChannelPeriod
Длина истории для канала Дончиана и защитных полос.
RiskFactor
Сдвиг входных полос (0–10). Меньшие значения расширяют диапазон, большие — сужают.
ExitLevel
Сдвиг защитных полос. Должен быть больше RiskFactor, чтобы располагаться внутри диапазона входа.
UseReEntry
Включает повторные входы при пробое активной полосы.
UseExitSignals
Включает выходы по внутренним защитным полосам.
CandleType
Тип свечей, на которых строятся расчёты.
UseTimeControl
Переключатель ограничения по времени.
StartHour / StartMinute
Начало торгового окна (включительно) при включённом фильтре.
EndHour / EndMinute
Конец торгового окна (исключительно) при включённом фильтре.
Правила входа и выхода
Вход в лонг: тренд стал бычьим или выполнено условие повторного входа, позиция отсутствует, свеча внутри торгового окна.
Вход в шорт: тренд стал медвежьим или выполнено условие повторного входа вниз, позиция отсутствует, свеча внутри торгового окна.
Выход из лонга: включены защитные сигналы и закрытие опустилось ниже защитной полосы после нахождения выше неё на предыдущей свече.
Выход из шорта: включены защитные сигналы и закрытие поднялось выше защитной полосы после нахождения ниже неё на предыдущей свече.
Дополнительные замечания
Стратегия использует рыночные заявки и не наращивает объём позиции.
Обработка выполняется только по завершённым свечам, что исключает перерисовку внутри бара.
Объём по умолчанию — 1 контракт, если не задан пользователем.
Временной фильтр повторяет поведение оригинала: конечное время задаётся как исключительное, допускается переход через полночь.
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()