Открыть на GitHub

Стратегия Flat Channel

Flat Channel Strategy — перенос эксперта MetaTrader 5 Flat Channel (barabashkakvn's edition) на высокоуровневый API StockSharp. Алгоритм полностью сохраняет оригинальную логику: сглаженное стандартное отклонение ищет периоды затухающей волатильности, экстремумы внутри диапазона формируют горизонтальный канал, а чуть выше/ниже его границ выставляются отложенные стоп-заявки. При пробое стратегия входит в направление импульса, получает заранее рассчитанные уровни стоп-лосса и тейк-профита и при необходимости сопровождает позицию трейлинг-стопом.

Как работает стратегия

  1. Поиск флэта по волатильности. Индикатор StandardDeviation с периодом StdDevPeriod дополнительно сглаживается скользящей средней длиной SmoothingLength. Если сглаженное значение FlatBars подряд не увеличивается, считается, что рынок вошёл во флэт, и разрешается постановка новых отложенных ордеров.
  2. Построение канала. После подтверждения флэта берутся максимум и минимум последних max(ChannelLookback, FlatBars + 1) свечей через стандартные индикаторы Highest/Lowest. Высота канала фильтруется по порогам ChannelMinPips и ChannelMaxPips (величины в пипсах переводятся в цену через параметр PipSize либо автоматически по шагу цены инструмента).
  3. Размещение заявок. Пока позиция отсутствует и разрешено торговать, стратегия ставит buy stop по цене high + IndentPips и sell stop по цене low − IndentPips. При постановке запоминаются соответствующие уровни защиты.
  4. Сопровождение пробоя. После срабатывания одной заявки противоположная отменяется. Цена исполнения становится опорной точкой для трейлинг-стопа, а запомненные стоп-лосс и тейк-профит активируются.
  5. Контроль позиции. На каждой завершённой свече проверяется достижение стоп-лосса или тейк-профита; при срабатывании отправляется рыночный выход. Если TrailingStopPips больше нуля, стоп переносится вслед за ценой при наборе прибыли не менее TrailingStopPips + TrailingStepPips.
  6. Торговые часы. При включённом 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;
	}
}