Ver en GitHub

Stairs Strategy

The Stairs Strategy reproduces the behaviour of the original MetaTrader expert. It starts by placing symmetrical stop orders around the current ask price and then continuously rebuilds the grid around the most recent fill. Profits are accumulated in price steps (pips) without weighting by volume, exactly as in the source script. When a profit target is hit, the strategy liquidates all positions by market order, removes any pending stops, and resets the grid.

Trading logic

  1. When no positions are open, place a buy stop and a sell stop at a distance of ChannelSteps / 2 price steps above and below the current ask price.
  2. After the first stop order is filled, re-arm the grid around the executed price:
    • If there are fewer than two active stop orders, cancel the stale ones.
    • As long as the current bid price remains within half of the channel distance from the last entry, place a new buy stop and sell stop ChannelSteps away from the most recent fill.
    • When AddLots is enabled, increase the pending order volume by the base lot after each fill.
  3. Maintain two running lists with all long and short entries in order to reproduce the hedged basket used by the MT4 version.
  4. Compute the unrealised profit of the basket on every finished candle using the best bid for longs and the best ask for shorts. Distances are normalised by the instrument price step, mirroring the original point calculation.
  5. Trigger a full liquidation when either threshold is exceeded:
    • ProfitSteps – profit produced by the current symbol only.
    • CommonProfitSteps – profit across the entire basket.
  6. Liquidation sends market orders to close every long and short exposure separately. Pending stop orders are cancelled once the basket is flat.

Note: The original expert attached stop-loss levels when registering pending orders. StockSharp does not support per-order protective levels through the high-level API, therefore the port closes trades exclusively through the profit-based logic described above.

Parameters

Parameter Description Default
ChannelSteps Distance (in minimum price steps) between the symmetric stop orders. 1000
ProfitSteps Profit threshold (in steps) required to close the local basket. 1500
CommonProfitSteps Global profit threshold (in steps) that forces a full liquidation. 1000
AddLots When enabled, increase the next pending order volume by the base lot after each fill. true
BaseVolume Volume used for the very first pair of stop orders. 0.1m
CandleType Timeframe used for candle subscriptions and trade management. 1 minute

Implementation notes

  • Uses the StockSharp high-level API with SubscribeCandles() and Bind() to process finished candles only.
  • Tracks individual entries inside OnOwnTradeReceived so the profit calculation can mimic the hedging logic of the MQL version.
  • Profit thresholds operate on pure price-step distances, without multiplying by the executed volume, matching the way the MT4 expert summed pips.
  • All stop orders are created through BuyStop and SellStop, while exits are executed with market orders to keep the logic portable across data providers.
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Stairs grid strategy: places trades at regular ATR-based intervals,
/// adding to position on trending moves, closing on profit target.
/// </summary>
public class StairsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _atrLength;
	private readonly StrategyParam<decimal> _gridMultiplier;
	private readonly StrategyParam<int> _maxLayers;
	private readonly StrategyParam<decimal> _profitMultiplier;
	private readonly StrategyParam<int> _emaLength;

	private decimal _entryPrice;
	private decimal _lastGridPrice;
	private int _gridCount;
	private decimal _prevEma;

	public StairsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period for grid step.", "Indicators");

		_gridMultiplier = Param(nameof(GridMultiplier), 1.5m)
			.SetDisplay("Grid Multiplier", "ATR multiplier for grid step.", "Grid");

		_maxLayers = Param(nameof(MaxLayers), 5)
			.SetDisplay("Max Layers", "Maximum grid layers.", "Grid");

		_profitMultiplier = Param(nameof(ProfitMultiplier), 2.0m)
			.SetDisplay("Profit Multiplier", "ATR multiplier for profit target.", "Grid");

		_emaLength = Param(nameof(EmaLength), 20)
			.SetDisplay("EMA Length", "EMA for trend direction.", "Indicators");
	}

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

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

	public decimal GridMultiplier
	{
		get => _gridMultiplier.Value;
		set => _gridMultiplier.Value = value;
	}

	public int MaxLayers
	{
		get => _maxLayers.Value;
		set => _maxLayers.Value = value;
	}

	public decimal ProfitMultiplier
	{
		get => _profitMultiplier.Value;
		set => _profitMultiplier.Value = value;
	}

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_entryPrice = 0;
		_lastGridPrice = 0;
		_gridCount = 0;
		_prevEma = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var atr = new AverageTrueRange { Length = AtrLength };
		var ema = new ExponentialMovingAverage { Length = EmaLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(atr, ema, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, ema);
			DrawOwnTrades(area);
		}
	}

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

		if (atrVal <= 0 || _prevEma == 0)
		{
			_prevEma = emaVal;
			return;
		}

		var close = candle.ClosePrice;
		var gridStep = atrVal * GridMultiplier;
		var profitTarget = atrVal * ProfitMultiplier;

		// Check profit target
		if (Position > 0 && _entryPrice > 0)
		{
			if (close - _entryPrice >= profitTarget || close < emaVal)
			{
				SellMarket();
				_gridCount = 0;
				_entryPrice = 0;
				_lastGridPrice = 0;
			}
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (_entryPrice - close >= profitTarget || close > emaVal)
			{
				BuyMarket();
				_gridCount = 0;
				_entryPrice = 0;
				_lastGridPrice = 0;
			}
		}

		// Grid: add to winning direction
		if (Position > 0 && _lastGridPrice > 0 && _gridCount < MaxLayers)
		{
			if (close - _lastGridPrice >= gridStep)
			{
				BuyMarket();
				_lastGridPrice = close;
				_gridCount++;
			}
		}
		else if (Position < 0 && _lastGridPrice > 0 && _gridCount < MaxLayers)
		{
			if (_lastGridPrice - close >= gridStep)
			{
				SellMarket();
				_lastGridPrice = close;
				_gridCount++;
			}
		}

		// Initial entry based on trend
		if (Position == 0)
		{
			var emaRising = emaVal > _prevEma;
			var emaFalling = emaVal < _prevEma;

			if (emaRising && close > emaVal)
			{
				_entryPrice = close;
				_lastGridPrice = close;
				_gridCount = 0;
				BuyMarket();
			}
			else if (emaFalling && close < emaVal)
			{
				_entryPrice = close;
				_lastGridPrice = close;
				_gridCount = 0;
				SellMarket();
			}
		}

		_prevEma = emaVal;
	}
}