Эта стратегия для StockSharp является дословной конверсией советника MetaTrader 4 Strategy_of_Regularities_of_Exchange_Rates.mq4. Идея проста: в заданный час формируется «коридор» из симметричных отложенных стоп-заявок, а в ночной час закрытия все позиции и заявки принудительно снимаются. Таким образом реализуется дневной брейкаут-стреддл, который живёт строго в пределах одной торговой сессии.
В отличие от индикаторных систем здесь нет сложных вычислений — только расписание и фиксированное расстояние. Когда наступает OpeningHour, стратегия берёт текущие bid/ask, отмеряет EntryOffsetPoints в брокерских пунктах (pips) и размещает Buy Stop выше ask и Sell Stop ниже bid. Алгоритм автоматически корректирует шаг цены для инструментов с 3- или 5-значными котировками, чтобы поведение совпадало с оригиналом.
Логика торговли
Час открытия. После закрытия свечи с часом OpeningHour стратегия удаляет остаточные заявки и создаёт пару стопов. Расстояние задаётся как EntryOffsetPoints * point, где point вычисляется из PriceStep инструмента с учётом дробных котировок.
Защитные ордера. При запуске вызывается StartProtection с параметром StopLossPoints, поэтому каждая исполненная сделка получает стоп-лосс, аналогичный MT4.
Контроль профита. На каждой завершённой свече проверяется, достигла ли прибыль значения TakeProfitPoints * point. Если да — позиция закрывается рыночной заявкой, что полностью повторяет цикл OrderClose в MQL.
Час закрытия. Когда часы равны ClosingHour, все отложенные заявки снимаются, а позиции ликвидируются независимо от текущего результата.
Суточный сброс. Новая пара стоп-заявок создаётся только один раз в сутки, что исключает дублирование сигналов на таймфреймах меньше часа.
Параметры
Параметр
Значение по умолчанию
Описание
OpeningHour
9
Час (0–23), в который размещаются отложенные стоп-заявки.
ClosingHour
2
Час (0–23), в который стратегия снимает заявки и закрывает позиции.
EntryOffsetPoints
20
Расстояние от текущих bid/ask до стоп-заявок в брокерских пунктах.
TakeProfitPoints
20
Цель по прибыли в пунктах. Значение 0 отключает ручной тейк-профит.
StopLossPoints
500
Размер стоп-лосса в пунктах, передаваемый в StartProtection.
OrderVolume
0.1
Объём каждой стоп-заявки.
CandleType
30 минут
Таймфрейм, по свечам которого контролируется расписание. Любой интервал ≤ 1 часа сохраняет поведение оригинала.
Особенности конверсии
В MQL советник работал по тиковым событиям и опирался на Hour(). В StockSharp используется поток завершённых свечей и их OpenTime.Hour, что соответствует правилам репозитория и сохраняет часовую логику.
Цены заявок нормализуются через Security.ShrinkPrice, поэтому уровни всегда кратны шагу цены инструмента.
Защитный стоп реализован через StartProtection, что повторяет присоединённый брокером стоп-лосс при OrderSend.
Введено отслеживание даты последнего сигнала, чтобы на подчасовых таймфреймах не возникало множества наборов заявок в течение одного дня.
В коде добавлены подробные английские комментарии, описывающие каждый этап работы стратегии, что упрощает поддержку и эксперименты.
using System;
using System.Linq;
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>
/// Time-based breakout strategy converted from the "Strategy of Regularities of Exchange Rates" MQL expert advisor.
/// At a scheduled hour captures reference price, then enters on breakout above/below offset levels.
/// Exits at a closing hour or on take-profit/stop-loss hit.
/// </summary>
public class RegularitiesOfExchangeRatesStrategy : Strategy
{
private readonly StrategyParam<int> _openingHour;
private readonly StrategyParam<int> _closingHour;
private readonly StrategyParam<decimal> _entryOffsetPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _dummySma;
private decimal _pointSize;
private DateTime? _lastEntryDate;
private decimal _referencePrice;
private decimal _entryPrice;
private bool _waitingForBreakout;
public int OpeningHour
{
get => _openingHour.Value;
set => _openingHour.Value = value;
}
public int ClosingHour
{
get => _closingHour.Value;
set => _closingHour.Value = value;
}
public decimal EntryOffsetPoints
{
get => _entryOffsetPoints.Value;
set => _entryOffsetPoints.Value = value;
}
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public RegularitiesOfExchangeRatesStrategy()
{
_openingHour = Param(nameof(OpeningHour), 9)
.SetDisplay("Opening Hour", "Hour (0-23) when breakout levels are set", "Schedule")
.SetRange(0, 23);
_closingHour = Param(nameof(ClosingHour), 2)
.SetDisplay("Closing Hour", "Hour (0-23) when the strategy exits", "Schedule")
.SetRange(0, 23);
_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 20m)
.SetDisplay("Entry Offset (points)", "Distance from reference price for breakout", "Orders")
.SetGreaterThanZero();
_takeProfitPoints = Param(nameof(TakeProfitPoints), 20m)
.SetDisplay("Take Profit (points)", "Profit target distance in points", "Risk")
.SetNotNegative();
_stopLossPoints = Param(nameof(StopLossPoints), 500m)
.SetDisplay("Stop Loss (points)", "Stop-loss distance in points", "Risk")
.SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used to evaluate trading hours", "General");
}
protected override void OnReseted()
{
base.OnReseted();
_pointSize = 0m;
_lastEntryDate = null;
_referencePrice = 0m;
_entryPrice = 0m;
_waitingForBreakout = false;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointSize = Security?.PriceStep ?? 0.01m;
if (_pointSize <= 0m)
_pointSize = 0.01m;
_dummySma = new SimpleMovingAverage { Length = 2 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_dummySma, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
var hour = candle.OpenTime.Hour;
var close = candle.ClosePrice;
var high = candle.HighPrice;
var low = candle.LowPrice;
// At closing hour: flatten position and cancel breakout watch
if (hour == ClosingHour)
{
if (Position > 0)
SellMarket(Position);
else if (Position < 0)
BuyMarket(-Position);
_waitingForBreakout = false;
_entryPrice = 0m;
}
// Manage take-profit and stop-loss for existing position
if (Position != 0 && _entryPrice > 0m)
{
var tp = TakeProfitPoints * _pointSize;
var sl = StopLossPoints * _pointSize;
if (Position > 0)
{
if ((tp > 0m && close - _entryPrice >= tp) || (sl > 0m && _entryPrice - close >= sl))
{
SellMarket(Position);
_entryPrice = 0m;
_waitingForBreakout = false;
}
}
else if (Position < 0)
{
if ((tp > 0m && _entryPrice - close >= tp) || (sl > 0m && close - _entryPrice >= sl))
{
BuyMarket(-Position);
_entryPrice = 0m;
_waitingForBreakout = false;
}
}
}
// At opening hour: set reference price for breakout
if (hour == OpeningHour && Position == 0)
{
var date = candle.OpenTime.Date;
if (!_lastEntryDate.HasValue || _lastEntryDate.Value != date)
{
_referencePrice = close;
_waitingForBreakout = true;
_lastEntryDate = date;
}
}
// Check for breakout entry
if (_waitingForBreakout && Position == 0 && _referencePrice > 0m)
{
var offset = EntryOffsetPoints * _pointSize;
var buyLevel = _referencePrice + offset;
var sellLevel = _referencePrice - offset;
if (high >= buyLevel)
{
BuyMarket();
_entryPrice = close;
_waitingForBreakout = false;
}
else if (low <= sellLevel)
{
SellMarket();
_entryPrice = close;
_waitingForBreakout = false;
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import SimpleMovingAverage
class regularities_of_exchange_rates_strategy(Strategy):
def __init__(self):
super(regularities_of_exchange_rates_strategy, self).__init__()
self._opening_hour = self.Param("OpeningHour", 9) \
.SetDisplay("Opening Hour", "Hour (0-23) when breakout levels are set", "Schedule")
self._closing_hour = self.Param("ClosingHour", 2) \
.SetDisplay("Closing Hour", "Hour (0-23) when the strategy exits", "Schedule")
self._entry_offset_points = self.Param("EntryOffsetPoints", 20.0) \
.SetDisplay("Entry Offset (points)", "Distance from reference price for breakout", "Orders")
self._take_profit_points = self.Param("TakeProfitPoints", 20.0) \
.SetDisplay("Take Profit (points)", "Profit target distance in points", "Risk")
self._stop_loss_points = self.Param("StopLossPoints", 500.0) \
.SetDisplay("Stop Loss (points)", "Stop-loss distance in points", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe used to evaluate trading hours", "General")
self._point_size = 0.01
self._last_entry_date = None
self._reference_price = 0.0
self._entry_price = 0.0
self._waiting_for_breakout = False
@property
def OpeningHour(self):
return self._opening_hour.Value
@property
def ClosingHour(self):
return self._closing_hour.Value
@property
def EntryOffsetPoints(self):
return self._entry_offset_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(regularities_of_exchange_rates_strategy, self).OnStarted2(time)
self._point_size = 0.01
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps > 0:
self._point_size = ps
self._dummy_sma = SimpleMovingAverage()
self._dummy_sma.Length = 2
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._dummy_sma, self.ProcessCandle).Start()
def ProcessCandle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
hour = candle.OpenTime.Hour
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
# At closing hour: flatten position and cancel breakout watch
if hour == self.ClosingHour:
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self._waiting_for_breakout = False
self._entry_price = 0.0
# Manage take-profit and stop-loss for existing position
if self.Position != 0 and self._entry_price > 0:
tp = float(self.TakeProfitPoints) * self._point_size
sl = float(self.StopLossPoints) * self._point_size
if self.Position > 0:
if (tp > 0 and close - self._entry_price >= tp) or (sl > 0 and self._entry_price - close >= sl):
self.SellMarket(Math.Abs(self.Position))
self._entry_price = 0.0
self._waiting_for_breakout = False
elif self.Position < 0:
if (tp > 0 and self._entry_price - close >= tp) or (sl > 0 and close - self._entry_price >= sl):
self.BuyMarket(Math.Abs(self.Position))
self._entry_price = 0.0
self._waiting_for_breakout = False
# At opening hour: set reference price for breakout
if hour == self.OpeningHour and self.Position == 0:
candle_date = candle.OpenTime.Date
if self._last_entry_date is None or self._last_entry_date != candle_date:
self._reference_price = close
self._waiting_for_breakout = True
self._last_entry_date = candle_date
# Check for breakout entry
if self._waiting_for_breakout and self.Position == 0 and self._reference_price > 0:
offset = float(self.EntryOffsetPoints) * self._point_size
buy_level = self._reference_price + offset
sell_level = self._reference_price - offset
if high >= buy_level:
self.BuyMarket()
self._entry_price = close
self._waiting_for_breakout = False
elif low <= sell_level:
self.SellMarket()
self._entry_price = close
self._waiting_for_breakout = False
def OnReseted(self):
super(regularities_of_exchange_rates_strategy, self).OnReseted()
self._point_size = 0.01
self._last_entry_date = None
self._reference_price = 0.0
self._entry_price = 0.0
self._waiting_for_breakout = False
def CreateClone(self):
return regularities_of_exchange_rates_strategy()