This strategy ports the Constituents EA from MQL/22595 into the StockSharp high-level API. It recreates the original
logic of placing two pending orders around the most recent range at a specific hour while keeping the workflow compatible with
StockSharp order handling and risk protection helpers.
How the Strategy Works
Scheduled activation – at the end of each candle the strategy checks whether the next bar will start at StartHour. Only
at that moment are fresh pending orders considered, which mirrors the MetaTrader code that reacted to the birth of the bar
whose open time matches the configured hour.
Range detection – the highest high and lowest low among the previous SearchDepth completed candles are tracked with
Highest/Lowest indicators. These two prices define the breakout/mean-reversion levels used for order placement.
Price distance filters – current best bid/ask quotes are streamed from the order book feed. Orders are placed only if the
distance between the quote and the candidate price is greater than or equal to MinOrderDistancePips (converted to absolute
price using PointValue). This reimplements the original freeze-level validation and prevents invalid pending orders.
Order style selection – PendingOrderMode chooses between limit orders (buy limit at the low, sell limit at the high) or
stop orders (buy stop above the high, sell stop below the low). Both orders are submitted simultaneously, just like in the
MetaTrader script.
Risk protection – the built-in StartProtection helper attaches stop-loss and take-profit levels expressed in absolute
price steps (StopLossPips/TakeProfitPips). Minimum-distance checks against MinStopDistancePips replicate the MT5
requirement that protective orders must respect the symbol stop level.
Order management – if one pending order fills, the opposite order is cancelled immediately. During the bar interval the
strategy never places additional orders as long as active ones exist, matching the source EA behaviour.
Parameters
Parameter
Description
StartHour
Hour (0-23) when the new pair of pending orders is created.
SearchDepth
Number of previous completed candles used to compute the high/low range.
PendingOrderMode
Limit replicates the mean-reversion variant, Stop places breakout orders.
StopLossPips
Stop-loss distance measured in pips (converted with PointValue). Set to 0 to disable.
TakeProfitPips
Take-profit distance in pips. Set to 0 to disable.
PointValue
Pip value in price units. Set to 0 to auto-detect from Security.PriceStep/MinStep.
MinOrderDistancePips
Minimal allowed distance between current bid/ask and the pending price, modelling freeze-level checks.
Timeframe used for the range calculation and scheduling logic.
Strategy.Volume controls the order size; keep it positive so that BuyLimit, SellLimit, BuyStop, and SellStop can submit
orders.
Usage
Attach the strategy to a security and set CandleType to the timeframe you want to trade.
Configure StartHour and SearchDepth exactly as in the MT5 inputs. Adjust the Min*Pips thresholds if the broker enforces
minimum distances between orders and the market price.
Calibrate PointValue when auto-detection from the security metadata is not possible (for example, on synthetic instruments).
Set StopLossPips and TakeProfitPips to match the original EA. The protection module will automatically attach stops and
targets once an order fills.
Provide a positive Volume and start the strategy. It will subscribe to candles and order book data, place both pending orders
at the scheduled bar, and cancel the opposite order whenever one trade is executed.
Differences from the Original EA
The MetaTrader MoneyFixedMargin risk mode (percentage-based sizing) is not ported. StockSharp users should configure
Strategy.Volume directly or wrap the strategy with an external position sizing module.
Freeze-level and stop-level checks are expressed through the configurable MinOrderDistancePips and MinStopDistancePips
parameters because the equivalent exchange metadata is not always available via StockSharp.
Order placement occurs when the prior candle closes and the upcoming bar starts at StartHour. This is functionally identical
to the MT5 implementation that triggered on the birth of the new bar.
All comments inside the source have been translated into English, while the external documentation is available in three
languages for convenience.
Tune the distances and trading hour to match the instrument you plan to trade. On markets with wide spreads you may need larger
MinOrderDistancePips or pip values to avoid immediate rejection by the broker.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Constituents breakout strategy converted from the original MetaTrader expert advisor.
/// Detects the recent high/low range from N candles and enters with market orders
/// when price breaks above the high (buy) or below the low (sell).
/// Uses stop-loss, take-profit, and trailing stop for risk management.
/// </summary>
public class ConstituentsEAStrategy : Strategy
{
private readonly StrategyParam<int> _searchDepth;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<DataType> _candleType;
private Highest _highest = null!;
private Lowest _lowest = null!;
private decimal _pipSize;
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal _prevHigh;
private decimal _prevLow;
private bool _exitRequested;
/// <summary>
/// Number of completed candles used to determine the recent range.
/// </summary>
public int SearchDepth
{
get => _searchDepth.Value;
set => _searchDepth.Value = value;
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Working candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ConstituentsEaStrategy"/> class.
/// </summary>
public ConstituentsEAStrategy()
{
_searchDepth = Param(nameof(SearchDepth), 3)
.SetGreaterThanZero()
.SetDisplay("Search Depth", "Number of completed candles used to find extremes", "Setup");
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Stop loss distance expressed in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 100m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take profit distance expressed in pips", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Working timeframe used to evaluate highs/lows", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highest = null!;
_lowest = null!;
_pipSize = 0m;
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
_prevHigh = 0m;
_prevLow = 0m;
_exitRequested = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
_highest = new Highest { Length = SearchDepth };
_lowest = new Lowest { Length = SearchDepth };
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;
// Process indicators
var highValue = _highest.Process(new DecimalIndicatorValue(_highest, candle.HighPrice, candle.OpenTime) { IsFinal = true });
var lowValue = _lowest.Process(new DecimalIndicatorValue(_lowest, candle.LowPrice, candle.OpenTime) { IsFinal = true });
if (!_highest.IsFormed || !_lowest.IsFormed)
return;
var currentHigh = highValue.ToDecimal();
var currentLow = lowValue.ToDecimal();
// Manage existing position
if (Position != 0)
{
ManagePosition(candle);
// Update range for next trade
_prevHigh = currentHigh;
_prevLow = currentLow;
return;
}
// Check for breakout signals using previous range
if (_prevHigh > 0m && _prevLow > 0m)
{
// Breakout above the recent high -> buy
if (candle.ClosePrice > _prevHigh)
{
_entryPrice = candle.ClosePrice;
_exitRequested = false;
if (StopLossPips > 0m)
_stopPrice = _entryPrice - StopLossPips * _pipSize;
else
_stopPrice = null;
if (TakeProfitPips > 0m)
_takePrice = _entryPrice + TakeProfitPips * _pipSize;
else
_takePrice = null;
BuyMarket();
}
// Breakout below the recent low -> sell
else if (candle.ClosePrice < _prevLow)
{
_entryPrice = candle.ClosePrice;
_exitRequested = false;
if (StopLossPips > 0m)
_stopPrice = _entryPrice + StopLossPips * _pipSize;
else
_stopPrice = null;
if (TakeProfitPips > 0m)
_takePrice = _entryPrice - TakeProfitPips * _pipSize;
else
_takePrice = null;
SellMarket();
}
}
// Update range for next candle
_prevHigh = currentHigh;
_prevLow = currentLow;
}
private void ManagePosition(ICandleMessage candle)
{
if (_exitRequested)
return;
if (Position > 0)
{
// Check take profit
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
_exitRequested = true;
SellMarket();
return;
}
// Check stop loss
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
_exitRequested = true;
SellMarket();
return;
}
}
else if (Position < 0)
{
// Check take profit
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
_exitRequested = true;
BuyMarket();
return;
}
// Check stop loss
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
_exitRequested = true;
BuyMarket();
return;
}
}
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return 0.01m;
return step;
}
}
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.Indicators import Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class constituents_ea_strategy(Strategy):
def __init__(self):
super(constituents_ea_strategy, self).__init__()
self._search_depth = self.Param("SearchDepth", 3) \
.SetDisplay("Search Depth", "Number of completed candles used to find extremes", "Setup")
self._stop_loss_pips = self.Param("StopLossPips", 50.0) \
.SetDisplay("Stop Loss pips", "Stop loss distance expressed in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 100.0) \
.SetDisplay("Take Profit pips", "Take profit distance expressed in pips", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Working timeframe used to evaluate highs and lows", "General")
self._highest = None
self._lowest = None
self._pip_size = 0.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
self._prev_high = 0.0
self._prev_low = 0.0
self._exit_requested = False
@property
def search_depth(self):
return self._search_depth.Value
@property
def stop_loss_pips(self):
return self._stop_loss_pips.Value
@property
def take_profit_pips(self):
return self._take_profit_pips.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(constituents_ea_strategy, self).OnReseted()
self._highest = None
self._lowest = None
self._pip_size = 0.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
self._prev_high = 0.0
self._prev_low = 0.0
self._exit_requested = False
def OnStarted2(self, time):
super(constituents_ea_strategy, self).OnStarted2(time)
step = self.Security.PriceStep if self.Security is not None else None
self._pip_size = float(step) if step is not None and float(step) > 0 else 0.01
self._highest = Highest()
self._highest.Length = self.search_depth
self._lowest = Lowest()
self._lowest.Length = self.search_depth
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._highest, self._lowest, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, high_value, low_value):
if candle.State != CandleStates.Finished:
return
if not self._highest.IsFormed or not self._lowest.IsFormed:
return
current_high = float(high_value)
current_low = float(low_value)
if self.Position != 0:
self._manage_position(candle)
self._prev_high = current_high
self._prev_low = current_low
return
close = float(candle.ClosePrice)
sl_pips = float(self.stop_loss_pips)
tp_pips = float(self.take_profit_pips)
if self._prev_high > 0 and self._prev_low > 0:
if close > self._prev_high:
self._entry_price = close
self._exit_requested = False
self._stop_price = self._entry_price - sl_pips * self._pip_size if sl_pips > 0 else None
self._take_price = self._entry_price + tp_pips * self._pip_size if tp_pips > 0 else None
self.BuyMarket()
elif close < self._prev_low:
self._entry_price = close
self._exit_requested = False
self._stop_price = self._entry_price + sl_pips * self._pip_size if sl_pips > 0 else None
self._take_price = self._entry_price - tp_pips * self._pip_size if tp_pips > 0 else None
self.SellMarket()
self._prev_high = current_high
self._prev_low = current_low
def _manage_position(self, candle):
if self._exit_requested:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self.Position > 0:
if self._take_price is not None and high >= self._take_price:
self._exit_requested = True
self.SellMarket()
return
if self._stop_price is not None and low <= self._stop_price:
self._exit_requested = True
self.SellMarket()
return
elif self.Position < 0:
if self._take_price is not None and low <= self._take_price:
self._exit_requested = True
self.BuyMarket()
return
if self._stop_price is not None and high >= self._stop_price:
self._exit_requested = True
self.BuyMarket()
return
def CreateClone(self):
return constituents_ea_strategy()