Open Two Pending Orders Strategy
Overview
This strategy replicates the MetaTrader expert advisor that simultaneously places a buy stop and a sell stop order around the current spread. It works on a single security and uses high-level StockSharp API calls to subscribe to the order book, manage pending orders, and handle portfolio risk controls. As soon as one pending order is filled, the opposite order is cancelled and the active position is managed with stop-loss, take-profit, and trailing-stop rules.
Trading Logic
- Subscribe to the order book and read the best bid and ask prices.
- When there is no open position or active entry order, calculate the entry volume and place two stop orders:
- Buy stop at ask + EntryOffsetPoints × PriceStep.
- Sell stop at bid − EntryOffsetPoints × PriceStep.
- When a stop order is executed:
- Cancel the opposite pending order.
- Store the execution price as the new entry price.
- Compute the initial stop-loss and take-profit levels in price steps relative to the fill.
- While the position is active, monitor the order book:
- Close longs when the bid reaches the stop-loss or take-profit level.
- Close shorts when the ask reaches the stop-loss or take-profit level.
- Activate the trailing stop after price moves in favour of the trade by the trailing distance and slide the stop level accordingly.
- When the position returns to flat, reset the internal state and place a fresh pair of stop orders.
Exits are executed with market orders once a protective level is touched. This keeps the logic close to the MQL implementation without relying on lower-level order modification APIs.
Money Management
The strategy can use either a fixed volume or dynamic risk-based sizing:
- Fixed Volume – use the constant lot size defined by the
FixedVolume parameter.
- Money Management – if enabled, calculate the volume from the portfolio equity, the risk percentage, and the stop-loss distance in price steps. Volumes are rounded to the instrument volume step and clamped between the instrument’s minimum and maximum values.
Parameters
| Parameter |
Description |
UseMoneyManagement |
Enables risk-based position sizing. Default: true. |
RiskPercent |
Percentage of portfolio equity to risk per trade when money management is active. Default: 2. |
FixedVolume |
Lot size used when money management is disabled. Default: 1. |
StopLossPoints |
Stop-loss distance in price steps from the entry price. Default: 100. |
TakeProfitPoints |
Take-profit distance in price steps from the entry price. Default: 300. |
TrailingStopPoints |
Trailing stop distance in price steps. A value of 0 disables trailing. Default: 50. |
EntryOffsetPoints |
Distance in price steps used to place the pending orders away from the spread. Default: 50. |
SlippagePoints |
Extra cushion in price steps reserved for slippage. Currently informational and not used directly. Default: 5. |
Notes
- The strategy relies on the order book feed. Ensure that market depth data is available for the selected security.
- Stop-loss and take-profit execution uses market orders once the bid/ask crosses the level, matching the behaviour of the original MQL trailing stop logic.
- Trailing stops start only after the price has moved by the configured trailing distance from the entry.
- The code uses tab indentation, English comments, and high-level StockSharp methods according to the project guidelines.
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>
/// Strategy that simulates placing both buy stop and sell stop orders around the current price.
/// It uses candle-based breakout detection and manages the resulting position
/// with fixed stop loss, take profit and optional trailing stop levels.
/// </summary>
public class OpenTwoPendingOrdersStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _entryOffsetPoints;
private readonly StrategyParam<DataType> _candleType;
private decimal? _pendingBuyPrice;
private decimal? _pendingSellPrice;
private decimal? _entryPrice;
private decimal? _stopLevel;
private decimal? _takeLevel;
private decimal _highestSinceEntry;
private decimal _lowestSinceEntry;
private int _cooldown;
/// <summary>
/// Stop loss distance expressed in price steps.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance expressed in price steps.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in price steps.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Distance in price steps used to place the pending entries away from the current price.
/// </summary>
public decimal EntryOffsetPoints
{
get => _entryOffsetPoints.Value;
set => _entryOffsetPoints.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="OpenTwoPendingOrdersStrategy"/>.
/// </summary>
public OpenTwoPendingOrdersStrategy()
{
_stopLossPoints = Param(nameof(StopLossPoints), 5000m)
.SetDisplay("Stop Loss (steps)", "Stop loss distance in price steps", "Risk")
.SetOptimize(20m, 300m, 20m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 8000m)
.SetDisplay("Take Profit (steps)", "Take profit distance in price steps", "Risk")
.SetOptimize(50m, 600m, 50m);
_trailingStopPoints = Param(nameof(TrailingStopPoints), 3000m)
.SetDisplay("Trailing Stop (steps)", "Trailing stop distance in price steps", "Risk")
.SetOptimize(10m, 200m, 10m);
_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 1000m)
.SetDisplay("Entry Offset (steps)", "Offset from close for pending entries", "Execution")
.SetOptimize(10m, 150m, 10m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
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;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var step = GetStep();
// Manage existing position
if (Position != 0 && _entryPrice.HasValue)
{
ManagePosition(candle, step);
// If position was closed, reset and set up new pending entries
if (Position == 0)
{
ResetState();
_cooldown = 20;
}
return;
}
// Check pending entries
if (_pendingBuyPrice.HasValue && _pendingSellPrice.HasValue)
{
var buyLevel = _pendingBuyPrice.Value;
var sellLevel = _pendingSellPrice.Value;
// Buy stop triggered: price went up to pending buy level
if (candle.HighPrice >= buyLevel)
{
_pendingBuyPrice = null;
_pendingSellPrice = null;
BuyMarket();
InitializePositionLevels(true, buyLevel, step);
return;
}
// Sell stop triggered: price went down to pending sell level
if (candle.LowPrice <= sellLevel)
{
_pendingBuyPrice = null;
_pendingSellPrice = null;
SellMarket();
InitializePositionLevels(false, sellLevel, step);
return;
}
}
else
{
// No pending entries, set up new ones
SetupPendingEntries(candle.ClosePrice, step);
}
}
private void SetupPendingEntries(decimal currentPrice, decimal step)
{
var offset = EntryOffsetPoints * step;
_pendingBuyPrice = currentPrice + offset;
_pendingSellPrice = currentPrice - offset;
}
private void InitializePositionLevels(bool isLong, decimal entryPrice, decimal step)
{
_entryPrice = entryPrice;
_highestSinceEntry = entryPrice;
_lowestSinceEntry = entryPrice;
_stopLevel = StopLossPoints > 0m
? entryPrice + (isLong ? -StopLossPoints : StopLossPoints) * step
: null;
_takeLevel = TakeProfitPoints > 0m
? entryPrice + (isLong ? TakeProfitPoints : -TakeProfitPoints) * step
: null;
}
private void ManagePosition(ICandleMessage candle, decimal step)
{
if (Position > 0)
{
_highestSinceEntry = Math.Max(_highestSinceEntry, candle.HighPrice);
if (_stopLevel.HasValue && candle.LowPrice <= _stopLevel.Value)
{
SellMarket();
return;
}
if (_takeLevel.HasValue && candle.HighPrice >= _takeLevel.Value)
{
SellMarket();
return;
}
UpdateTrailingStop(true, step);
}
else if (Position < 0)
{
_lowestSinceEntry = Math.Min(_lowestSinceEntry, candle.LowPrice);
if (_stopLevel.HasValue && candle.HighPrice >= _stopLevel.Value)
{
BuyMarket();
return;
}
if (_takeLevel.HasValue && candle.LowPrice <= _takeLevel.Value)
{
BuyMarket();
return;
}
UpdateTrailingStop(false, step);
}
}
private void UpdateTrailingStop(bool isLong, decimal step)
{
if (TrailingStopPoints <= 0m || _entryPrice == null)
return;
var trailingDistance = TrailingStopPoints * step;
if (trailingDistance <= 0m)
return;
if (isLong)
{
if (_highestSinceEntry - _entryPrice.Value >= trailingDistance)
{
var desiredStop = _highestSinceEntry - trailingDistance;
if (_stopLevel == null || desiredStop > _stopLevel.Value)
_stopLevel = desiredStop;
}
}
else
{
if (_entryPrice.Value - _lowestSinceEntry >= trailingDistance)
{
var desiredStop = _lowestSinceEntry + trailingDistance;
if (_stopLevel == null || desiredStop < _stopLevel.Value)
_stopLevel = desiredStop;
}
}
}
private void ResetState()
{
_pendingBuyPrice = null;
_pendingSellPrice = null;
_entryPrice = null;
_stopLevel = null;
_takeLevel = null;
_highestSinceEntry = 0m;
_lowestSinceEntry = 0m;
_cooldown = 0;
}
private decimal GetStep()
{
var step = Security?.PriceStep ?? 0m;
return step > 0m ? step : 0.01m;
}
}
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 datatype_extensions import *
class open_two_pending_orders_strategy(Strategy):
def __init__(self):
super(open_two_pending_orders_strategy, self).__init__()
self._sl_points = self.Param("StopLossPoints", 5000.0).SetDisplay("Stop Loss (steps)", "Stop loss distance in price steps", "Risk")
self._tp_points = self.Param("TakeProfitPoints", 8000.0).SetDisplay("Take Profit (steps)", "Take profit distance in price steps", "Risk")
self._trail_points = self.Param("TrailingStopPoints", 3000.0).SetDisplay("Trailing Stop (steps)", "Trailing stop distance in price steps", "Risk")
self._entry_offset = self.Param("EntryOffsetPoints", 1000.0).SetDisplay("Entry Offset (steps)", "Offset from close for pending entries", "Execution")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Type of candles", "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(open_two_pending_orders_strategy, self).OnReseted()
self._reset_state()
def _reset_state(self):
self._pending_buy = None
self._pending_sell = None
self._entry_price = None
self._stop_level = None
self._take_level = None
self._highest = 0.0
self._lowest = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(open_two_pending_orders_strategy, self).OnStarted2(time)
self._reset_state()
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 _get_step(self):
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > 0:
return float(self.Security.PriceStep)
return 0.01
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
if self._cooldown > 0:
self._cooldown -= 1
return
step = self._get_step()
# Manage existing position
if self.Position != 0 and self._entry_price is not None:
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self.Position > 0:
self._highest = max(self._highest, h)
if self._stop_level is not None and lo <= self._stop_level:
self.SellMarket()
elif self._take_level is not None and h >= self._take_level:
self.SellMarket()
else:
# trailing
if float(self._trail_points.Value) > 0:
trail_dist = float(self._trail_points.Value) * step
if self._highest - self._entry_price >= trail_dist:
desired = self._highest - trail_dist
if self._stop_level is None or desired > self._stop_level:
self._stop_level = desired
elif self.Position < 0:
self._lowest = min(self._lowest, lo)
if self._stop_level is not None and h >= self._stop_level:
self.BuyMarket()
elif self._take_level is not None and lo <= self._take_level:
self.BuyMarket()
else:
if float(self._trail_points.Value) > 0:
trail_dist = float(self._trail_points.Value) * step
if self._entry_price - self._lowest >= trail_dist:
desired = self._lowest + trail_dist
if self._stop_level is None or desired < self._stop_level:
self._stop_level = desired
if self.Position == 0:
self._reset_state()
self._cooldown = 20
return
# Check pending entries
if self._pending_buy is not None and self._pending_sell is not None:
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if h >= self._pending_buy:
entry = self._pending_buy
self._pending_buy = None
self._pending_sell = None
self.BuyMarket()
self._entry_price = entry
self._highest = entry
self._lowest = entry
sl = float(self._sl_points.Value)
tp = float(self._tp_points.Value)
self._stop_level = entry - sl * step if sl > 0 else None
self._take_level = entry + tp * step if tp > 0 else None
return
if lo <= self._pending_sell:
entry = self._pending_sell
self._pending_buy = None
self._pending_sell = None
self.SellMarket()
self._entry_price = entry
self._highest = entry
self._lowest = entry
sl = float(self._sl_points.Value)
tp = float(self._tp_points.Value)
self._stop_level = entry + sl * step if sl > 0 else None
self._take_level = entry - tp * step if tp > 0 else None
return
else:
offset = float(self._entry_offset.Value) * step
self._pending_buy = float(candle.ClosePrice) + offset
self._pending_sell = float(candle.ClosePrice) - offset
def CreateClone(self):
return open_two_pending_orders_strategy()