Эта стратегия переносит советник MetaTrader «Pending orders by time» в StockSharp. Логика полностью завязана на расписание: в выбранный час открытия она выставляет симметричные стоп-заявки выше/ниже рынка, а в заданный час закрытия отменяет все незаполненные заявки и принудительно фиксирует позиции. Все параметры задаются в пунктах, как и в оригинале, а внутри конвертируются в цену с учётом точности котировок инструмента.
Как работает алгоритм
Триггер по времени. Когда приходит свеча, закрывающаяся в час открытия, стратегия отправляет Buy Stop над лучшей ценой ask и Sell Stop под лучшей ценой bid. Смещение рассчитывается по параметру Distance (pips).
Защитные ордера. Метод StartProtection автоматически добавляет стоп-лосс и тейк-профит с расстоянием, указанным в пунктах. Дополнительно метод ManageRisk проверяет закрывшиеся свечи и вручную закрывает позицию, если ценовой ход превысил стоп или цель.
Завершение сессии. В час закрытия стратегия отменяет все оставшиеся стоп-заявки и закрывает позицию по рынку, чтобы начать следующий день «с чистого листа».
Учёт количества знаков. Размер пункта вычисляется как шаг цены, умноженный на десять для инструментов с тремя или пятью знаками после запятой (JPY, пятизначные котировки). Благодаря этому исходные параметры в пунктах ведут себя так же, как в MetaTrader.
По умолчанию используется таймфрейм 30 минут — это укладывается в требование оригинала работать на периодах не длиннее H1. При необходимости можно выбрать другую свечную серию, главное, чтобы отметки часов совпадали с нужным расписанием.
Параметры
Имя
Описание
Значение по умолчанию
Opening Hour
Час (0–23), когда выставляется пара стоп-заявок.
9
Closing Hour
Час (0–23), когда отменяются заявки и закрываются позиции.
2
Distance (pips)
Смещение в пунктах от текущей цены до уровней стоп-заявок.
20
Stop Loss (pips)
Расстояние стоп-лосса в пунктах.
20
Take Profit (pips)
Расстояние тейк-профита в пунктах.
500
Order Volume
Объём каждой стоп-заявки.
0.1
Candle Type
Таймфрейм, по которому отслеживаются часы.
30-минутный таймфрейм
Все параметры пригодны для оптимизации. Внутри стратегии значения в пунктах преобразуются через шаг цены, поэтому она корректно работает на разных FX-инструментах с отличающейся точностью котировки.
Суточный цикл
На каждом закрытии свечи проверяется, достигнут ли стоп-лосс или тейк-профит. Если да — позиция закрывается рыночным ордером.
В момент закрывающего часа отменяются незаполненные заявки и закрывается позиция, чтобы не переносить сделки на следующий день.
В момент открывающего часа (при отсутствии позиции) дополнительно отменяются старые заявки и отправляется новая пара Sell Stop / Buy Stop симметрично относительно текущего спреда.
В течение сессии защитные ордера, запущенные через StartProtection, следят за превышением стопа или цели внутри свечи и срабатывают немедленно.
Практические замечания
Используйте инструменты, у которых шаг цены соответствует «пункту» — так сохранится логика исходного советника. Для экзотических тик-сайзов может потребоваться корректировка расстояний.
Предполагается один торговый цикл в сутки. Если данные содержат несколько совпадений часов, скорректируйте параметры открытия/закрытия.
Поскольку решения принимаются на закрытии свечи, выбирайте таймфрейм с нужной частотой контроля. Часовые свечи полностью повторяют поведение оригинала.
Новые стоп-заявки выставляются только при нулевой позиции, чтобы не наращивать риск, если пробой уже в работе.
Отличия от MQL-версии
Защитные уровни реализованы через StartProtection и дополнительную проверку, а не через прямое присвоение стоп-лосса заявке.
Цены bid/ask берутся из Security.BestBid и Security.BestAsk. При отсутствии котировок используется цена закрытия свечи.
Для фиксации позиции в час закрытия применяются рыночные ордера — это упрощает переносимость между брокерами.
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>
/// Places simulated symmetric stop entries at scheduled hours and manages them with daily resets.
/// </summary>
public class PendingOrdersByTimeStrategy : Strategy
{
private readonly StrategyParam<int> _openingHour;
private readonly StrategyParam<int> _closingHour;
private readonly StrategyParam<decimal> _distancePips;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<DataType> _candleType;
private decimal _pipSize;
private decimal? _pendingBuyPrice;
private decimal? _pendingSellPrice;
private decimal? _entryPrice;
public int OpeningHour
{
get => _openingHour.Value;
set => _openingHour.Value = value;
}
public int ClosingHour
{
get => _closingHour.Value;
set => _closingHour.Value = value;
}
public decimal DistancePips
{
get => _distancePips.Value;
set => _distancePips.Value = value;
}
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public PendingOrdersByTimeStrategy()
{
_openingHour = Param(nameof(OpeningHour), 2)
.SetDisplay("Opening Hour", "Hour to activate pending orders", "Schedule")
.SetRange(0, 23);
_closingHour = Param(nameof(ClosingHour), 22)
.SetDisplay("Closing Hour", "Hour to cancel orders and flat positions", "Schedule")
.SetRange(0, 23);
_distancePips = Param(nameof(DistancePips), 500m)
.SetDisplay("Distance (pips)", "Offset for entry stop orders", "Orders")
.SetGreaterThanZero();
_stopLossPips = Param(nameof(StopLossPips), 500m)
.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk")
.SetGreaterThanZero();
_takeProfitPips = Param(nameof(TakeProfitPips), 2000m)
.SetDisplay("Take Profit (pips)", "Profit target distance", "Risk")
.SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Working timeframe for the schedule", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_pipSize = 0m;
_pendingBuyPrice = null;
_pendingSellPrice = null;
_entryPrice = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 0.01m;
if (step <= 0m)
return 0.01m;
return step;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var hour = candle.OpenTime.Hour;
// Check pending stop entries
CheckPendingEntries(candle);
// Manage existing position
ManageRisk(candle);
if (hour == ClosingHour)
{
// Closing hour: cancel pending and exit any open trades.
_pendingBuyPrice = null;
_pendingSellPrice = null;
ExitPosition();
}
if (hour == OpeningHour && hour != ClosingHour && Position == 0m && !_pendingBuyPrice.HasValue)
{
// Opening hour: set up new pending entries.
SetupPendingEntries(candle.ClosePrice);
}
}
private void CheckPendingEntries(ICandleMessage candle)
{
if (_pendingBuyPrice is decimal buyPrice && candle.HighPrice >= buyPrice && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_entryPrice = buyPrice;
_pendingBuyPrice = null;
_pendingSellPrice = null;
return;
}
if (_pendingSellPrice is decimal sellPrice && candle.LowPrice <= sellPrice && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_entryPrice = sellPrice;
_pendingBuyPrice = null;
_pendingSellPrice = null;
}
}
private void ManageRisk(ICandleMessage candle)
{
if (_pipSize <= 0m || _entryPrice is not decimal entry)
return;
var takeProfitDistance = TakeProfitPips * _pipSize;
var stopLossDistance = StopLossPips * _pipSize;
if (Position > 0m)
{
if (takeProfitDistance > 0m && candle.HighPrice - entry >= takeProfitDistance)
{
SellMarket();
_entryPrice = null;
return;
}
if (stopLossDistance > 0m && entry - candle.LowPrice >= stopLossDistance)
{
SellMarket();
_entryPrice = null;
}
}
else if (Position < 0m)
{
if (takeProfitDistance > 0m && entry - candle.LowPrice >= takeProfitDistance)
{
BuyMarket();
_entryPrice = null;
return;
}
if (stopLossDistance > 0m && candle.HighPrice - entry >= stopLossDistance)
{
BuyMarket();
_entryPrice = null;
}
}
}
private void ExitPosition()
{
if (Position > 0m)
SellMarket();
else if (Position < 0m)
BuyMarket();
_entryPrice = null;
}
private void SetupPendingEntries(decimal referencePrice)
{
if (_pipSize <= 0m)
return;
var distance = DistancePips * _pipSize;
if (distance <= 0m)
return;
_pendingBuyPrice = referencePrice + distance;
_pendingSellPrice = referencePrice - distance;
}
}
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 pending_orders_by_time_strategy(Strategy):
"""Places symmetric stop entries at scheduled hours with daily resets and SL/TP management."""
def __init__(self):
super(pending_orders_by_time_strategy, self).__init__()
self._opening_hour = self.Param("OpeningHour", 2) \
.SetDisplay("Opening Hour", "Hour to activate pending orders", "Schedule")
self._closing_hour = self.Param("ClosingHour", 22) \
.SetDisplay("Closing Hour", "Hour to cancel orders and flat positions", "Schedule")
self._distance_pips = self.Param("DistancePips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("Distance (pips)", "Offset for entry stop orders", "Orders")
self._stop_loss_pips = self.Param("StopLossPips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 2000.0) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "Profit target distance", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Working timeframe for the schedule", "General")
self._pip_size = 0.0
self._pending_buy_price = None
self._pending_sell_price = None
self._entry_price = None
@property
def OpeningHour(self):
return int(self._opening_hour.Value)
@property
def ClosingHour(self):
return int(self._closing_hour.Value)
@property
def DistancePips(self):
return float(self._distance_pips.Value)
@property
def StopLossPips(self):
return float(self._stop_loss_pips.Value)
@property
def TakeProfitPips(self):
return float(self._take_profit_pips.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _calculate_pip_size(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 0.01
if step <= 0:
return 0.01
return step
def OnStarted2(self, time):
super(pending_orders_by_time_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._pending_buy_price = None
self._pending_sell_price = None
self._entry_price = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
hour = candle.OpenTime.Hour
# Check pending stop entries
self._check_pending_entries(candle)
# Manage existing position
self._manage_risk(candle)
if hour == self.ClosingHour:
# Closing hour: cancel pending and exit any open trades
self._pending_buy_price = None
self._pending_sell_price = None
self._exit_position()
if hour == self.OpeningHour and hour != self.ClosingHour and self.Position == 0 and self._pending_buy_price is None:
# Opening hour: set up new pending entries
self._setup_pending_entries(float(candle.ClosePrice))
def _check_pending_entries(self, candle):
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self._pending_buy_price is not None and h >= self._pending_buy_price and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = self._pending_buy_price
self._pending_buy_price = None
self._pending_sell_price = None
return
if self._pending_sell_price is not None and lo <= self._pending_sell_price and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = self._pending_sell_price
self._pending_buy_price = None
self._pending_sell_price = None
def _manage_risk(self, candle):
if self._pip_size <= 0 or self._entry_price is None:
return
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
entry = self._entry_price
tp_dist = self.TakeProfitPips * self._pip_size
sl_dist = self.StopLossPips * self._pip_size
if self.Position > 0:
if tp_dist > 0 and h - entry >= tp_dist:
self.SellMarket()
self._entry_price = None
return
if sl_dist > 0 and entry - lo >= sl_dist:
self.SellMarket()
self._entry_price = None
elif self.Position < 0:
if tp_dist > 0 and entry - lo >= tp_dist:
self.BuyMarket()
self._entry_price = None
return
if sl_dist > 0 and h - entry >= sl_dist:
self.BuyMarket()
self._entry_price = None
def _exit_position(self):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._entry_price = None
def _setup_pending_entries(self, reference_price):
if self._pip_size <= 0:
return
distance = self.DistancePips * self._pip_size
if distance <= 0:
return
self._pending_buy_price = reference_price + distance
self._pending_sell_price = reference_price - distance
def OnReseted(self):
super(pending_orders_by_time_strategy, self).OnReseted()
self._pip_size = 0.0
self._pending_buy_price = None
self._pending_sell_price = None
self._entry_price = None
def CreateClone(self):
return pending_orders_by_time_strategy()