Открыть на GitHub

Стратегия Flat Channel (2684)

Стратегия представляет собой перенос на C# советника MetaTrader 5 Flat Channel (редакция barabashkakvn). Алгоритм выявляет периоды затухающей волатильности по индикатору Standard Deviation и строит ценовой коридор. При прорыве границ коридора выставленные отложенные ордера Buy Stop или Sell Stop активируются, а противоположный ордер снимается, чтобы не попасть в ловушку обеих сторон рынка.

Основная логика

  1. Фильтр волатильности. Для каждой свечи рассчитывается стандартное отклонение медианной цены. Плоский участок считается сформированным, если значение индикатора уменьшается как минимум FlatBars свечей подряд.
  2. Построение канала. После подтверждения флэта фиксируются максимумы и минимумы диапазона. Ширина канала должна оставаться между порогами ChannelMinPips и ChannelMaxPips (значения пересчитываются из пунктов в цену с учётом PriceStep).
  3. Выставление ордеров. Пока цена остаётся внутри канала, создаются:
    • Buy Stop на верхней границе с стоп-лоссом на расстоянии 2 × ширина канала ниже и тейк-профитом на 1 × ширина выше.
    • Sell Stop на нижней границе с зеркальными уровнями стоп-лосса и тейк-профита.
  4. Время жизни ордеров. Периметр OrderLifetimeSeconds задаёт срок действия отложенных стоп-ордеров. По его истечении активные заявки снимаются и могут быть выставлены заново, если условия флэта сохраняются.
  5. Управление позицией. После срабатывания стоп-ордера противоположная заявка отменяется, а для текущей позиции регистрируются защитные стоп-лосс и тейк-профит. При включённом параметре UseBreakeven стоп-лосс переносится в точку входа, когда цена проходит долю FiboTrail от расстояния до тейк-профита (Fibonacci trailing).
  6. Торговый календарь. Флаг UseTradingHours ограничивает торговлю по дням недели и по времени начала/окончания сессии в понедельник/пятницу, полностью повторяя логику оригинального советника.

Используемые индикаторы

  • StandardDeviation по медианной цене (период = StdDevPeriod) — фиксирует снижение волатильности.
  • DonchianChannels (период = FlatBars) — даёт начальные значения верхней и нижней границ канала.

Риск-менеджмент

  • Параметр FixedVolume задаёт базовый объём сделки при отключённом управлении капиталом.
  • Если UseMoneyManagement = true, объём рассчитывается исходя из RiskPercent капитала и денежного эквивалента расстояния до стоп-лосса (PriceStep × StepPrice).
  • После убыточной сделки следующий вход выполняется объёмом FixedVolume × 4, что соответствует механизму восстановления в оригинальном коде.

Параметры

Параметр Назначение
UseTradingHours Включить/выключить торговый календарь.
TradeTuesday, TradeWednesday, TradeThursday Разрешение торговли во вторник, среду и четверг.
MondayStartHour, FridayStopHour Часы начала торговли в понедельник и окончания в пятницу (формат 0–23).
UseMoneyManagement, RiskPercent, FixedVolume Опции управления капиталом.
OrderLifetimeSeconds Время жизни отложенных ордеров (0 = без ограничения).
StdDevPeriod, FlatBars Настройки индикаторов для определения флэта.
ChannelMinPips, ChannelMaxPips Минимальная и максимальная ширина канала в пунктах.
UseBreakeven, FiboTrail Перевод стоп-лосса в безубыток и коэффициент Fibonacci.
CandleType Тип/таймфрейм свечей для расчётов.

Дополнительные замечания

  • Для корректного перевода пунктов в цену инструмент должен предоставлять PriceStep и StepPrice.
  • Если стандартное отклонение перестаёт снижаться, состояние флэта сбрасывается и все отложенные заявки удаляются.
  • Стоп-лосс и тейк-профит отменяются автоматически при закрытии позиции.

Отказ от ответственности

Материал предоставлен исключительно в образовательных целях и не является торговой рекомендацией. Перед использованием стратегии на реальном рынке протестируйте её на исторических данных и убедитесь в понимании всех рисков.

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Flat channel breakout strategy converted from the MetaTrader 5 version.
/// Detects consolidation via falling standard deviation, then trades breakouts of the channel.
/// </summary>
public class FlatChannelStrategy : Strategy
{
	private readonly StrategyParam<int> _stdDevPeriod;
	private readonly StrategyParam<int> _flatBars;
	private readonly StrategyParam<decimal> _channelMinPips;
	private readonly StrategyParam<decimal> _channelMaxPips;
	private readonly StrategyParam<DataType> _candleType;

	private StandardDeviation _stdDev = null!;
	private DonchianChannels _donchian = null!;

	private decimal _previousStdDev;
	private int _flatBarCount;
	private decimal _channelHigh;
	private decimal _channelLow;

	private decimal? _pendingBuyPrice;
	private decimal? _pendingSellPrice;
	private decimal _entryPrice;
	private decimal _longStop;
	private decimal _longTake;
	private decimal _shortStop;
	private decimal _shortTake;

	/// <summary>
	/// Standard deviation indicator period.
	/// </summary>
	public int StdDevPeriod
	{
		get => _stdDevPeriod.Value;
		set => _stdDevPeriod.Value = value;
	}

	/// <summary>
	/// Minimum number of bars with falling volatility required to form a flat channel.
	/// </summary>
	public int FlatBars
	{
		get => _flatBars.Value;
		set => _flatBars.Value = value;
	}

	/// <summary>
	/// Minimum channel width expressed in pips.
	/// </summary>
	public decimal ChannelMinPips
	{
		get => _channelMinPips.Value;
		set => _channelMinPips.Value = value;
	}

	/// <summary>
	/// Maximum channel width expressed in pips.
	/// </summary>
	public decimal ChannelMaxPips
	{
		get => _channelMaxPips.Value;
		set => _channelMaxPips.Value = value;
	}

	/// <summary>
	/// Candle type to analyse.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public FlatChannelStrategy()
	{
		_stdDevPeriod = Param(nameof(StdDevPeriod), 37)
			.SetDisplay("StdDev Period", "Standard deviation indicator period", "Indicators")
			.SetGreaterThanZero();

		_flatBars = Param(nameof(FlatBars), 2)
			.SetDisplay("Flat Bars", "Minimum bars in flat state", "Indicators")
			.SetGreaterThanZero();

		_channelMinPips = Param(nameof(ChannelMinPips), 10m)
			.SetDisplay("Channel Min Pips", "Minimum channel width in pips", "Indicators")
			.SetGreaterThanZero();

		_channelMaxPips = Param(nameof(ChannelMaxPips), 100000m)
			.SetDisplay("Channel Max Pips", "Maximum channel width in pips", "Indicators")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle type", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_previousStdDev = 0m;
		_flatBarCount = 0;
		_channelHigh = 0m;
		_channelLow = 0m;
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
		_entryPrice = 0m;
		_longStop = 0m;
		_longTake = 0m;
		_shortStop = 0m;
		_shortTake = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_stdDev = new StandardDeviation { Length = StdDevPeriod };
		_donchian = new DonchianChannels { Length = FlatBars };

		var subscription = SubscribeCandles(CandleType);

		subscription
			.BindEx(_donchian, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _donchian);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue channelValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var medianPrice = (candle.HighPrice + candle.LowPrice) / 2m;
		var stdDevValue = _stdDev.Process(new DecimalIndicatorValue(_stdDev, medianPrice, candle.CloseTime) { IsFinal = true }).ToDecimal();

		if (!_stdDev.IsFormed || channelValue is not DonchianChannelsValue donchianValue)
		{
			_previousStdDev = stdDevValue;
			return;
		}

		if (donchianValue.UpperBand is not decimal upper || donchianValue.LowerBand is not decimal lower)
		{
			_previousStdDev = stdDevValue;
			return;
		}

		// Update flat state based on StdDev direction.
		UpdateStdDevState(stdDevValue, upper, lower, candle);

		// Check simulated pending entries.
		CheckPendingEntries(candle);

		// Manage existing positions with SL/TP.
		ManagePosition(candle);

		// If flat and no position, set up pending breakout entries.
		if (Position == 0 && _flatBarCount >= FlatBars && _channelHigh > _channelLow)
		{
			var channelWidth = _channelHigh - _channelLow;
			var priceStep = Security?.PriceStep ?? 0.01m;
			if (priceStep <= 0m) priceStep = 0.01m;
			var minWidth = ChannelMinPips * priceStep;
			var maxWidth = ChannelMaxPips * priceStep;

			if (channelWidth >= minWidth && channelWidth <= maxWidth)
			{
				// Set pending breakout entries at channel boundaries.
				_pendingBuyPrice = _channelHigh;
				_pendingSellPrice = _channelLow;
				_longStop = _channelHigh - channelWidth * 2m;
				_longTake = _channelHigh + channelWidth;
				_shortStop = _channelLow + channelWidth * 2m;
				_shortTake = _channelLow - channelWidth;
			}
		}

		_previousStdDev = stdDevValue;
	}

	private void UpdateStdDevState(decimal stdDevValue, decimal upper, decimal lower, ICandleMessage candle)
	{
		if (_previousStdDev == 0m)
		{
			_previousStdDev = stdDevValue;
			return;
		}

		if (stdDevValue < _previousStdDev)
		{
			_flatBarCount++;

			if (_flatBarCount == FlatBars)
			{
				_channelHigh = upper;
				_channelLow = lower;
			}
			else if (_flatBarCount > FlatBars)
			{
				if (candle.HighPrice > _channelHigh)
					_channelHigh = candle.HighPrice;
				if (candle.LowPrice < _channelLow)
					_channelLow = candle.LowPrice;
			}
		}
		else if (stdDevValue > _previousStdDev)
		{
			_flatBarCount = 0;
			_channelHigh = 0m;
			_channelLow = 0m;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
		}
		else if (_flatBarCount >= FlatBars && _channelHigh <= _channelLow)
		{
			_channelHigh = upper;
			_channelLow = lower;
		}
	}

	private void CheckPendingEntries(ICandleMessage candle)
	{
		if (Position != 0)
			return;

		if (_pendingBuyPrice is decimal buyPrice && candle.HighPrice >= buyPrice)
		{
			BuyMarket();
			_entryPrice = buyPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
			return;
		}

		if (_pendingSellPrice is decimal sellPrice && candle.LowPrice <= sellPrice)
		{
			SellMarket();
			_entryPrice = sellPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
		}
	}

	private void ManagePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop > 0m && candle.LowPrice <= _longStop)
			{
				SellMarket();
				ResetPositionState();
				return;
			}
			if (_longTake > 0m && candle.HighPrice >= _longTake)
			{
				SellMarket();
				ResetPositionState();
			}
		}
		else if (Position < 0)
		{
			if (_shortStop > 0m && candle.HighPrice >= _shortStop)
			{
				BuyMarket();
				ResetPositionState();
				return;
			}
			if (_shortTake > 0m && candle.LowPrice <= _shortTake)
			{
				BuyMarket();
				ResetPositionState();
			}
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_longStop = 0m;
		_longTake = 0m;
		_shortStop = 0m;
		_shortTake = 0m;
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
	}
}