GitHub で見る

Pending Orders By Time Strategy

This strategy recreates the classic “Pending orders by time” MetaTrader expert for StockSharp. It runs on a discrete schedule: every day it places symmetric stop orders around the market when a new session hour begins, and it clears all orders plus open positions at a specified closing hour. The implementation keeps the original pip-based inputs, converts them to native price units, and uses the high-level API to manage risk.

How it works

  1. Time-based trigger – When a candle that ends at the configured opening hour is received, the strategy submits a buy stop above the ask and a sell stop below the bid. Both orders are offset by the Distance (pips) parameter converted to price units.
  2. Protective ordersStartProtection automatically attaches stop-loss and take-profit protection using the pip distances defined in the parameters. ManageRisk doubles as a safeguard, closing any residual position if a completed candle shows the thresholds have been crossed.
  3. Session shutdown – When the closing hour arrives, the strategy cancels any remaining pending orders and forcefully exits open trades regardless of profit or loss. This reproduces the original expert’s behaviour of resetting at the end of the session.
  4. Digit-aware pip size – The pip multiplier emulates the MetaTrader implementation by multiplying the price step by ten for symbols quoted with three or five decimal places (e.g., JPY or 5-digit FX pairs). This keeps legacy inputs consistent across brokers.

The default candle type is 30-minute bars to stay under the original restriction of periods shorter than H1. Any other time frame can be used, as long as the resulting hourly timestamps match the desired session hours.

Parameters

Name Description Default
Opening Hour Hour (0-23) when the strategy will place the pair of stop orders. 9
Closing Hour Hour (0-23) when all orders are cancelled and positions are closed. 2
Distance (pips) Offset, in pips, between current price and the pending stop entries. 20
Stop Loss (pips) Pip distance for the protective stop once a position is open. 20
Take Profit (pips) Pip distance for the profit target once a position is open. 500
Order Volume Quantity used when placing each pending stop order. 0.1
Candle Type Time frame that drives the hourly schedule. 30-minute TimeFrame

All parameters can be optimised. Pip-based inputs are converted internally using the instrument’s price step so they remain portable between FX symbols with different decimal precision.

Daily workflow

  1. At every candle close the strategy checks whether the stop-loss or take-profit distance has been hit. If so, it closes the active position at market.
  2. When the closing hour is reached it cancels any unfilled pending orders and exits the position, ensuring the book is flat before the next session.
  3. When the opening hour is reached (and the strategy is flat) it cancels old orders just in case and submits a fresh sell stop below the bid and a buy stop above the ask. The orders are mirrored around the spread so either breakout can be captured.
  4. Throughout the session the platform-level protection created by StartProtection keeps a stop-loss and take-profit attached, acting immediately if intrabar price action hits the thresholds.

Usage notes

  • Use instruments whose tick size represents a single “point” so that the pip adjustment mirrors the original expert. Exotic tick sizes may require manual tuning of the distance parameters.
  • The logic assumes one trading cycle per day. If you use intraday data with multiple opening/closing matches, adjust the hours accordingly.
  • Because all actions happen on candle completion, select a candle size that matches how often you want to evaluate the schedule. For example, hourly candles provide the same cadence as the MetaTrader version.
  • The strategy only places new pending orders when the position is flat, avoiding overexposure if a breakout trade is still active during the next opening hour.

Differences from the MQL version

  • Protective exits are handled via StartProtection plus explicit checks, leveraging StockSharp’s high-level API instead of direct stop-loss assignment on the pending order ticket.
  • Bid/ask prices are read from Security.BestBid and Security.BestAsk. If those quotes are unavailable, the candle close is used as a fallback reference.
  • Market orders are used to liquidate positions at the closing hour for simplicity and to avoid broker-specific behaviours.
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;
	}
}