Открыть на GitHub

Стратегия 21hour

Обзор

Стратегия 21hour воспроизводит поведение советника MQL4 21hour.mq4. Она работает в рамках дневного торгового окна: в заданный час запуска выставляется пара отложенных стоп-заявок, а в заданный час остановки все позиции и заявки закрываются. Реализация на StockSharp сохраняет идею «двух стоп-заявок вокруг цены», используя высокоуровневый API для подписок на данные, постановки сделок и управления тейк-профитом.

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

  • В начале каждого торгового дня, когда серверное время достигает StartHour:00, стратегия читает последние котировки bid/ask и размещает одновременно buy stop и sell stop.
    • Расстояние от текущей цены ask до buy stop равно StepPoints * PriceStep.
    • Расстояние от текущей цены bid до sell stop такое же, но вниз от рынка.
    • Значение TakeProfitPoints переводится в абсолютную цену через шаг цены инструмента и передаётся в StartProtection, поэтому для длинных и коротких позиций сразу создаётся защитный тейк-профит.
  • В день допускается только один набор отложенных ордеров. Если активной остаётся лишь одна стоп-заявка (например, после срабатывания другой), стратегия отменяет её, повторяя логику оригинального советника.
  • Когда часы достигают StopHour:00, стратегия закрывает любые открытые позиции по рынку и снимает все отложенные заявки, даже если пробой не произошёл.
  • По умолчанию используется поток минутных свечей. Он нужен только для запуска проверки расписания на закрытых свечах и имитирует защиту prevtime в MQL.

Параметры

Параметр Описание Значение по умолчанию
Volume Объём ордера в лотах для обеих стоп-заявок. 0.1
StartHour Час (0–23), когда формируется пара отложенных ордеров. 10
StopHour Час (0–23), когда стратегия закрывает позиции и снимает заявки. 22
StepPoints Расстояние в пунктах между текущей ценой bid/ask и уровнями входа стоп-заявок. В ценах рассчитывается через PriceStep. 15
TakeProfitPoints Расстояние в пунктах от цены входа до тейк-профита, которым управляет StartProtection. Значение 0 отключает цель. 200
CandleType Тип свечных данных для контроля времени. По умолчанию — минутный таймфрейм (TimeSpan.FromMinutes(1).TimeFrame()). 1 минута

Особенности реализации

  • Использует SubscribeCandles для получения закрытых свечей и проверки расписания один раз в минуту.
  • Подписывается на поток котировок первого уровня через SubscribeLevel1(), чтобы всегда иметь актуальные значения bid/ask для расчёта цен стоп-заявок.
  • Передаёт тейк-профит в StartProtection, что повторяет привязанный тейк-профит исходного советника без ручного управления ордерами.
  • Хранит ссылки на активные buy stop и sell stop, и вызывает CancelOrder, если в системе остаётся только одна стоп-заявка, не допуская односторонних отложенных ордеров.
  • Для принудительного выхода в час остановки использует высокоуровневые вызовы BuyMarket и SellMarket.

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

  • Стратегия ожидает, что соединение брокера предоставит информацию о шаге цены. Если PriceStep отсутствует, округление цен не выполняется.
  • Отложенные ордера создаются лишь один раз за календарный день и будут выставлены заново на следующий день в заданный час начала, даже если пробой так и не состоялся.
  • При TakeProfitPoints = 0 стратегия по-прежнему выставляет стоп-заявки, но защитный тейк-профит не формируется.
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Time-based breakout strategy. At start hour, detects breakout direction from previous candle range.
/// At stop hour, closes all positions.
/// </summary>
public class TwentyOneHourStrategy : Strategy
{
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _stopHour;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevHigh;
	private decimal _prevLow;
	private bool _hasPrev;
	private bool _tradedToday;
	private int _lastTradeDay;

	public TwentyOneHourStrategy()
	{
		_startHour = Param(nameof(StartHour), 10)
			.SetDisplay("Start Hour", "Hour to look for breakout entries.", "Schedule");

		_stopHour = Param(nameof(StopHour), 22)
			.SetDisplay("Stop Hour", "Hour to close positions.", "Schedule");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candles used for time tracking.", "General");
	}

	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	public int StopHour
	{
		get => _stopHour.Value;
		set => _stopHour.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

		_prevHigh = 0;
		_prevLow = 0;
		_hasPrev = false;
		_tradedToday = false;
		_lastTradeDay = -1;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_prevHigh = 0;
		_prevLow = 0;
		_hasPrev = false;
		_tradedToday = false;
		_lastTradeDay = -1;

		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 hour = candle.OpenTime.Hour;
		var day = candle.OpenTime.DayOfYear;

		// Reset daily flag
		if (day != _lastTradeDay)
		{
			_tradedToday = false;
			_lastTradeDay = day;
		}

		// Close at stop hour
		if (hour >= StopHour && Position != 0)
		{
			if (Position > 0)
				SellMarket();
			else
				BuyMarket();
		}

		// Entry at start hour window
		if (hour >= StartHour && hour < StopHour && !_tradedToday && _hasPrev && Position == 0)
		{
			if (candle.ClosePrice > _prevHigh)
			{
				BuyMarket();
				_tradedToday = true;
			}
			else if (candle.ClosePrice < _prevLow)
			{
				SellMarket();
				_tradedToday = true;
			}
		}

		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_hasPrev = true;
	}
}