Открыть на GitHub

Стратегия Channel EA Limits

Общее описание

  • Происхождение: конвертация эксперта MetaTrader 5 ChannelEA1.mq5.
  • Задача: отслеживать внутридневной ценовой канал между двумя заданными часами и выставлять лимитные заявки на границах канала по завершении сессии.
  • Подход: стратегия фиксирует экстремальные значения (максимум и минимум) за выбранное торговое окно и размещает симметричные лимитные ордера, рассчитывая на возврат цены к противоположной границе диапазона.

Подходит для инструментов с выраженной среднерыночной динамикой внутри дня. Работает в режиме неттинга: исполненный SellLimit закроет текущую длинную позицию до открытия новой короткой и наоборот.

Параметры

Параметр Значение по умолчанию Описание
BeginHour 1 Час (0-23), когда начинается построение диапазона. В этот момент стратегия снимает все активные заявки и закрывает позицию.
EndHour 10 Час (0-23), когда диапазон фиксируется и выставляются новые лимитные заявки. Поддерживается ночная сессия: если BeginHour > EndHour, диапазон пересекает полуночь.
OrderVolume 1 Объём для каждой лимитной заявки.
CandleType таймфрейм 1 час Тип свечей, используемых для построения канала. Можно выбрать любой доступный таймфрейм StockSharp.

Логика торговли

  1. Управление сессией
    • Временные границы рассчитываются по параметрам BeginHour и EndHour на основе времени свечей. Если BeginHour > EndHour, конец сессии переносится на следующий день.
    • При первой завершённой свече, время закрытия которой достигает стартовой границы, стратегия отменяет все активные заявки, закрывает текущую позицию и обнуляет статистику диапазона.
  2. Построение канала
    • В расчёт попадают только свечи, чьё время открытия лежит внутри сессии. Поддерживаются накопительные значения максимума и минимума, а также счётчик свечей.
    • Для формирования канала требуется минимум две завершённые свечи — как и в оригинальном советнике (условие n > 2).
  3. Выставление заявок в конце сессии
    • Когда завершённая свеча пересекает конечную границу, стратегия проверяет, что диапазон сформирован и минимум строго меньше максимума.
    • Затем размещаются две лимитные заявки:
      • BuyLimit по зафиксированному минимуму с объёмом OrderVolume.
      • SellLimit по зафиксированному максимуму с тем же объёмом.
    • Заявки остаются активными до следующего открытия сессии. В режиме неттинга они работают как точки входа и выхода: SellLimit закроет открытую длинную позицию на уровне максимума, прежде чем открыть новую короткую.
  4. Подготовка к следующей сессии
    • В момент следующей стартовой границы стратегия закрывает все позиции, отменяет оставшиеся заявки и начинает построение нового диапазона.

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

  • Стоп-лосс не устанавливается. Управляйте рисками через объём позиции, внешнюю защиту или ручное сопровождение.
  • Используются только завершённые свечи (CandleStates.Finished), чтобы повторить поведение оригинального советника.
  • Важно учитывать часовую зону источника котировок: границы сессии рассчитываются во времени биржи/поставщика данных.
  • При оптимизации одновременно анализируйте часы работы и выбранный таймфрейм — ширина диапазона напрямую зависит от длительности свечей.
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>
/// Channel trading strategy that places limit orders at the end of the monitored session.
/// </summary>
public class ChannelEaLimitsStrategy : Strategy
{
	private readonly StrategyParam<int> _beginHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private DateTimeOffset _sessionStart;
	private DateTimeOffset _sessionEnd;
	private decimal _sessionHigh;
	private decimal _sessionLow;
	private int _barsInSession;
	private DateTimeOffset? _prevCandleClose;
	private bool _ordersPlaced;
	private bool _needsSessionReset;
	private bool _tradeTaken;

	/// <summary>
	/// Initializes a new instance of the <see cref="ChannelEaLimitsStrategy"/> class.
	/// </summary>
	public ChannelEaLimitsStrategy()
	{
		_beginHour = Param(nameof(BeginHour), 1)
			.SetDisplay("Begin Hour", "Hour when session tracking starts (0-23)", "Session")
			.SetRange(0, 23);

		_endHour = Param(nameof(EndHour), 10)
			.SetDisplay("End Hour", "Hour when limit orders are placed (0-23)", "Session")
			.SetRange(0, 23);

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetDisplay("Order Volume", "Volume for each limit order", "Trading")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to build the session channel", "General");
	}

	/// <summary>
	/// Hour when session tracking starts.
	/// </summary>
	public int BeginHour
	{
		get => _beginHour.Value;
		set => _beginHour.Value = value;
	}

	/// <summary>
	/// Hour when the strategy places new pending orders.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Volume per limit order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Working candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_sessionStart = DateTimeOffset.MinValue;
		_sessionEnd = DateTimeOffset.MinValue;
		_sessionHigh = decimal.MinValue;
		_sessionLow = decimal.MaxValue;
		_barsInSession = 0;
		_prevCandleClose = null;
		_ordersPlaced = false;
		_needsSessionReset = false;
		_tradeTaken = false;
	}

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

		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;

		var closeTime = candle.CloseTime;
		var sessionStart = CalculateSessionStart(closeTime);

		if (_sessionStart != sessionStart)
		{
			_sessionStart = sessionStart;
			_sessionEnd = CalculateSessionEnd(_sessionStart);
			ResetSessionState();
		}

		if (_needsSessionReset)
		{
			// Close any open position at session reset
			if (Position > 0)
				SellMarket(Position);
			else if (Position < 0)
				BuyMarket(-Position);
			_needsSessionReset = false;
		}

		if (candle.OpenTime >= _sessionStart && candle.OpenTime < _sessionEnd)
		{
			var high = candle.HighPrice;
			var low = candle.LowPrice;

			if (_sessionHigh == decimal.MinValue || high > _sessionHigh)
				_sessionHigh = high;

			if (_sessionLow == decimal.MaxValue || low < _sessionLow)
				_sessionLow = low;

			_barsInSession++;
		}

		// After session ends, trade breakouts of the channel
		if (_ordersPlaced && !_tradeTaken && _barsInSession >= 2 && _sessionLow < _sessionHigh)
		{
			if (Position == 0)
			{
				// Buy when price touches session low, sell when it touches session high
				if (candle.LowPrice <= _sessionLow)
				{
					BuyMarket(OrderVolume);
					_tradeTaken = true;
				}
				else if (candle.HighPrice >= _sessionHigh)
				{
					SellMarket(OrderVolume);
					_tradeTaken = true;
				}
			}
		}

		if (!_ordersPlaced && _prevCandleClose.HasValue)
		{
			var previousClose = _prevCandleClose.Value;

			if (previousClose < _sessionEnd && closeTime >= _sessionEnd)
			{
				if (_barsInSession >= 2 && _sessionLow < _sessionHigh)
				{
					_ordersPlaced = true;
				}
			}
		}

		_prevCandleClose = closeTime;
	}

	private void ResetSessionState()
	{
		_sessionHigh = decimal.MinValue;
		_sessionLow = decimal.MaxValue;
		_barsInSession = 0;
		_ordersPlaced = false;
		_needsSessionReset = true;
		_tradeTaken = false;
	}

	private DateTimeOffset CalculateSessionStart(DateTimeOffset time)
	{
		var offset = time.Offset;
		var day = new DateTimeOffset(time.Date, offset);
		var start = day.AddHours(BeginHour);
		var startHour = TimeSpan.FromHours(BeginHour);

		if (BeginHour <= EndHour)
		{
			if (time < start)
				start = start.AddDays(-1);
		}
		else
		{
			if (time.TimeOfDay < startHour)
				start = start.AddDays(-1);
		}

		return start;
	}

	private DateTimeOffset CalculateSessionEnd(DateTimeOffset sessionStart)
	{
		var offset = sessionStart.Offset;
		var day = new DateTimeOffset(sessionStart.Date, offset);
		var end = day.AddHours(EndHour);

		if (EndHour <= BeginHour || end <= sessionStart)
			end = end.AddDays(1);

		return end;
	}
}