Constituents EA 策略
该策略把 MQL/22595 中的 Constituents EA 移植到 StockSharp 的高级 API。它会在指定的小时附近,围绕最近的价
格区间自动挂出两张挂单,并利用 StockSharp 自带的风控工具来处理止损和止盈。
策略流程
- 时间过滤:每根 K 线收盘时,策略检查下一根 K 线是否会在
StartHour指定的小时开始。只有满足该条件时才会再次生成新的挂单,这一点与原始 MT5 程序完全一致。 - 区间计算:使用
Highest与Lowest指标跟踪最近SearchDepth根已完成 K 线的最高点与最低点,作为挂单的价 格水平。 - 距离限制:策略通过订阅盘口,实时获得最优买价/卖价。只有当挂单价格与当前报价之间的距离大于或等于
MinOrderDistancePips(按PointValue转换为绝对价格)时才会提交订单,从而复现原代码中的冻结区间检查。 - 挂单类型:
PendingOrderMode用于选择挂单类型。Limit表示在区间内做回归(低位 buy limit、高位 sell limit),Stop表示做突破(高位 buy stop、低位 sell stop)。两张挂单会同时提交。 - 风险控制:
StartProtection会根据StopLossPips与TakeProfitPips自动附加止损和止盈。参数MinStopDistancePips用来模拟 MT5 中的StopsLevel校验,避免止损/止盈距离过近。 - 订单管理:一旦其中一张挂单成交,另一张会立即撤单。在存在活跃挂单期间不会重复下单,这与 MetaTrader 的实 现一致。
参数说明
| 参数 | 说明 |
|---|---|
StartHour |
生成挂单的小时(0-23)。 |
SearchDepth |
计算最高价/最低价时所使用的已完成 K 线数量。 |
PendingOrderMode |
选择挂单类型:Limit 为回归挂单,Stop 为突破挂单。 |
StopLossPips |
止损距离(单位:点),0 表示不使用止损。 |
TakeProfitPips |
止盈距离(单位:点),0 表示不使用止盈。 |
PointValue |
每一个点对应的价格增量。设为 0 时将尝试从 PriceStep/MinStep 自动推断。 |
MinOrderDistancePips |
当前报价与挂单价格之间允许的最小距离,用于模拟交易商的冻结区间。 |
MinStopDistancePips |
止损/止盈必须满足的最小距离,用于模拟 StopsLevel 检查。 |
CandleType |
进行计算与调度时所使用的 K 线类型。 |
下单数量由 Strategy.Volume 控制,请确保该值为正,以便 BuyLimit、SellLimit、BuyStop、SellStop 能够提交订单。
使用步骤
- 将策略绑定到目标品种,并设置
CandleType(时间框架)。 - 根据原 MT5 参数调整
StartHour与SearchDepth,必要时修改Min*Pips以满足经纪商的最小距离要求。 - 如果自动推断
PointValue失败(例如交易合成品或差价合约),请手工设置点值。 - 设定
StopLossPips、TakeProfitPips、MinOrderDistancePips、MinStopDistancePips以符合交易规则。 - 设置
Volume后启动策略。系统会自动订阅 K 线与盘口,在指定时刻挂出两张订单,并在成交后撤销另一张挂单。
与原版 EA 的差异
- 原始 EA 的
MoneyFixedMargin风控(按账户百分比计算手数)未实现。请直接设置Volume,或在外部封装自己的风 控模块。 - 冻结区间与最小止损距离通过参数
MinOrderDistancePips、MinStopDistancePips控制,因为部分经纪商不会在数据中 提供这些限制。 - 策略在上一根 K 线收盘、并且下一根 K 线开盘时间等于
StartHour时提交挂单,这与 MetaTrader 的执行时机相同。 - 代码注释统一改为英文,文档提供英文、俄文和中文三个版本,便于跨语言使用。
在点差较大的市场中,通常需要增加 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()