Стратегия 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;
}
}
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.Strategies import Strategy
class twenty_one_hour_strategy(Strategy):
"""Time-based breakout: enter on breakout from previous candle range, close at stop hour."""
def __init__(self):
super(twenty_one_hour_strategy, self).__init__()
self._start_hour = self.Param("StartHour", 10).SetDisplay("Start Hour", "Hour to look for breakout entries", "Schedule")
self._stop_hour = self.Param("StopHour", 22).SetDisplay("Stop Hour", "Hour to close positions", "Schedule")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(twenty_one_hour_strategy, self).OnReseted()
self._prev_high = 0
self._prev_low = 0
self._has_prev = False
self._traded_today = False
self._last_trade_day = -1
def OnStarted2(self, time):
super(twenty_one_hour_strategy, self).OnStarted2(time)
self._prev_high = 0
self._prev_low = 0
self._has_prev = False
self._traded_today = False
self._last_trade_day = -1
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
hour = candle.OpenTime.Hour
day = candle.OpenTime.DayOfYear
# Reset daily flag
if day != self._last_trade_day:
self._traded_today = False
self._last_trade_day = day
# Close at stop hour
start_h = self._start_hour.Value
stop_h = self._stop_hour.Value
if hour >= stop_h and self.Position != 0:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
# Entry at start hour window
if hour >= start_h and hour < stop_h and not self._traded_today and self._has_prev and self.Position == 0:
close = float(candle.ClosePrice)
if close > self._prev_high:
self.BuyMarket()
self._traded_today = True
elif close < self._prev_low:
self.SellMarket()
self._traded_today = True
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
self._has_prev = True
def CreateClone(self):
return twenty_one_hour_strategy()