Esta estrategia recrea el clásico experto de MetaTrader "Pending orders by time" para StockSharp. Funciona con un horario discreto: cada día coloca órdenes stop simétricas alrededor del mercado cuando comienza una nueva hora de sesión, y elimina todas las órdenes más las posiciones abiertas a una hora de cierre especificada. La implementación mantiene las entradas originales basadas en pips, las convierte a unidades de precio nativas y usa el API de alto nivel para gestionar el riesgo.
Cómo funciona
Disparador basado en tiempo – Cuando se recibe una vela que termina a la hora de apertura configurada, la estrategia envía un buy stop por encima del ask y un sell stop por debajo del bid. Ambas órdenes se desplazan por el parámetro Distance (pips) convertido a unidades de precio.
Órdenes protectoras – StartProtection adjunta automáticamente protección de stop-loss y take-profit usando las distancias en pips definidas en los parámetros. ManageRisk también actúa como salvaguarda, cerrando cualquier posición residual si una vela completada muestra que se han cruzado los umbrales.
Cierre de sesión – Cuando llega la hora de cierre, la estrategia cancela cualquier orden pendiente restante y sale forzosamente de las operaciones abiertas independientemente del beneficio o pérdida. Esto reproduce el comportamiento del experto original de restablecer al final de la sesión.
Tamaño de pip con conciencia de dígitos – El multiplicador de pip emula la implementación de MetaTrader multiplicando el paso de precio por diez para símbolos cotizados con tres o cinco decimales (p. ej., JPY o pares FX de 5 dígitos). Esto mantiene las entradas heredadas consistentes entre brokers.
El tipo de vela predeterminado son barras de 30 minutos para mantenerse bajo la restricción original de períodos menores que H1. Se puede usar cualquier otro marco temporal, siempre que las marcas de tiempo horarias resultantes coincidan con las horas de sesión deseadas.
Parámetros
Nombre
Descripción
Predeterminado
Opening Hour
Hora (0-23) cuando la estrategia colocará el par de órdenes stop.
9
Closing Hour
Hora (0-23) cuando se cancelan todas las órdenes y se cierran las posiciones.
2
Distance (pips)
Desplazamiento, en pips, entre el precio actual y las entradas stop pendientes.
20
Stop Loss (pips)
Distancia en pips para el stop protector una vez que hay una posición abierta.
20
Take Profit (pips)
Distancia en pips para el objetivo de beneficio una vez que hay una posición abierta.
500
Order Volume
Cantidad usada al colocar cada orden stop pendiente.
0.1
Candle Type
Marco temporal que impulsa el horario.
Marco temporal de 30 minutos
Todos los parámetros pueden optimizarse. Las entradas basadas en pips se convierten internamente usando el paso de precio del instrumento para que permanezcan portátiles entre símbolos FX con diferente precisión decimal.
Flujo de trabajo diario
En cada cierre de vela la estrategia verifica si se ha alcanzado la distancia de stop-loss o take-profit. Si es así, cierra la posición activa a mercado.
Cuando se alcanza la hora de cierre cancela cualquier orden pendiente no llenada y sale de la posición, asegurando que el libro esté plano antes de la próxima sesión.
Cuando se alcanza la hora de apertura (y la estrategia está plana) cancela órdenes antiguas por precaución y envía un nuevo sell stop por debajo del bid y un buy stop por encima del ask. Las órdenes están reflejadas alrededor del spread para que se pueda capturar cualquier ruptura.
A lo largo de la sesión la protección a nivel de plataforma creada por StartProtection mantiene un stop-loss y take-profit adjuntos, actuando inmediatamente si la acción del precio dentro de la barra alcanza los umbrales.
Notas de uso
Use instrumentos cuyo tamaño de tick represente un "punto" único para que el ajuste de pip refleje el experto original. Los tamaños de tick exóticos pueden requerir ajuste manual de los parámetros de distancia.
La lógica asume un ciclo de trading por día. Si usa datos intradía con múltiples coincidencias de apertura/cierre, ajuste las horas en consecuencia.
Dado que todas las acciones ocurren al completar la vela, seleccione un tamaño de vela que coincida con la frecuencia con que desea evaluar el horario. Por ejemplo, velas horarias proporcionan la misma cadencia que la versión de MetaTrader.
La estrategia solo coloca nuevas órdenes pendientes cuando la posición está plana, evitando sobreexposición si una operación de ruptura todavía está activa durante la próxima hora de apertura.
Diferencias con la versión MQL
Las salidas protectoras se manejan a través de StartProtection más verificaciones explícitas, aprovechando el API de alto nivel de StockSharp en lugar de la asignación directa de stop-loss en el ticket de la orden pendiente.
Los precios bid/ask se leen de Security.BestBid y Security.BestAsk. Si esas cotizaciones no están disponibles, el cierre de la vela se usa como referencia de respaldo.
Se usan órdenes de mercado para liquidar posiciones a la hora de cierre por simplicidad y para evitar comportamientos específicos del broker.
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()