Flat Channel Strategy — перенос эксперта MetaTrader 5 Flat Channel (barabashkakvn's edition) на высокоуровневый API StockSharp. Алгоритм полностью сохраняет оригинальную логику: сглаженное стандартное отклонение ищет периоды затухающей волатильности, экстремумы внутри диапазона формируют горизонтальный канал, а чуть выше/ниже его границ выставляются отложенные стоп-заявки. При пробое стратегия входит в направление импульса, получает заранее рассчитанные уровни стоп-лосса и тейк-профита и при необходимости сопровождает позицию трейлинг-стопом.
Как работает стратегия
Поиск флэта по волатильности. Индикатор StandardDeviation с периодом StdDevPeriod дополнительно сглаживается скользящей средней длиной SmoothingLength. Если сглаженное значение FlatBars подряд не увеличивается, считается, что рынок вошёл во флэт, и разрешается постановка новых отложенных ордеров.
Построение канала. После подтверждения флэта берутся максимум и минимум последних max(ChannelLookback, FlatBars + 1) свечей через стандартные индикаторы Highest/Lowest. Высота канала фильтруется по порогам ChannelMinPips и ChannelMaxPips (величины в пипсах переводятся в цену через параметр PipSize либо автоматически по шагу цены инструмента).
Размещение заявок. Пока позиция отсутствует и разрешено торговать, стратегия ставит buy stop по цене high + IndentPips и sell stop по цене low − IndentPips. При постановке запоминаются соответствующие уровни защиты.
Сопровождение пробоя. После срабатывания одной заявки противоположная отменяется. Цена исполнения становится опорной точкой для трейлинг-стопа, а запомненные стоп-лосс и тейк-профит активируются.
Контроль позиции. На каждой завершённой свече проверяется достижение стоп-лосса или тейк-профита; при срабатывании отправляется рыночный выход. Если TrailingStopPips больше нуля, стоп переносится вслед за ценой при наборе прибыли не менее TrailingStopPips + TrailingStepPips.
Торговые часы. При включённом UseTradingHours сигналы отрабатываются только между StartHour (включительно) и EndHour (не включительно). Допускается переход через полночь (StartHour > EndHour).
Управление рисками
Динамическая или фиксированная защита. При положительных StopLossPips / TakeProfitPips используются фиксированные расстояния в пипсах. Значение 0 переключает расчёт на динамическую формулу с коэффициентами DynamicStopMultiplier и DynamicTakeMultiplier.
Трейлинг-стоп. Параметр TrailingStopPips включает подтягивание стопа по мере движения цены. Шаг переноса задаётся TrailingStepPips, чтобы избежать лишних перестановок.
Ограничение объёма.MaxPositions задаёт максимум совокупной позиции в долях TradeVolume. Пока суммарный объём достигает лимита, новые отложенные заявки не создаются.
Фильтр направлений. Флаги UseBuy и UseSell позволяют запускать стратегию только на пробой вверх, только на пробой вниз или в обе стороны одновременно.
Параметры
Параметр
Значение по умолчанию
Описание
TradeVolume
1
Объём каждой отложенной заявки.
PipSize
0.0001
Размер пункта в ценовых единицах. Ноль — использовать шаг цены инструмента (с учётом 3/5-знакового котирования).
StdDevPeriod
46
Период базового индикатора стандартного отклонения.
SmoothingLength
3
Длина сглаживающей скользящей средней.
FlatBars
3
Минимальное количество подряд уменьшающихся значений волатильности для разрешения торговли.
ChannelLookback
5
Число свечей для расчёта максимумов и минимумов после обнаружения флэта (сравнивается с FlatBars + 1).
ChannelMinPips
15
Минимальная высота канала в пипсах. 0 отключает нижний порог.
ChannelMaxPips
105
Максимальная высота канала в пипсах. 0 отключает верхний порог.
DynamicStopMultiplier
1
Множитель высоты канала для динамического стоп-лосса (если StopLossPips = 0).
DynamicTakeMultiplier
1
Множитель высоты канала для динамического тейк-профита (если TakeProfitPips = 0).
StopLossPips
0
Фиксированное расстояние до стоп-лосса в пипсах. При положительном значении замещает динамический расчёт.
TakeProfitPips
0
Фиксированное расстояние до тейк-профита в пипсах. При положительном значении замещает динамический расчёт.
IndentPips
0
Дополнительный отступ (в пипсах) от границ канала при размещении заявок.
TrailingStopPips
5
Дистанция трейлинг-стопа в пипсах. 0 отключает сопровождение.
TrailingStepPips
5
Минимальный шаг переноса трейлинг-стопа.
UseBuy
true
Разрешить пробои вверх (buy stop).
UseSell
true
Разрешить пробои вниз (sell stop).
MaxPositions
5
Максимальный совокупный объём в долях TradeVolume.
UseTradingHours
true
Включить фильтр торговых часов.
StartHour
0
Час начала торговой сессии (включительно).
EndHour
23
Час окончания торговой сессии (не включительно).
CandleType
H1
Таймфрейм свечей для расчётов (по умолчанию 1 час).
Примечания
Стратегия работает только по завершённым свечам через высокоуровневый вызов SubscribeCandles().Bind(...), что обеспечивает воспроизводимость как в оригинальном советнике.
Все ценовые уровни нормализуются функцией Security.ShrinkPrice, чтобы соблюдать шаг котирования биржи.
При срабатывании одной отложенной заявки противоположная немедленно снимается, поэтому одновременно может существовать только одна пробойная позиция.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Flat Channel Breakout strategy using EMA crossover.
/// Buys when fast EMA crosses above slow EMA, sells on reverse.
/// </summary>
public class FlatChannelBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _slow;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
private int _cooldown;
public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
public FlatChannelBreakoutStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
protected override void OnReseted()
{
base.OnReseted();
_fast = null; _slow = null;
_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new ExponentialMovingAverage { Length = FastPeriod };
_slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _slow, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished) return;
if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
}
if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }
_prevFast = fastValue; _prevSlow = slowValue;
}
}
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 ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class flat_channel_breakout_strategy(Strategy):
def __init__(self):
super(flat_channel_breakout_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 14) \
.SetDisplay("Fast Period", "Fast MA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 50) \
.SetDisplay("Slow Period", "Slow MA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit in price steps", "Risk")
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(flat_channel_breakout_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(flat_channel_breakout_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._slow.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._prev_fast <= self._prev_slow and fast_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 100
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return flat_channel_breakout_strategy()