The Rectangle Test strategy reproduces the MetaTrader "RectangleTest" expert using StockSharp's high-level API. It detects sideways ranges on an intraday time frame, checks whether two moving averages and the current price stay inside the detected range, and then trades breakouts away from the rectangle in the direction of the faster EMA. All logic is executed on completed candles received from a configurable candle source.
Trading Logic
Subscribe to the primary candle stream (default: 1-hour time frame) and feed it into the following indicators:
ExponentialMovingAverage (EMA) with configurable length EmaPeriod.
SimpleMovingAverage (SMA) with configurable length SmaPeriod.
Highest and Lowest indicators with length RangeCandles, configured to read candle highs and lows. They provide the rectangle boundaries that emulate the MetaTrader array-based calculations.
Once all indicators are formed, compute the rectangle height in percent relative to the upper boundary. Only candles where the height is smaller than RectangleSizePercent are considered valid consolidations.
Require the EMA, SMA, and candle close to remain inside the rectangle. This reproduces the sideways filter from the MQL version.
Short setup:
EMA is above the SMA.
Close price is above the EMA (matching the "Ask > EMA" condition from MetaTrader on completed candles).
Optional liquidation of an existing long happens first, after which a short market order is sent.
Long setup:
EMA is below the SMA.
Close price is below the EMA (mirroring the "Bid < EMA" rule).
Existing shorts are liquidated before opening the long.
Every entry records the expected entry price and volume. When the position reaches zero, the strategy compares the exit price with the stored entry price. Losing trades increase the daily loss counter, enforcing the MaxLosingTradesPerDay filter exactly like the MQL helper Loss().
Money and Risk Management
The strategy can work in two modes:
Risk-based mode (UseRiskMoneyManagement = true): position volume is sized from the account value, the RiskPercent, and the configured StopLossPoints. The calculation uses Security.PriceStep, Security.StepPrice, and Security.VolumeStep to mirror the MetaTrader lot sizing routine.
Fixed volume mode (UseRiskMoneyManagement = false): trades use the FixedVolume parameter.
After the net position changes from flat to non-zero, SetStopLoss and SetTakeProfit register protective orders using StopLossPoints and TakeProfitPoints (expressed in price steps), matching the SL/TP distances passed to m_trade.Sell/Buy in the original expert.
MaxLosingTradesPerDay stops new signals for the rest of the day once the specified number of losing trades has been detected.
Time Management
Trading is allowed only between TradeStartTime and TradeEndTime. The helper handles intervals that span midnight as well as daytime sessions.
When EnableTimeClose is true, all open positions are liquidated after TimeClose, replicating the MetaTrader "TimeCloseTrue" and TimeClose inputs.
Differences vs. MetaTrader Version
The original indicator created graphical rectangles on the chart. StockSharp does not create drawing objects; instead, the same range is calculated internally via Highest/Lowest indicators.
Losing trades are counted using closing prices from the signal candle. This matches the intention of Loss() (counting losing orders per day) while staying within high-level StockSharp abstractions.
Order filling characteristics such as ORDER_FILLING_FOK/IOC are handled by StockSharp's environment, so explicit filling-mode configuration is not required.
Parameters
Name
Default
Description
EmaPeriod
45
Period of the fast EMA.
SmaPeriod
200
Period of the slow SMA.
RangeCandles
10
Number of candles forming the rectangle.
RectangleSizePercent
0.5
Maximum rectangle height allowed for trading.
StopLossPoints
250
Stop-loss distance in price steps.
TakeProfitPoints
750
Take-profit distance in price steps.
UseRiskMoneyManagement
true
Toggle between risk-based and fixed volume.
RiskPercent
1
Percentage of account equity risked per trade.
FixedVolume
1
Fixed volume when risk-based sizing is disabled.
MaxLosingTradesPerDay
1
Daily cap on losing trades.
TradeStartTime
03:00
Time of day when entries are allowed.
TradeEndTime
22:50
Time of day after which no new entries are generated.
EnableTimeClose
false
Enables end-of-day liquidation.
TimeClose
23:00
Time of day to close all positions.
CandleType
1-hour candles
Primary candle data source.
Charting
If a chart area is available, the strategy draws the price candles, fast EMA, slow SMA, and own trades to visualize range breakouts and trade timing.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Rectangle breakout strategy: detects tight consolidation ranges and trades breakouts
/// using EMA/SMA trend direction as a filter.
/// </summary>
public class RectangleTestStrategy : Strategy
{
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _smaPeriod;
private readonly StrategyParam<int> _rangeCandles;
private readonly StrategyParam<decimal> _rectangleSizePercent;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _highs = new();
private readonly List<decimal> _lows = new();
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
public int SmaPeriod
{
get => _smaPeriod.Value;
set => _smaPeriod.Value = value;
}
public int RangeCandles
{
get => _rangeCandles.Value;
set => _rangeCandles.Value = value;
}
public decimal RectangleSizePercent
{
get => _rectangleSizePercent.Value;
set => _rectangleSizePercent.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public RectangleTestStrategy()
{
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetDisplay("Fast EMA Period", "Length of the fast EMA", "Indicators");
_smaPeriod = Param(nameof(SmaPeriod), 50)
.SetDisplay("Slow SMA Period", "Length of the slow SMA", "Indicators");
_rangeCandles = Param(nameof(RangeCandles), 10)
.SetDisplay("Rectangle Candles", "Number of candles for range detection", "Logic");
_rectangleSizePercent = Param(nameof(RectangleSizePercent), 10m)
.SetDisplay("Rectangle Size (%)", "Maximum range height in percent", "Logic");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary candle source", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var sma = new SimpleMovingAverage { Length = SmaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, sma, ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent));
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (_highs.Count >= RangeCandles)
{
// Use PREVIOUS window (excluding current candle) for rectangle detection
var highestValue = decimal.MinValue;
var lowestValue = decimal.MaxValue;
var startIdx = _highs.Count - RangeCandles;
for (var i = startIdx; i < _highs.Count; i++)
{
if (_highs[i] > highestValue) highestValue = _highs[i];
if (_lows[i] < lowestValue) lowestValue = _lows[i];
}
if (highestValue > 0m && lowestValue > 0m)
{
var rangePercent = (highestValue - lowestValue) / highestValue * 100m;
if (rangePercent > 0 && rangePercent < RectangleSizePercent)
{
var close = candle.ClosePrice;
// Breakout above rectangle with bullish trend (EMA > SMA)
if (close > highestValue && emaValue > smaValue && Position == 0)
{
BuyMarket();
}
// Breakout below rectangle with bearish trend (EMA < SMA)
else if (close < lowestValue && emaValue < smaValue && Position == 0)
{
SellMarket();
}
}
}
}
_highs.Add(candle.HighPrice);
_lows.Add(candle.LowPrice);
if (_highs.Count > RangeCandles + 1)
{
_highs.RemoveAt(0);
_lows.RemoveAt(0);
}
}
/// <inheritdoc />
protected override void OnReseted()
{
_highs.Clear();
_lows.Clear();
base.OnReseted();
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class rectangle_test_strategy(Strategy):
def __init__(self):
super(rectangle_test_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._ema_period = self.Param("EmaPeriod", 20)
self._sma_period = self.Param("SmaPeriod", 50)
self._range_candles = self.Param("RangeCandles", 10)
self._rectangle_size_percent = self.Param("RectangleSizePercent", 10.0)
self._highs = []
self._lows = []
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def EmaPeriod(self):
return self._ema_period.Value
@EmaPeriod.setter
def EmaPeriod(self, value):
self._ema_period.Value = value
@property
def SmaPeriod(self):
return self._sma_period.Value
@SmaPeriod.setter
def SmaPeriod(self, value):
self._sma_period.Value = value
@property
def RangeCandles(self):
return self._range_candles.Value
@RangeCandles.setter
def RangeCandles(self, value):
self._range_candles.Value = value
@property
def RectangleSizePercent(self):
return self._rectangle_size_percent.Value
@RectangleSizePercent.setter
def RectangleSizePercent(self, value):
self._rectangle_size_percent.Value = value
def OnReseted(self):
super(rectangle_test_strategy, self).OnReseted()
self._highs = []
self._lows = []
def OnStarted2(self, time):
super(rectangle_test_strategy, self).OnStarted2(time)
self._highs = []
self._lows = []
ema = ExponentialMovingAverage()
ema.Length = self.EmaPeriod
sma = SimpleMovingAverage()
sma.Length = self.SmaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, sma, self._process_candle).Start()
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent))
def _process_candle(self, candle, ema_value, sma_value):
if candle.State != CandleStates.Finished:
return
ema_val = float(ema_value)
sma_val = float(sma_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
range_candles = self.RangeCandles
if len(self._highs) >= range_candles:
start_idx = len(self._highs) - range_candles
highest = max(self._highs[start_idx:])
lowest = min(self._lows[start_idx:])
if highest > 0 and lowest > 0:
range_pct = (highest - lowest) / highest * 100.0
rect_size = float(self.RectangleSizePercent)
if 0 < range_pct < rect_size:
# Breakout above rectangle with bullish trend
if close > highest and ema_val > sma_val and self.Position == 0:
self.BuyMarket()
# Breakout below rectangle with bearish trend
elif close < lowest and ema_val < sma_val and self.Position == 0:
self.SellMarket()
self._highs.append(high)
self._lows.append(low)
while len(self._highs) > range_candles + 1:
self._highs.pop(0)
self._lows.pop(0)
def CreateClone(self):
return rectangle_test_strategy()