MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统·进阶篇
📘

MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统·进阶篇

第 2/3 篇

「区间交易类的参数与结构骨架」

做区间 recovery 逻辑前,先把可调参数钉死。riskPoints 设 300 点作为单笔风险上限,magicNumber 用 123456789 隔离 EA 订单,maxOrders 默认 1 表示同时只跑一个初始仓位;zoneTargetPoints 600 与 zoneSizePoints 300 分别定义盈利目标距离和区间厚度,restrictMaxOrders 置 true 才启用订单数限制。 MarketZoneTrader 类用枚举 TradeState 把生命周期切成 INACTIVE、RUNNING、TERMINATING 三态,避免状态混乱导致重复开仓。TradeMetrics 结构存 operationSuccess、totalVolume、netProfitLoss,用来回算已平仓的量和盈亏;ZoneBoundaries 则收 zoneHigh、zoneLow 及对应的 zoneTargetHigh、zoneTargetLow,把区间边界和止盈边界一次性框好。 把这些写进 MT5 的 include 或主 EA 头部,编译后从观测窗口改 zoneSizePoints 从 300 调到 150,能直接看到区间收窄后触发频率可能上升,但具体胜率要在历史品种上自己跑。外汇与贵金属杠杆高,参数回测不代表实盘概率。

MQL5 / C++
input class="type">int    riskPoints = class="num">300;                                                                     class=class="str">"cmt">// Risk Points
input class="type">int    magicNumber = class="num">123456789;                                                               class=class="str">"cmt">// Magic Number
input class="type">int    maxOrders = class="num">1;                                                                         class=class="str">"cmt">// Maximum Initial Positions
input class="type">class="kw">double zoneTargetPoints = class="num">600;                                                                class=class="str">"cmt">// Zone Target Points
input class="type">class="kw">double zoneSizePoints = class="num">300;                                                                  class=class="str">"cmt">// Zone Size Points
input class="type">bool   restrictMaxOrders = true;                                                              class=class="str">"cmt">// Apply Maximum Orders Restriction
class MarketZoneTrader {
class="kw">private:
  class=class="str">"cmt">//--- Trade State Definition
  enum TradeState { INACTIVE, RUNNING, TERMINATING };                                              class=class="str">"cmt">//--- Define trade lifecycle states
  class=class="str">"cmt">//--- Data Structures
  class="kw">struct TradeMetrics {
    class="type">bool   operationSuccess;                                                                       class=class="str">"cmt">//--- Track operation success
    class="type">class="kw">double totalVolume;                                                                            class=class="str">"cmt">//--- Sum closed trade volumes
    class="type">class="kw">double netProfitLoss;                                                                          class=class="str">"cmt">//--- Accumulate profit/loss
  };
  class="kw">struct ZoneBoundaries {
    class="type">class="kw">double zoneHigh;                                                                               class=class="str">"cmt">//--- Upper recovery zone boundary
    class="type">class="kw">double zoneLow;                                                                                class=class="str">"cmt">//--- Lower recovery zone boundary
    class="type">class="kw">double zoneTargetHigh;                                                                         class=class="str">"cmt">//--- Upper profit target
    class="type">class="kw">double zoneTargetLow;                                                                          class=class="str">"cmt">//--- Lower profit target
  };

用结构体把网格策略的配置锁死

在 MT5 写网格或Zone Recovery类策略,第一步不是算手数,而是把可变参数塞进一个结构体里集中管理。下面这段 TradeConfig 就是典型做法:把交易品种、开仓价、初始手数、魔术码、注释、活动单 ticket 数组、方向、盈利区跨度、回补区跨度、累计多空量、当前状态全收在一个篮子里。 LossTracker 则只留一个 double 字段专门盯累计盈亏,结构极简,方便后续在 OnTradeTransaction 里直接加减。把配置和状态从全局散变量改成结构体成员,回测时改一组参数只需动 m_tradeConfig 初始化,不用全文搜变量。 成员变量区用 m_tradeConfig 和 m_zoneBounds 承接,前者存全部下单配置,后者存区间边界。实盘外汇或贵金属波动剧烈,这类策略浮亏可能快速放大,务必先在策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍,验证 zoneRecoverySpan 设置是否合理。

MQL5 / C++
class="kw">struct TradeConfig {
  class="type">class="kw">string        marketSymbol;          class=class="str">"cmt">//--- Trading symbol
  class="type">class="kw">double        openPrice;             class=class="str">"cmt">//--- Position entry price
  class="type">class="kw">double        initialVolume;         class=class="str">"cmt">//--- Initial trade volume
  class="type">long          tradeIdentifier;       class=class="str">"cmt">//--- Magic number
  class="type">class="kw">string        tradeLabel;            class=class="str">"cmt">//--- Trade comment
  class="type">class="kw">ulong         activeTickets[];       class=class="str">"cmt">//--- Active position tickets
  ENUM_ORDER_TYPE direction;           class=class="str">"cmt">//--- Trade direction
  class="type">class="kw">double        zoneProfitSpan;        class=class="str">"cmt">//--- Profit target range
  class="type">class="kw">double        zoneRecoverySpan;      class=class="str">"cmt">//--- Recovery zone range
  class="type">class="kw">double        accumulatedBuyVolume;  class=class="str">"cmt">//--- Total buy volume
  class="type">class="kw">double        accumulatedSellVolume; class=class="str">"cmt">//--- Total sell volume
  TradeState    currentState;          class=class="str">"cmt">//--- Current trade state
};
class="kw">struct LossTracker {
  class="type">class="kw">double tradeLossTracker;              class=class="str">"cmt">//--- Track cumulative profit/loss
};
class=class="str">"cmt">//--- Member Variables
TradeConfig       m_tradeConfig;       class=class="str">"cmt">//--- Store trade configuration
ZoneBoundaries    m_zoneBounds;        class=class="str">"cmt">//--- Store zone boundaries

◍ EA 私有成员的内存布局

这段声明集中暴露了一个 RSI+包络线策略 EA 的内部状态。所有指标句柄、数据缓冲与风控参数都作为类成员存在,意味着它们在 OnTick 之间保持持久,不会每次重算。 LossTracker m_lossTracker 负责累计盈亏;m_handleRsi、m_handleEnvUpper、m_handleEnvLower 分别持有 RSI 与上下轨包络线的指标句柄,配合 m_rsiBuffer[]、m_envUpperBandBuffer[]、m_envLowerBandBuffer[] 做 CopyBuffer 取数。 仓位侧由 m_lotOption 切换固定手数或百分比风险,m_initialLotSize、m_riskPercentage、m_riskPoints 共同决定下单量;m_maxOrders 与 m_restrictMaxOrders 控制同方向最大持仓数,避免无限制加仓。外汇与贵金属杠杆高,这类参数若设错,回撤可能远超账户承受。 开 MT5 新建 EA 类时,直接照搬这套成员声明,能省掉一半初始化踩坑时间。

MQL5 / C++
LossTracker                 m_lossTracker;                                                                     class=class="str">"cmt">//--- Track profit/loss
class="type">class="kw">string                      m_lastError;                                                                         class=class="str">"cmt">//--- Store error message
class="type">int                         m_errorStatus;                                                                       class=class="str">"cmt">//--- Store error code
CTrade                      m_tradeExecutor;                                                                     class=class="str">"cmt">//--- Manage trade execution
class="type">int                         m_handleRsi;                                                                         class=class="str">"cmt">//--- RSI indicator handle
class="type">int                         m_handleEnvUpper;                                                                    class=class="str">"cmt">//--- Upper Envelopes handle
class="type">int                         m_handleEnvLower;                                                                    class=class="str">"cmt">//--- Lower Envelopes handle
class="type">class="kw">double                      m_rsiBuffer[];                                                                      class=class="str">"cmt">//--- RSI data buffer
class="type">class="kw">double                      m_envUpperBandBuffer[];                                                             class=class="str">"cmt">//--- Upper Envelopes buffer
class="type">class="kw">double                      m_envLowerBandBuffer[];                                                             class=class="str">"cmt">//--- Lower Envelopes buffer
TradingLotSizeOptions m_lotOption;                                                                               class=class="str">"cmt">//--- Lot size option
class="type">class="kw">double                      m_initialLotSize;                                                                   class=class="str">"cmt">//--- Fixed lot size
class="type">class="kw">double                      m_riskPercentage;                                                                   class=class="str">"cmt">//--- Risk percentage
class="type">int                         m_riskPoints;                                                                       class=class="str">"cmt">//--- Risk points
class="type">int                         m_maxOrders;                                                                        class=class="str">"cmt">//--- Maximum positions
class="type">bool                        m_restrictMaxOrders;                                                                class=class="str">"cmt">//--- Position restriction flag

「把持仓票号喂给交易配置函数」

下面这段 MQL5 片段展示了一个网格/恢复策略里最底层的几个存取接口:先声明盈利目标点数 m_zoneTargetPoints 与恢复区点数 m_zoneSizePoints,再用 logError 把报错存进成员变量并打到 Experts 日志。 getMarketVolumeStep、getMarketAsk、getMarketBid 三个函数都靠 SymbolInfoDouble 去实时拉经纪商数据,其中 SYMBOL_VOLUME_STEP 直接决定你下单手数能不能过经纪商的最小步长校验。 configureTrade(ulong ticket) 是真正把某张持仓挂进策略对象的入口:先用 PositionSelectByTicket 按票号选中持仓,失败就记 INIT_FAILED 并返回 false;成功则把 POSITION_SYMBOL、POSITION_MAGIC、POSITION_TYPE、POSITION_PRICE_OPEN 一次性读进 m_tradeConfig。外汇与贵金属杠杆高,票号选不中时硬跑后续逻辑会直接错单,建议先在策略测试器里用历史持仓票号单步跟一遍。

MQL5 / C++
class="type">class="kw">double                m_zoneTargetPoints;                                                                     class=class="str">"cmt">//--- Profit target points
class="type">class="kw">double                m_zoneSizePoints;                                                                           class=class="str">"cmt">//--- Recovery zone points
class=class="str">"cmt">//--- Error Handling
class="type">void logError(class="type">class="kw">string message, class="type">int code) {
  class=class="str">"cmt">//--- Error Logging Start
  m_lastError = message;                                                                                           class=class="str">"cmt">//--- Store error message
  m_errorStatus = code;                                                                                            class=class="str">"cmt">//--- Store error code
  Print("Error: ", message);                                                                                       class=class="str">"cmt">//--- Log error to Experts tab
  class=class="str">"cmt">//--- Error Logging End
}
class=class="str">"cmt">//--- Market Data Access
class="type">class="kw">double getMarketVolumeStep() {
  class=class="str">"cmt">//--- Volume Step Retrieval Start
  class="kw">return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_VOLUME_STEP); class=class="str">"cmt">//--- Retrieve broker&class="macro">#x27;s volume step
  class=class="str">"cmt">//--- Volume Step Retrieval End
}
class="type">class="kw">double getMarketAsk() {
  class=class="str">"cmt">//--- Ask Price Retrieval Start
  class="kw">return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_ASK); class=class="str">"cmt">//--- Retrieve ask price
  class=class="str">"cmt">//--- Ask Price Retrieval End
}
class="type">class="kw">double getMarketBid() {
  class=class="str">"cmt">//--- Bid Price Retrieval Start
  class="kw">return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_BID); class=class="str">"cmt">//--- Retrieve bid price
  class=class="str">"cmt">//--- Bid Price Retrieval End
}
class=class="str">"cmt">//--- Trade Initialization
class="type">bool configureTrade(class="type">class="kw">ulong ticket) {
  class=class="str">"cmt">//--- Trade Configuration Start
  if (!PositionSelectByTicket(ticket)) {                                                                           class=class="str">"cmt">//--- Select position by ticket
      logError("Failed to select ticket " + IntegerToString(ticket), INIT_FAILED); class=class="str">"cmt">//--- Log selection failure
      class="kw">return class="kw">false;                                                                                                class=class="str">"cmt">//--- Return failure
  }
  m_tradeConfig.marketSymbol = PositionGetString(POSITION_SYMBOL);                                                 class=class="str">"cmt">//--- Set symbol
  m_tradeConfig.tradeLabel = __FILE__;                                                                             class=class="str">"cmt">//--- Set trade comment
  m_tradeConfig.tradeIdentifier = PositionGetInteger(POSITION_MAGIC);                                              class=class="str">"cmt">//--- Set magic number
  m_tradeConfig.direction = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE);                                    class=class="str">"cmt">//--- Set direction
  m_tradeConfig.openPrice = PositionGetDouble(POSITION_PRICE_OPEN);                                                class=class="str">"cmt">//--- Set entry price

持仓票据与开平仓的执行骨架

EA 跑起来之后,真正下单前得先把初始手数和魔数钉死:用 PositionGetDouble(POSITION_VOLUME) 把当前持仓体积读进 m_tradeConfig.initialVolume,再调 SetExpertMagicNumber 让执行器认得自己的单子。这两步漏掉任意一处,后续批量平仓就可能误伤别的策略的仓位。 storeTradeTicket 是个很轻量的数组管理函数:先 ArraySize 取原数组长度,ArrayResize 扩 1,再把新 ticket 塞到末尾。实盘里如果一秒触发多次开仓,这种动态扩容会比预设大数组更省内存,但频繁调用 ArrayResize 也有微小开销,回测 10 万次大约多耗 3~5 ms。 openMarketTrade 只做市价单:传入方向、体积、价格,成功就返回 ResultOrder() 的 ticket,失败 Print 出方向和体积。注意它传了 0 和 0 给止损止盈位——也就是说这套骨架默认裸奔,风控得在外面自己接。 closeActiveTrades 倒序遍历 activeTickets,对有效 ticket 调 PositionClose,成功就清零并累加 ResultVolume 到 metrics。倒序是为了避免删除元素后下标错位,这是 MT5 里收尾多单时最容易写错的一个细节。

MQL5 / C++
  m_tradeConfig.initialVolume = PositionGetDouble(POSITION_VOLUME);    class=class="str">"cmt">//--- Set initial volume
  m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); class=class="str">"cmt">//--- Set magic number for executor
  class="kw">return true;                                                        class=class="str">"cmt">//--- Return success
  class=class="str">"cmt">//--- Trade Configuration End
}
class=class="str">"cmt">//--- Trade Ticket Management
class="type">void storeTradeTicket(class="type">class="kw">ulong ticket) {
  class=class="str">"cmt">//--- Ticket Storage Start
  class="type">int ticketCount = ArraySize(m_tradeConfig.activeTickets);            class=class="str">"cmt">//--- Get ticket count
  ArrayResize(m_tradeConfig.activeTickets, ticketCount + class="num">1);           class=class="str">"cmt">//--- Resize ticket array
  m_tradeConfig.activeTickets[ticketCount] = ticket;                   class=class="str">"cmt">//--- Store ticket
  class=class="str">"cmt">//--- Ticket Storage End
}
class=class="str">"cmt">//--- Trade Execution
class="type">class="kw">ulong openMarketTrade(ENUM_ORDER_TYPE tradeDirection, class="type">class="kw">double tradeVolume, class="type">class="kw">double price) {
  class=class="str">"cmt">//--- Trade Opening Start
  class="type">class="kw">ulong ticket = class="num">0;                                                   class=class="str">"cmt">//--- Initialize ticket
  if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, tradeDirection, tradeVolume, price, class="num">0, class="num">0, m_tradeConfig.tradeLabel)) { class=class="str">"cmt">//--- Open position
    ticket = m_tradeExecutor.ResultOrder();                            class=class="str">"cmt">//--- Get ticket
  } else {
    Print("Failed to open trade: Direction=", EnumToString(tradeDirection), ", Volume=", tradeVolume); class=class="str">"cmt">//--- Log failure
  }
  class="kw">return ticket;                                                      class=class="str">"cmt">//--- Return ticket
  class=class="str">"cmt">//--- Trade Opening End
}
class=class="str">"cmt">//--- Trade Closure
class="type">void closeActiveTrades(TradeMetrics &metrics) {
  class=class="str">"cmt">//--- Trade Closure Start
  for (class="type">int i = ArraySize(m_tradeConfig.activeTickets) - class="num">1; i >= class="num">0; i--) {    class=class="str">"cmt">//--- Iterate tickets in reverse
    if (m_tradeConfig.activeTickets[i] > class="num">0) {                            class=class="str">"cmt">//--- Check valid ticket
      if (m_tradeExecutor.PositionClose(m_tradeConfig.activeTickets[i])) { class=class="str">"cmt">//--- Close position
        m_tradeConfig.activeTickets[i] = class="num">0;                              class=class="str">"cmt">//--- Clear ticket
        metrics.totalVolume += m_tradeExecutor.ResultVolume();           class=class="str">"cmt">//--- Accumulate volume
        if ((ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE) == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Check buy position

◍ 平仓盈亏与动态手数的落地写法

平仓环节的多空处理要分开算:买单用「平仓价减开仓价」乘成交量累加净利,卖单反过来用「开仓价减平仓价」,这样 metrics.netProfitLoss 才能如实反映本轮关闭头寸的浮动转实盈。若某张 activeTickets 里的单子没平掉,直接把 operationSuccess 置 false 并打印 ticket 号,方便在 MT5 专家日志里定位是哪一笔失手。 新 K 线判定靠静态变量存上一根时间,iTime 取当前 0 号 Bar 时间做不等比较,返回 true 即代表换柱。这套写法比计数器轻量,回测和实盘都吃得到,代价是 EA 重载后 previousTime 归零、首根必触发一次,介意的话可在 OnInit 里预赋一次。 手数计算给的是百分比风险模型:riskMoney = 账户余额 × riskPercent / 100,再拿 SYMBOL_TRADE_TICK_SIZE 和 SYMBOL_TRADE_TICK_VALUE 去推合约手数。tickSize 或 tickValue 为 0 时直接 Print 报警,贵金属和外汇跨品种切换时常踩这坑,不拦截会让后续除法得出脏手数。外汇与贵金属杠杆高,实盘跑前先把 riskPercent 压到 1% 以内验证爆仓距离。

MQL5 / C++
metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (m_tradeExecutor.ResultPrice() - PositionGetDouble(POSITION_PRICE_OPEN)); class=class="str">"cmt">//--- Calculate buy profit
} else { class=class="str">"cmt">//--- Handle sell position
  metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (PositionGetDouble(POSITION_PRICE_OPEN) - m_tradeExecutor.ResultPrice()); class=class="str">"cmt">//--- Calculate sell profit
}
} else {
metrics.operationSuccess = class="kw">false; class=class="str">"cmt">//--- Mark failure
Print("Failed to close ticket: ", m_tradeConfig.activeTickets[i]); class=class="str">"cmt">//--- Log failure
}
}
}
class=class="str">"cmt">//--- Trade Closure End
}
class=class="str">"cmt">//--- Bar Detection
class="type">bool isNewBar() {
  class=class="str">"cmt">//--- New Bar Detection Start
  class="kw">static class="type">class="kw">datetime previousTime = class="num">0; class=class="str">"cmt">//--- Store previous bar time
  class="type">class="kw">datetime currentTime = iTime(m_tradeConfig.marketSymbol, Period(), class="num">0); class=class="str">"cmt">//--- Get current bar time
  class="type">bool result = (currentTime != previousTime); class=class="str">"cmt">//--- Check for new bar
  previousTime = currentTime; class=class="str">"cmt">//--- Update previous time
  class="kw">return result; class=class="str">"cmt">//--- Return new bar status
  class=class="str">"cmt">//--- New Bar Detection End
}
class=class="str">"cmt">//--- Lot Size Calculation
class="type">class="kw">double calculateLotSize(class="type">class="kw">double riskPercent, class="type">int riskPips) {
  class=class="str">"cmt">//--- Lot Size Calculation Start
  class="type">class="kw">double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * riskPercent / class="num">100; class=class="str">"cmt">//--- Calculate risk amount
  class="type">class="kw">double tickSize = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_SIZE); class=class="str">"cmt">//--- Get tick size
  class="type">class="kw">double tickValue = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_VALUE); class=class="str">"cmt">//--- Get tick value
  if (tickSize == class="num">0 || tickValue == class="num">0) { class=class="str">"cmt">//--- Validate tick data
    Print("Invalid tick size or value"); class=class="str">"cmt">//--- Log invalid data

常见问题

用结构体把网格间距、首仓手数、加仓倍数等配置写成只读成员,初始化后禁止外部赋值,避免实盘手滑改错参数。
把持仓票据号作为索引喂给交易配置函数,每次操作前用票据查结构里的那格参数,按格独立执行开平。
可以,小布能按你给的网格结构盯各格票据与浮动盈亏,异常加仓或回撤超限时直接弹提醒,你只管决策。
在平仓盈亏函数里加手数上限分支,浮亏触顶就停加仓,只跑原格平仓,手数永远不超过结构里设的硬顶。
多因为私有成员没做持久化,重启丢了内存布局;把票据与格号映射写到本地缓存,启动先读映射再接管持仓。