Trade Channel — это стратегия возврата в диапазон, портированная с советника MetaTrader «TradeChannel». Она строит ценовой канал по максимумам и минимумам последних завершённых свечей. Когда границы канала перестают обновляться, а цена повторно тестирует одну из них, стратегия открывает позицию в противоположную сторону, ожидая возврата внутрь диапазона.
Ключевые идеи
Используются индикаторы Highest и Lowest для построения канала, похожего на Donchian Channel.
Торговля разрешена только при «плоском» канале, то есть когда не появилось новых максимумов или минимумов.
При касании сопротивления открывается короткая позиция, при касании поддержки — длинная.
Стоп-лосс устанавливается на расстоянии одного ATR от точки пробоя.
При желании стоп можно сопровождать, когда сделка переходит в прибыль.
Параметры
Имя
Описание
Значение по умолчанию
Оптимизация
Volume
Объём сделки в лотах/контрактах.
1
Включена (0.1 → 2.0, шаг 0.1)
ChannelLength
Количество завершённых свечей для расчёта границ канала.
20
Включена (10 → 60, шаг 5)
AtrPeriod
Период ATR для расчёта защитного стопа.
4
Включена (2 → 20, шаг 2)
TrailingPoints
Отступ трейлинг-стопа в шагах цены инструмента. 0 — отключить сопровождение.
30
Включена (0 → 100, шаг 10)
CandleType
Тип и таймфрейм свечей для расчётов.
30-минутные свечи
—
Логика торговли
Подписаться на выбранную свечную серию и передавать данные индикаторам Highest, Lowest и ATR.
Дождаться формирования всех индикаторов. Первые доступные значения лишь инициализируют канал, сделки на этой свече не совершаются.
Для каждой новой завершённой свечи:
Обновить границы канала и рассчитать пивот (сопротивление + поддержка + закрытие) / 3.
Проверить, изменилась ли граница канала по сравнению с прошлой свечой. Плоское сопротивление даёт право искать шорты, плоская поддержка — лонги.
Шорт: сопротивление плоское, а свеча либо касается верхней границы, либо закрывается между пивотом и сопротивлением.
Лонг: поддержка плоская, а свеча либо касается нижней границы, либо закрывается между поддержкой и пивотом.
Одновременно может быть только одна позиция; пока она открыта, новые сигналы игнорируются.
После входа:
Сохранить цену входа.
Установить стоп: для шорта сопротивление + ATR, для лонга поддержка − ATR.
Управление позицией:
Выход из лонга:
Цена касается верхней границы при неизменном сопротивлении;
Минимум свечи пробивает текущий (первоначальный или трейлинг) стоп.
Выход из шорта:
Цена касается нижней границы при неизменной поддержке;
Максимум свечи пробивает текущий стоп.
Трейлинг-стоп (если TrailingPoints > 0):
Перевести параметр в денежные единицы через Security.Step; при отсутствии данных используется сам параметр.
Для лонга, когда закрытие превышает цену входа на величину отступа, перенести стоп на закрытие − отступ.
Для шорта, когда закрытие падает ниже цены входа на величину отступа, опустить стоп на закрытие + отступ.
Стоп двигается только в сторону уменьшения риска и никогда не откатывается назад.
Примечания
Все решения принимаются по завершённым свечам, что соответствует оригинальному MQL-коду, использовавшему High[1], Low[1] и Close[1].
Сравнение текущей и предыдущей границы выполняется с допуском, зависящим от шага цены, чтобы избежать проблем с точностью чисел с плавающей запятой.
Если Security.Step неизвестен, для трейлинга используется исходное значение TrailingPoints.
Отправка писем и динамический расчёт объёма из оригинальной версии не перенесены, так как зависят от инфраструктуры MetaTrader.
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trade Channel Breakout strategy - Highest/Lowest channel breakout.
/// Buys when close crosses above channel midpoint.
/// Sells when close crosses below channel midpoint.
/// </summary>
public class TradeChannelBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _channelPeriod;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevClose;
private decimal _prevMid;
private bool _hasPrev;
public int ChannelPeriod { get => _channelPeriod.Value; set => _channelPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public TradeChannelBreakoutStrategy()
{
_channelPeriod = Param(nameof(ChannelPeriod), 20)
.SetDisplay("Channel Period", "Highest/Lowest lookback", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
protected override void OnReseted() { base.OnReseted(); _prevClose = 0m; _prevMid = 0m; _hasPrev = false; }
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hasPrev = false;
var highest = new Highest { Length = ChannelPeriod };
var lowest = new Lowest { Length = ChannelPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(highest, lowest, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal highest, decimal lowest)
{
if (candle.State != CandleStates.Finished) return;
var close = candle.ClosePrice;
var mid = (highest + lowest) / 2;
if (!_hasPrev) { _prevClose = close; _prevMid = mid; _hasPrev = true; return; }
if (_prevClose <= _prevMid && close > mid && Position <= 0)
{
if (Position < 0) BuyMarket();
BuyMarket();
}
else if (_prevClose >= _prevMid && close < mid && Position >= 0)
{
if (Position > 0) SellMarket();
SellMarket();
}
_prevClose = close; _prevMid = mid;
}
}
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.Indicators import Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class trade_channel_breakout_strategy(Strategy):
def __init__(self):
super(trade_channel_breakout_strategy, self).__init__()
self._channel_period = self.Param("ChannelPeriod", 20).SetDisplay("Channel Period", "Highest/Lowest lookback", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_close = 0.0; self._prev_mid = 0.0; self._has_prev = False
@property
def channel_period(self): return self._channel_period.Value
@property
def candle_type(self): return self._candle_type.Value
def OnReseted(self):
super(trade_channel_breakout_strategy, self).OnReseted()
self._prev_close = 0.0; self._prev_mid = 0.0; self._has_prev = False
def OnStarted2(self, time):
super(trade_channel_breakout_strategy, self).OnStarted2(time)
self._has_prev = False
highest = Highest(); highest.Length = self.channel_period
lowest = Lowest(); lowest.Length = self.channel_period
sub = self.SubscribeCandles(self.candle_type)
sub.Bind(highest, lowest, self.process_candle).Start()
def process_candle(self, candle, highest, lowest):
if candle.State != CandleStates.Finished: return
close = float(candle.ClosePrice); mid = (float(highest) + float(lowest)) / 2.0
if not self._has_prev: self._prev_close = close; self._prev_mid = mid; self._has_prev = True; return
if self._prev_close <= self._prev_mid and close > mid and self.Position <= 0:
if self.Position < 0: self.BuyMarket()
self.BuyMarket()
elif self._prev_close >= self._prev_mid and close < mid and self.Position >= 0:
if self.Position > 0: self.SellMarket()
self.SellMarket()
self._prev_close = close; self._prev_mid = mid
def CreateClone(self): return trade_channel_breakout_strategy()