GitHub で見る

Pending tread Grid Strategy

Overview

The Pending tread Grid Strategy is a faithful StockSharp port of the MetaTrader 4 expert advisor Pending_tread.mq4. The original EA constantly rebuilds two ladders of pending orders: one ladder above the market and one below. Each ladder can be configured to use buy or sell orders, and spacing is defined in pips. The StockSharp implementation reproduces the same behaviour through the high-level API without introducing additional indicators or collections.

Trading Logic

  1. Bid/ask driven maintenance – the strategy subscribes to Level 1 quotes (SubscribeLevel1) and keeps the latest bid and ask prices. Every time new data arrives, the maintenance routine runs (with a configurable throttle) and compares the existing pending orders with the configured grid size.
  2. Above-market ladder – depending on AboveMarketSide, the algorithm places either buy stop or sell limit orders at increments of PipStep pips above the market. Each new order receives its own take-profit level, offset by TakeProfitPips pips.
  3. Below-market ladder – the BelowMarketSide parameter selects between buy limit and sell stop orders stacked below the market. The same pip spacing and take-profit logic applies.
  4. Stop-level guard – the MinStopDistancePoints parameter emulates the MetaTrader MODE_STOPLEVEL check. Orders are skipped when the distance between the price and the relevant bid/ask anchor is smaller than the provided limit.
  5. ThrottleThrottleSeconds mirrors the original five-second throttle that avoided TRADE_CONTEXT_BUSY errors. Only one maintenance cycle is executed during that interval, regardless of how many ticks arrive.

All pip-based inputs (PipStep, TakeProfitPips) are converted into absolute price offsets using the instrument PriceStep and Decimals. Five-digit quotes automatically multiply the step by ten to match the MetaTrader "adjusted point" logic.

Parameters

Parameter Default Description
OrderVolume 0.01 Volume used when placing every pending order. Rounded to the instrument volume step before registration.
PipStep 12 Spacing between consecutive orders in the ladder, expressed in pips.
TakeProfitPips 10 Distance in pips used to place the take-profit for every pending order.
OrdersPerSide 10 Maximum number of active orders maintained above the market and below the market.
AboveMarketSide Buy Order type used above the market. Buy creates buy stop orders, Sell creates sell limit orders.
BelowMarketSide Sell Order type used below the market. Buy creates buy limit orders, Sell creates sell stop orders.
MinStopDistancePoints 0 Minimal distance (in raw points) allowed between the bid/ask and the pending price. Set this to the broker MODE_STOPLEVEL if needed.
ThrottleSeconds 5 Cooldown period between grid maintenance cycles.
SlippagePoints 3 Preserved for documentation parity; StockSharp pending orders do not use this value.

Implementation Notes

  • Uses only the StockSharp high-level helpers (SubscribeLevel1, BuyLimit, SellLimit, BuyStop, SellStop).
  • Prices are normalised through Security.ShrinkPrice so that the broker receives valid tick-aligned values.
  • Volume is adjusted to respect VolumeStep, MinVolume, and MaxVolume before each order is sent.
  • All diagnostic messages are routed through AddInfoLog / AddWarningLog, mirroring the verbose output of the MetaTrader script.
  • Python implementation is intentionally omitted, as requested.

Usage Tips

  1. Assign a liquid instrument and portfolio, then start the strategy. Pending ladders will appear instantly after the first level-1 update.
  2. Increase OrdersPerSide with caution: every additional rung results in another live pending order on the broker side.
  3. To mimic the original EA precisely, keep the default throttle at five seconds and configure MinStopDistancePoints with the broker's stop level requirement.
  4. Remember that StockSharp handles net positions; if opposite ladders are triggered simultaneously, resulting fills will partially offset each other rather than create hedged sub-positions.
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Pending grid strategy converted from the MetaTrader 4 expert advisor "Pending_tread".
/// Maintains two independent ladders of limit orders above and below the market with configurable direction and spacing.
/// When price reaches a grid level, a market order is placed in the configured direction.
/// </summary>
public class PendingTreadStrategy : Strategy
{
	private readonly StrategyParam<decimal> _pipStep;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _ordersPerSide;
	private readonly StrategyParam<Sides> _aboveMarketSide;
	private readonly StrategyParam<Sides> _belowMarketSide;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal _anchorPrice;
	private bool _initialized;
	private readonly List<decimal> _triggeredLevelsAbove = new();
	private readonly List<decimal> _triggeredLevelsBelow = new();
	private decimal _entryPrice;

	public PendingTreadStrategy()
	{
		_pipStep = Param(nameof(PipStep), 200000m)
			.SetGreaterThanZero()
			.SetDisplay("Grid step (pips)", "Distance between adjacent pending orders expressed in pips", "Trading");

		_takeProfitPips = Param(nameof(TakeProfitPips), 150000m)
			.SetGreaterThanZero()
			.SetDisplay("Take profit (pips)", "Individual take-profit distance assigned to every pending order", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order volume", "Volume sent with each pending order", "Trading");

		_ordersPerSide = Param(nameof(OrdersPerSide), 2)
			.SetGreaterThanZero()
			.SetDisplay("Orders per side", "Maximum number of grid levels maintained above and below the anchor", "Trading");

		_aboveMarketSide = Param(nameof(AboveMarketSide), Sides.Buy)
			.SetDisplay("Above market side", "Type of orders triggered above the current price", "Orders");

		_belowMarketSide = Param(nameof(BelowMarketSide), Sides.Sell)
			.SetDisplay("Below market side", "Type of orders triggered below the current price", "Orders");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle type", "Candle timeframe", "General");
	}

	public decimal PipStep
	{
		get => _pipStep.Value;
		set => _pipStep.Value = value;
	}

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	public int OrdersPerSide
	{
		get => _ordersPerSide.Value;
		set => _ordersPerSide.Value = value;
	}

	public Sides AboveMarketSide
	{
		get => _aboveMarketSide.Value;
		set => _aboveMarketSide.Value = value;
	}

	public Sides BelowMarketSide
	{
		get => _belowMarketSide.Value;
		set => _belowMarketSide.Value = value;
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_pipSize = 0m;
		_anchorPrice = 0m;
		_initialized = false;
		_triggeredLevelsAbove.Clear();
		_triggeredLevelsBelow.Clear();
		_entryPrice = 0m;
	}

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

		_pipSize = GetPipSize();

		this
			.SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var close = candle.ClosePrice;

		if (!_initialized)
		{
			_anchorPrice = close;
			_initialized = true;
			return;
		}

		var distance = PipStep * _pipSize;
		if (distance <= 0m)
			return;

		var tpOffset = TakeProfitPips * _pipSize;

		// Check above-market grid levels
		for (var i = 1; i <= OrdersPerSide; i++)
		{
			var level = _anchorPrice + distance * i;

			if (_triggeredLevelsAbove.Contains(level))
				continue;

			if (close >= level)
			{
				_triggeredLevelsAbove.Add(level);
				ExecuteGridOrder(AboveMarketSide, close, tpOffset);
				return; // one order per candle
			}
		}

		// Check below-market grid levels
		for (var i = 1; i <= OrdersPerSide; i++)
		{
			var level = _anchorPrice - distance * i;

			if (_triggeredLevelsBelow.Contains(level))
				continue;

			if (close <= level)
			{
				_triggeredLevelsBelow.Add(level);
				ExecuteGridOrder(BelowMarketSide, close, tpOffset);
				return; // one order per candle
			}
		}

		// Check take-profit for existing position
		CheckTakeProfit(close, tpOffset);
	}

	private void ExecuteGridOrder(Sides side, decimal price, decimal tpOffset)
	{
		// Close existing opposite position first
		if (Position != 0)
		{
			if ((Position > 0 && side == Sides.Sell) || (Position < 0 && side == Sides.Buy))
			{
				ClosePosition(side);
			}
		}

		var vol = OrderVolume;

		if (side == Sides.Buy)
		{
			BuyMarket(vol);
			_entryPrice = price;
		}
		else
		{
			SellMarket(vol);
			_entryPrice = price;
		}
	}

	private void ClosePosition(Sides newSide)
	{
		var absPos = Position.Abs();
		if (absPos <= 0)
			return;

		if (Position > 0)
			SellMarket(absPos);
		else
			BuyMarket(absPos);
	}

	private void CheckTakeProfit(decimal close, decimal tpOffset)
	{
		if (Position == 0 || _entryPrice == 0 || tpOffset <= 0)
			return;

		if (Position > 0 && close >= _entryPrice + tpOffset)
		{
			SellMarket(Position.Abs());
			_entryPrice = 0;

			// Reset grid to re-establish levels around current price
			ResetGrid(close);
		}
		else if (Position < 0 && close <= _entryPrice - tpOffset)
		{
			BuyMarket(Position.Abs());
			_entryPrice = 0;

			ResetGrid(close);
		}
	}

	private void ResetGrid(decimal newAnchor)
	{
		_anchorPrice = newAnchor;
		_triggeredLevelsAbove.Clear();
		_triggeredLevelsBelow.Clear();
	}

	private decimal GetPipSize()
	{
		var security = Security;
		if (security == null)
			return 0.01m;

		var step = security.PriceStep ?? 0.01m;
		return step > 0m ? step : 0.01m;
	}
}