Эта стратегия переносит советника Constituents EA из MQL/22595 в высокоуровневый API StockSharp. Логика построена так,
чтобы в заданный час размещать пару отложенных заявок вокруг последнего ценового диапазона и при этом использовать стандартные
возможности StockSharp по управлению рисками и заявками.
Как устроена логика
Проверка времени. В конце каждой свечи стратегия оценивает, начнётся ли следующая свеча в час StartHour. Только в этот
момент формируется новая пара отложенных заявок. Тем самым повторяется поведение MQL-версии, которая реагировала на рождение
бара с нужным временем открытия.
Определение диапазона. Индикаторы Highest и Lowest отслеживают максимум и минимум среди SearchDepth последних
завершённых свечей. Эти значения используются как уровни для лимитных либо стоповых заявок.
Контроль расстояния. Из книги заявок берутся лучшие бид/аск. Заявка отправляется только если расстояние между котировкой и
расчётной ценой не меньше MinOrderDistancePips, преобразованного в абсолютное значение через PointValue. Это аналог
проверки freeze level в исходном коде.
Тип отложенных заявок. Параметр PendingOrderMode выбирает, какие заявки выставлять: Limit — возврат к диапазону (buy
limit на минимуме, sell limit на максимуме), Stop — пробой диапазона (buy stop выше максимума, sell stop ниже минимума).
Защита позиции. Встроенная функция StartProtection подключает стоп-лосс и тейк-профит в абсолютных шагах цены. Перед
выставлением проверяется, что значения StopLossPips и TakeProfitPips удовлетворяют ограничению MinStopDistancePips, что
соответствует проверке StopsLevel в MetaTrader.
Управление ордерами. При исполнении одной заявки противоположная немедленно отменяется. Пока активны отложенные заявки,
новые заявки не формируются — поведение полностью совпадает с оригинальным советником.
Параметры
Параметр
Описание
StartHour
Час (0-23), в который создаётся новая пара отложенных заявок.
SearchDepth
Количество завершённых свечей для расчёта максимума и минимума.
PendingOrderMode
Limit — лимитные заявки, Stop — стоп-заявки.
StopLossPips
Размер стоп-лосса в пунктах. Значение 0 отключает защиту.
TakeProfitPips
Размер тейк-профита в пунктах. Значение 0 отключает цель.
PointValue
Стоимость пункта в единицах цены. При 0 определяется автоматически из PriceStep/MinStep.
MinOrderDistancePips
Минимальное расстояние между текущей ценой и отложенной заявкой (аналог freeze level).
MinStopDistancePips
Минимальное расстояние для стоп-лосса/тейк-профита (аналог StopsLevel).
CandleType
Таймфрейм, на котором выполняются все вычисления.
Размер позиции задаётся свойством Strategy.Volume. Оно должно быть положительным, иначе методы BuyLimit, SellLimit,
BuyStop и SellStop не смогут отправить заявки.
Рекомендации по использованию
Подключите стратегию к нужному инструменту и установите CandleType в требуемый таймфрейм.
Настройте StartHour и SearchDepth в соответствии с параметрами исходного советника MetaTrader.
При необходимости скорректируйте PointValue, если шаг цены инструмента не определяется автоматически (например, на
синтетических инструментах или CFD).
Установите StopLossPips, TakeProfitPips, MinOrderDistancePips и MinStopDistancePips согласно требованиям брокера.
Задайте объём в Volume и запустите стратегию. Она подпишется на свечи и стакан, выставит две отложенные заявки в нужный
момент и отменит противоположную после срабатывания одной из них.
Отличия от оригинальной реализации
Денежное управление MoneyFixedMargin (расчёт объёма в процентах от депозита) не перенесено. В StockSharp следует напрямую
управлять Volume или подключить внешний модуль риск-менеджмента.
Проверки freeze level и стоп-уровня вынесены в параметры MinOrderDistancePips и MinStopDistancePips, потому что не все
брокеры передают эти значения через API.
Срабатывание условий происходит на закрытии предыдущей свечи, если следующая начинается в StartHour. Это полностью
соответствует моменту выставления заявок в MQL-версии.
В коде и документации используются комментарии на английском языке, а описания доступны на трёх языках.
Подбирайте значения параметров под конкретный инструмент. Для инструментов с широким спредом, как правило, требуется увеличить
MinOrderDistancePips и расстояние стопов, чтобы избежать отклонения заявок брокером.
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()