Auf GitHub ansehen

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

  1. Subscribe to the order book and read the best bid and ask prices.
  2. 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.
  3. 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.
  4. 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.
  5. 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;
	}
}