View on GitHub

Two Pending Orders 2

Overview

This strategy is a StockSharp port of the MetaTrader expert advisor "Two pending orders 2". It keeps two symmetric pending orders around the market price and lets the first triggered side manage the trade with configurable stop-loss, take-profit, and trailing rules. The conversion uses the high level StockSharp API and keeps the core ideas of the original algorithm while exposing every tuning knob through strategy parameters.

Trading logic

  1. The strategy subscribes to the selected candle series (daily candles by default). When a candle is finished it becomes the decision point for the next trading cycle.
  2. Active pending orders are cancelled once they expire or before new orders are placed. This guarantees there are only the freshest levels in the market.
  3. If the current spread is within the allowed threshold and the active position/order count is below the configured limit, the strategy places two symmetric pending orders:
    • Stop mode (default) places a buy stop above the market and a sell stop below it.
    • Limit mode places a buy limit below the market and a sell limit above it.
    • The Reverse Levels flag swaps the order types to replicate the original EA reverse switch.
  4. Entry prices are offset from the current bid/ask by the Pending Indent parameter. Orders are skipped when they are closer than the Min Step distance to existing positions.
  5. Pending orders can expire after a given number of minutes. When expiration is reached all remaining orders are cancelled.

Position management

  • Once an order is filled the strategy tracks the average entry price and volume for the corresponding side. Opposite fills reduce or close the existing position before opening a new one.
  • The strategy exits long positions when price hits any of these conditions:
    • Price touches the stop-loss distance below the average entry price.
    • Price reaches the take-profit distance above the average entry price.
    • A trailing stop is activated after the profit exceeds the activation threshold and subsequently price pulls back to the trailing level (moved in steps).
  • Short trades use the mirrored rules with inverted price comparisons.
  • When Only One Position is enabled the engine waits for the current exposure to be closed before new pending orders are placed.

Parameters

Name Description
StopLossPoints Distance to the protective stop-loss in points (0 disables it).
TakeProfitPoints Distance to the take-profit target in points (0 disables it).
MaxPositions Maximum number of simultaneously active positions and pending orders.
MinStepPoints Minimum distance between the entry price of existing trades and new pending orders.
TrailingActivatePoints Profit threshold that activates the trailing stop (0 disables trailing).
TrailingStopPoints Distance between the market price and trailing stop once activated.
TrailingStepPoints Minimum price improvement required to move the trailing stop again.
TradeMode Allowed direction for new pending orders: Buy, Sell, or BuySell.
PendingType Type of pending orders to place: Stop or Limit.
PendingExpirationMinutes Lifetime of pending orders in minutes (0 keeps them until filled or cancelled manually).
PendingIndentPoints Offset from the current bid/ask used to calculate pending order prices.
PendingMaxSpreadPoints Maximum allowed spread between bid and ask to place pending orders (0 disables the filter).
OnlyOnePosition If true, prevents opening new trades until the current position is closed.
ReverseLevels Swaps the placement of buy and sell orders to mirror the original EA reverse mode.
CandleType Time frame used to trigger signal evaluation (daily by default).

Notes

  • Price distances are expressed in points and automatically converted to the instrument tick size.
  • The strategy relies on StockSharp helper methods (BuyStop, SellStop, BuyLimit, SellLimit) for order registration and uses CancelActiveOrders to reset the book each time a new decision is made.
  • Trailing stop logic is evaluated on finished candles. For intrabar trailing behaviour use a shorter CandleType.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified from "Two pending orders 2" MetaTrader expert.
/// Places symmetric breakout levels around recent range and enters on breakout.
/// Uses high/low of N bars as breakout boundaries.
/// </summary>
public class TwoPendingOrders2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _lookback;

	private readonly Queue<decimal> _highs = new();
	private readonly Queue<decimal> _lows = new();

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int Lookback
	{
		get => _lookback.Value;
		set => _lookback.Value = value;
	}

	public TwoPendingOrders2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for breakout detection", "General");

		_lookback = Param(nameof(Lookback), 10)
			.SetGreaterThanZero()
			.SetDisplay("Lookback", "Number of bars for high/low range", "Indicators");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_highs.Clear();
		_lows.Clear();

		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 (_highs.Count < Lookback)
		{
			EnqueueCandle(candle);
			return;
		}

		decimal highest = decimal.MinValue;
		decimal lowest = decimal.MaxValue;
		var highs = _highs.ToArray();
		var lows = _lows.ToArray();
		foreach (var h in highs)
			if (h > highest) highest = h;
		foreach (var l in lows)
			if (l < lowest) lowest = l;

		var close = candle.ClosePrice;
		var volume = Volume;
		if (volume <= 0)
			volume = 1;
		var range = highest - lowest;
		var breakoutPadding = range * 0.05m;

		// Breakout above range
		if (close > highest + breakoutPadding)
		{
			if (Position <= 0)
				BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
		}
		// Breakout below range
		else if (close < lowest - breakoutPadding)
		{
			if (Position >= 0)
				SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
		}

		EnqueueCandle(candle);
	}

	private void EnqueueCandle(ICandleMessage candle)
	{
		_highs.Enqueue(candle.HighPrice);
		_lows.Enqueue(candle.LowPrice);

		if (_highs.Count > Lookback)
		{
			_highs.Dequeue();
			_lows.Dequeue();
		}
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_highs.Clear();
		_lows.Clear();

		base.OnReseted();
	}
}