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
- 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.
- 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.
- Maintain two running lists with all long and short entries in order to reproduce the hedged basket used by the MT4 version.
- 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.
- Trigger a full liquidation when either threshold is exceeded:
ProfitSteps – profit produced by the current symbol only.
CommonProfitSteps – profit across the entire basket.
- 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import AverageTrueRange, ExponentialMovingAverage
class stairs_strategy(Strategy):
def __init__(self):
super(stairs_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period for grid step", "Indicators")
self._grid_multiplier = self.Param("GridMultiplier", 1.5) \
.SetDisplay("Grid Multiplier", "ATR multiplier for grid step", "Grid")
self._max_layers = self.Param("MaxLayers", 5) \
.SetDisplay("Max Layers", "Maximum grid layers", "Grid")
self._profit_multiplier = self.Param("ProfitMultiplier", 2.0) \
.SetDisplay("Profit Multiplier", "ATR multiplier for profit target", "Grid")
self._ema_length = self.Param("EmaLength", 20) \
.SetDisplay("EMA Length", "EMA for trend direction", "Indicators")
self._entry_price = 0.0
self._last_grid_price = 0.0
self._grid_count = 0
self._prev_ema = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def AtrLength(self):
return self._atr_length.Value
@property
def GridMultiplier(self):
return self._grid_multiplier.Value
@property
def MaxLayers(self):
return self._max_layers.Value
@property
def ProfitMultiplier(self):
return self._profit_multiplier.Value
@property
def EmaLength(self):
return self._ema_length.Value
def OnStarted2(self, time):
super(stairs_strategy, self).OnStarted2(time)
self._entry_price = 0.0
self._last_grid_price = 0.0
self._grid_count = 0
self._prev_ema = 0.0
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
self._ema = ExponentialMovingAverage()
self._ema.Length = self.EmaLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._atr, self._ema, self.ProcessCandle).Start()
def ProcessCandle(self, candle, atr_val, ema_val):
if candle.State != CandleStates.Finished:
return
atr_v = float(atr_val)
ema_v = float(ema_val)
if atr_v <= 0 or self._prev_ema == 0:
self._prev_ema = ema_v
return
close = float(candle.ClosePrice)
grid_step = atr_v * float(self.GridMultiplier)
profit_target = atr_v * float(self.ProfitMultiplier)
# Check profit target
if self.Position > 0 and self._entry_price > 0:
if close - self._entry_price >= profit_target or close < ema_v:
self.SellMarket()
self._grid_count = 0
self._entry_price = 0.0
self._last_grid_price = 0.0
elif self.Position < 0 and self._entry_price > 0:
if self._entry_price - close >= profit_target or close > ema_v:
self.BuyMarket()
self._grid_count = 0
self._entry_price = 0.0
self._last_grid_price = 0.0
# Grid: add to winning direction
max_layers = self.MaxLayers
if self.Position > 0 and self._last_grid_price > 0 and self._grid_count < max_layers:
if close - self._last_grid_price >= grid_step:
self.BuyMarket()
self._last_grid_price = close
self._grid_count += 1
elif self.Position < 0 and self._last_grid_price > 0 and self._grid_count < max_layers:
if self._last_grid_price - close >= grid_step:
self.SellMarket()
self._last_grid_price = close
self._grid_count += 1
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_ema = ema_v
return
# Initial entry based on trend
if self.Position == 0:
ema_rising = ema_v > self._prev_ema
ema_falling = ema_v < self._prev_ema
if ema_rising and close > ema_v:
self._entry_price = close
self._last_grid_price = close
self._grid_count = 0
self.BuyMarket()
elif ema_falling and close < ema_v:
self._entry_price = close
self._last_grid_price = close
self._grid_count = 0
self.SellMarket()
self._prev_ema = ema_v
def OnReseted(self):
super(stairs_strategy, self).OnReseted()
self._entry_price = 0.0
self._last_grid_price = 0.0
self._grid_count = 0
self._prev_ema = 0.0
def CreateClone(self):
return stairs_strategy()