Esta estrategia porta el Constituents EA de MQL/22595 a la API de alto nivel de StockSharp. Recrea la lógica original
de colocar dos órdenes pendientes alrededor del rango más reciente a una hora específica mientras mantiene el flujo de trabajo
compatible con el manejo de órdenes y los helpers de protección de riesgo de StockSharp.
Cómo funciona la estrategia
Activación programada – al final de cada vela la estrategia verifica si la siguiente barra comenzará en StartHour. Solo
en ese momento se consideran nuevas órdenes pendientes, lo que replica el código de MetaTrader que reaccionaba al nacimiento
de la barra cuyo tiempo de apertura coincide con la hora configurada.
Detección de rango – el máximo más alto y el mínimo más bajo entre las SearchDepth velas completadas anteriores se
rastrean con indicadores Highest/Lowest. Estos dos precios definen los niveles de ruptura/reversión a la media usados para
la colocación de órdenes.
Filtros de distancia de precio – las mejores cotizaciones bid/ask actuales se reciben del feed del libro de órdenes. Las
órdenes se colocan solo si la distancia entre la cotización y el precio candidato es mayor o igual a MinOrderDistancePips
(convertido a precio absoluto usando PointValue). Esto reimplementa la validación del nivel de congelación original y
previene órdenes pendientes inválidas.
Selección de estilo de orden – PendingOrderMode elige entre órdenes limitadas (buy limit en el mínimo, sell limit en el
máximo) u órdenes stop (buy stop encima del máximo, sell stop debajo del mínimo). Ambas órdenes se envían simultáneamente,
igual que en el script de MetaTrader.
Protección de riesgo – el helper integrado StartProtection adjunta niveles de stop-loss y take-profit expresados en pasos
de precio absolutos (StopLossPips/TakeProfitPips). Las verificaciones de distancia mínima contra MinStopDistancePips
replican el requisito de MT5 de que las órdenes protectoras deben respetar el nivel de stop del símbolo.
Gestión de órdenes – si una orden pendiente se ejecuta, la orden opuesta se cancela inmediatamente. Durante el intervalo
de la barra la estrategia nunca coloca órdenes adicionales mientras existen órdenes activas, coincidiendo con el comportamiento
del EA de origen.
Parámetros
Parámetro
Descripción
StartHour
Hora (0-23) cuando se crea el nuevo par de órdenes pendientes.
SearchDepth
Número de velas completadas previas usadas para calcular el rango máximo/mínimo.
PendingOrderMode
Limit replica la variante de reversión a la media, Stop coloca órdenes de ruptura.
StopLossPips
Distancia de stop-loss medida en pips (convertido con PointValue). Establecer en 0 para deshabilitar.
TakeProfitPips
Distancia de take-profit en pips. Establecer en 0 para deshabilitar.
PointValue
Valor del pip en unidades de precio. Establecer en 0 para auto-detectar desde Security.PriceStep/MinStep.
MinOrderDistancePips
Distancia mínima permitida entre bid/ask actual y el precio pendiente, modelando verificaciones de freeze-level.
MinStopDistancePips
Distancia mínima permitida para stop/take, reflejando verificaciones de StopsLevel.
CandleType
Marco temporal usado para el cálculo de rango y lógica de programación.
Strategy.Volume controla el tamaño de la orden; mantenerlo positivo para que BuyLimit, SellLimit, BuyStop y SellStop
puedan enviar órdenes.
Uso
Adjuntar la estrategia a un instrumento y establecer CandleType al marco temporal que se desea operar.
Configurar StartHour y SearchDepth exactamente como en las entradas de MT5. Ajustar los umbrales Min*Pips si el broker
aplica distancias mínimas entre órdenes y el precio de mercado.
Calibrar PointValue cuando la auto-detección de los metadatos del instrumento no sea posible (por ejemplo, en instrumentos
sintéticos).
Establecer StopLossPips y TakeProfitPips para que coincidan con el EA original. El módulo de protección adjuntará
automáticamente stops y objetivos una vez que se ejecute una orden.
Proporcionar un Volume positivo e iniciar la estrategia. Se suscribirá a velas y datos del libro de órdenes, colocará ambas
órdenes pendientes en la barra programada y cancelará la orden opuesta cuando se ejecute una operación.
Diferencias respecto al EA original
El modo de riesgo MoneyFixedMargin de MetaTrader (dimensionamiento basado en porcentaje) no está portado. Los usuarios de
StockSharp deben configurar Strategy.Volume directamente o envolver la estrategia con un módulo de dimensionamiento de
posición externo.
Las verificaciones de freeze-level y stop-level se expresan a través de los parámetros configurables MinOrderDistancePips y
MinStopDistancePips porque los metadatos equivalentes del exchange no siempre están disponibles a través de StockSharp.
La colocación de órdenes ocurre cuando la vela anterior cierra y la próxima barra comienza en StartHour. Esto es
funcionalmente idéntico a la implementación MT5 que se activaba en el nacimiento de la nueva barra.
Todos los comentarios dentro del código fuente han sido traducidos al inglés, mientras que la documentación externa está
disponible en varios idiomas por conveniencia.
Ajustar las distancias y la hora de trading para que coincidan con el instrumento que se planea operar. En mercados con spreads
amplios puede ser necesario aumentar MinOrderDistancePips o los valores de pip para evitar el rechazo inmediato por parte del
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()