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

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

第 3/3 篇

◍ 从风险金额反推开仓手数的代码骨架

这段 MQL5 片段承接了前面按风险算手数的逻辑,核心在 lotValue 的算法:用风险点数乘 _Point 得到每点波动金额基准,再除以 tickSize 乘 tickValue,得出每手对应的风险价值。若 lotValue 为 0 直接返回 -1,避免后续除零崩单。 开仓函数 openOrder 里先按 ORDER_TYPE_BUY / ORDER_TYPE_SELL 分流取 ask / bid 价,并用 NormalizeDouble(..., Digits()) 对齐品种报价精度;非法订单类型打印日志并返回 -1。 手数变量 lotSize 初始化为 0,准备接前面 calculateLotSize 之类函数的返回值。外汇与贵金属杠杆高,这套按风险反推手数的逻辑在 MT5 里实测能压住单笔回撤,但滑点可能让实际亏损偏大,需自行校验 tickValue 取值。

MQL5 / C++
   class="kw">return -class="num">1;                 class=class="str">"cmt">//--- Return invalid lot
   }
   class="type">class="kw">double lotValue = (riskPips * _Point) / tickSize * tickValue;     class=class="str">"cmt">//--- Calculate lot value
   if (lotValue == class="num">0) {                                            class=class="str">"cmt">//--- Validate lot value
      Print("Invalid lot value");                                  class=class="str">"cmt">//--- Log invalid lot
      class="kw">return -class="num">1;                                                   class=class="str">"cmt">//--- Return invalid lot
   }
   class="kw">return NormalizeDouble(riskMoney / lotValue, class="num">2);                 class=class="str">"cmt">//--- Return normalized lot size
   class=class="str">"cmt">//--- Lot Size Calculation End
}
class=class="str">"cmt">//--- Order Execution
class="type">int openOrder(ENUM_ORDER_TYPE orderType, class="type">class="kw">double stopLoss, class="type">class="kw">double takeProfit) {
   class=class="str">"cmt">//--- Order Opening Start
   class="type">int ticket;                                                     class=class="str">"cmt">//--- Initialize ticket
   class="type">class="kw">double openPrice;                                               class=class="str">"cmt">//--- Initialize open price

   if (orderType == ORDER_TYPE_BUY) {                              class=class="str">"cmt">//--- Check buy order
      openPrice = NormalizeDouble(getMarketAsk(), Digits());       class=class="str">"cmt">//--- Set buy price
   } else if (orderType == ORDER_TYPE_SELL) {                      class=class="str">"cmt">//--- Check sell order
      openPrice = NormalizeDouble(getMarketBid(), Digits());       class=class="str">"cmt">//--- Set sell price
   } else {
      Print("Invalid order type");                                 class=class="str">"cmt">//--- Log invalid type
      class="kw">return -class="num">1;                                                   class=class="str">"cmt">//--- Return invalid ticket
   }

   class="type">class="kw">double lotSize = class="num">0;                                             class=class="str">"cmt">//--- Initialize lot size

开仓前的手数分支与失效拦截

EA 在下单前先判定手数模式:固定手数直接取 m_initialLotSize,动态手数则交给 calculateLotSize(),按风险百分比与止损点数反推。若算出的 lotSize ≤ 0,立刻 Print 报错并返回 -1,避免把废单丢给 broker。 真正发单走 m_tradeExecutor.PositionOpen(),传入品种、订单类型、手数、开仓价,滑点与止损止盈先填 0。成功就取 ResultOrder() 转成 ticket 并打印类型与成交量;失败则 ticket 置 -1 并留日志,调用方靠这个值判断有没有单。 构造函数里把状态锁在 INACTIVE、活跃 ticket 数组清空,再用 targetPts * _Point 算出以点数表达的盈利跨度。外汇与贵金属杠杆高,动态手数若 riskPct 设大,回撤可能瞬间吃掉保证金,MT5 里把这两参数调小一档更稳。

MQL5 / C++
if (m_lotOption == FIXED_LOTSIZE) {                 class=class="str">"cmt">//--- Check fixed lot
    lotSize = m_initialLotSize;                       class=class="str">"cmt">//--- Use fixed lot size
} else if (m_lotOption == UNFIXED_LOTSIZE) {          class=class="str">"cmt">//--- Check dynamic lot
    lotSize = calculateLotSize(m_riskPercentage, m_riskPoints); class=class="str">"cmt">//--- Calculate risk-based lot
}

if (lotSize <= class="num">0) {                                   class=class="str">"cmt">//--- Validate lot size
    Print("Invalid lot size: ", lotSize);             class=class="str">"cmt">//--- Log invalid lot
    class="kw">return -class="num">1;                                        class=class="str">"cmt">//--- Return invalid ticket
}

if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, class="num">0, class="num">0, __FILE__)) { class=class="str">"cmt">//--- Open position
    ticket = (class="type">int)m_tradeExecutor.ResultOrder();      class=class="str">"cmt">//--- Get ticket
    Print("New trade opened: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize); class=class="str">"cmt">//--- Log success
} else {
    ticket = -class="num">1;                                      class=class="str">"cmt">//--- Set invalid ticket
    Print("Failed to open order: Type=", EnumToString(orderType), ", Volume=", lotSize); class=class="str">"cmt">//--- Log failure
}

class="kw">return ticket;                                        class=class="str">"cmt">//--- Return ticket
class=class="str">"cmt">//--- Order Opening End
}
class="kw">public:
class=class="str">"cmt">//--- Constructor
MarketZoneTrader(TradingLotSizeOptions lotOpt, class="type">class="kw">double initLot, class="type">class="kw">double riskPct, class="type">int riskPts, class="type">int maxOrds, class="type">bool restrictOrds, class="type">class="kw">double targetPts, class="type">class="kw">double sizePts) {
class=class="str">"cmt">//--- Constructor Start
    m_tradeConfig.currentState = INACTIVE;            class=class="str">"cmt">//--- Set initial state
    ArrayResize(m_tradeConfig.activeTickets, class="num">0);      class=class="str">"cmt">//--- Initialize ticket array
    m_tradeConfig.zoneProfitSpan = targetPts * _Point; class=class="str">"cmt">//--- Set profit target

「构造函数里的参数落地与析构清理」

在 MarketZoneTrader 的构造函数里,一堆成员变量的初始化被集中完成,这决定了后续 zone 恢复交易的边界与风控基线。zoneRecoverySpan 直接用 sizePts 乘以 _Point 换算成真实价格跨度,意味着你在 EURUSD 上设 sizePts=50,恢复区就是 5 个点(50×0.0001),XAUUSD 上则是 0.5 美元——同一参数跨品种量级完全不同。 lossTracker 初始化为 0.0,表示新实例不继承任何历史浮亏;lotOption、initialLotSize、riskPercentage、riskPoints 分别接管手数模式与风险锚点。maxOrders 与 restrictMaxOrders 控制网格层数上限与是否硬限制,zoneTargetPoints 和 zoneSizePoints 框定盈利目标和区间宽度。 析构函数只做一件事:调 cleanup() 释放指标句柄。RSI 与上下轨 Envelopes 的句柄若不清,EA 重载时会漏句柄导致内存占用累积,MT5 终端跑几天可能报 'indicator cannot be created'。 开 MT5 把这段粘进类定义,改 sizePts 从 20 到 200 各挂一次回测,能直接看到恢复区跨度对成交频率的挤压效果。外汇与贵金属杠杆高,参数误设可能瞬间放大回撤,验证时先用模拟盘。

MQL5 / C++
m_tradeConfig.zoneRecoverySpan = sizePts * _Point;                 class=class="str">"cmt">//--- Set recovery zone
m_lossTracker.tradeLossTracker = class="num">0.0;                               class=class="str">"cmt">//--- Initialize loss tracker
m_lotOption = lotOpt;                                              class=class="str">"cmt">//--- Set lot size option
m_initialLotSize = initLot;                                        class=class="str">"cmt">//--- Set initial lot
m_riskPercentage = riskPct;                                        class=class="str">"cmt">//--- Set risk percentage
m_riskPoints = riskPts;                                            class=class="str">"cmt">//--- Set risk points
m_maxOrders = maxOrds;                                             class=class="str">"cmt">//--- Set max positions
m_restrictMaxOrders = restrictOrds;                                class=class="str">"cmt">//--- Set restriction flag
m_zoneTargetPoints = targetPts;                                    class=class="str">"cmt">//--- Set target points
m_zoneSizePoints = sizePts;                                        class=class="str">"cmt">//--- Set zone points
m_tradeConfig.marketSymbol = _Symbol;                              class=class="str">"cmt">//--- Set symbol
m_tradeConfig.tradeIdentifier = magicNumber;                       class=class="str">"cmt">//--- Set magic number
class=class="str">"cmt">//--- Constructor End
   }
   class=class="str">"cmt">//--- Destructor
   ~MarketZoneTrader() {
   class=class="str">"cmt">//--- Destructor Start
   cleanup();                                                      class=class="str">"cmt">//--- Release resources
   class=class="str">"cmt">//--- Destructor End
   }
class=class="str">"cmt">//--- Cleanup
class="type">void cleanup() {
   class=class="str">"cmt">//--- Cleanup Start
   IndicatorRelease(m_handleRsi);                                  class=class="str">"cmt">//--- Release RSI handle
   ArrayFree(m_rsiBuffer);                                         class=class="str">"cmt">//--- Free RSI buffer
   IndicatorRelease(m_handleEnvUpper);                             class=class="str">"cmt">//--- Release upper Envelopes handle

◍ 清理句柄与状态读取的落地写法

在 EA 销毁阶段,先释放 Envelopes 指标句柄再清空关联数组缓冲,能避免 MT5 终端在重载脚本时残留隐藏引用。下面这段释放逻辑直接对应上轨与下轨两套缓冲:ArrayFree 清掉 m_envUpperBandBuffer、m_envLowerBandBuffer,IndicatorRelease 关掉 m_handleEnvLower,顺序错了可能报 handle 占用。 状态与区间读取全部走 getter 返回成员变量,getCurrentState 吐出 m_tradeConfig.currentState,getZoneTargetHigh / Low 给盈利目标边界,getZoneHigh / Low 给回收区边界。这样外部调度层只管取值,不碰内部结构体。 initialize 里第一件事是 m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier),把魔数钉死;随后 PositionsTotal 拿总持仓数,按 ticket 逐仓 PositionSelectByTicket,再比对 POSITION_SYMBOL 与 POSITION_MAGIC 是否等于本 EA 的配置。实战中若魔数漏设,历史持仓不会被本 EA 识别,可能在外汇或贵金属上重复下单,这类品种杠杆高、滑点跳空频繁,务必在初始化就卡死标识。 开 MT5 把这段塞进你的类析构前和 OnInit 后,断点看 m_handleEnvLower 是否为 0,能立刻验证释放是否生效。

MQL5 / C++
  ArrayFree(m_envUpperBandBuffer);                                                                     class=class="str">"cmt">//--- Free upper Envelopes buffer
  IndicatorRelease(m_handleEnvLower);                                                                     class=class="str">"cmt">//--- Release lower Envelopes handle
  ArrayFree(m_envLowerBandBuffer);                                                                         class=class="str">"cmt">//--- Free lower Envelopes buffer
  class=class="str">"cmt">//--- Cleanup End
}
class=class="str">"cmt">//--- Getters
TradeState getCurrentState() {
  class=class="str">"cmt">//--- Get Current State Start
  class="kw">return m_tradeConfig.currentState;                                                                       class=class="str">"cmt">//--- Return trade state
  class=class="str">"cmt">//--- Get Current State End
}
class="type">class="kw">double getZoneTargetHigh() {
  class=class="str">"cmt">//--- Get Target High Start
  class="kw">return m_zoneBounds.zoneTargetHigh;                                                                      class=class="str">"cmt">//--- Return profit target high
  class=class="str">"cmt">//--- Get Target High End
}
class="type">class="kw">double getZoneTargetLow() {
  class=class="str">"cmt">//--- Get Target Low Start
  class="kw">return m_zoneBounds.zoneTargetLow;                                                                       class=class="str">"cmt">//--- Return profit target low
  class=class="str">"cmt">//--- Get Target Low End
}
class="type">class="kw">double getZoneHigh() {
  class=class="str">"cmt">//--- Get Zone High Start
  class="kw">return m_zoneBounds.zoneHigh;                                                                            class=class="str">"cmt">//--- Return recovery zone high
  class=class="str">"cmt">//--- Get Zone High End
}
class="type">class="kw">double getZoneLow() {
  class=class="str">"cmt">//--- Get Zone Low Start
  class="kw">return m_zoneBounds.zoneLow;                                                                             class=class="str">"cmt">//--- Return recovery zone low
  class=class="str">"cmt">//--- Get Zone Low End
}
class=class="str">"cmt">//--- Initialization
class="type">int initialize() {
  class=class="str">"cmt">//--- Initialization Start
  m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); class=class="str">"cmt">//--- Set magic number
  class="type">int totalPositions = PositionsTotal();                                                                   class=class="str">"cmt">//--- Get total positions
  
  for (class="type">int i = class="num">0; i < totalPositions; i++) {                                                               class=class="str">"cmt">//--- Iterate positions
     class="type">class="kw">ulong ticket = PositionGetTicket(i);                                                                  class=class="str">"cmt">//--- Get ticket
     if (PositionSelectByTicket(ticket)) {                                                                 class=class="str">"cmt">//--- Select position
       if (PositionGetString(POSITION_SYMBOL) == m_tradeConfig.marketSymbol && PositionGetInteger(POSITION_MAGIC) == m_tradeConfig.tradeIdentifier) { class=class="str">"cmt">//--- Check symbol and magic

指标句柄初始化与失败兜底

EA 启动阶段先把已有持仓尝试激活,再依次创建 RSI 与上下轨 Envelopes 的句柄。激活成功会打印 Ticket 号,失败也只记日志不中断,真正能叫停初始化的是指标句柄拿不到。 RSI 用的是当前周期、周期数 8、收盘价计算,拿到 INVALID_HANDLE 就直接 return INIT_FAILED,说明这套逻辑对动量通道的依赖性很高。上下轨 Envelopes 参数不对称:上轨 150 周期 SMA 偏移 0.1%,下轨 95 周期 SMA 偏移 1.4%,这种非对称包络更偏向捕捉贵金属或外汇中急拉不追、缓跌接刀的价格行为。 最后三行把 RSI 与上轨缓冲区设为时间序列(ArraySetAsSeries true),保证下标 0 是最新一根 K 线。你拷这段进 MT5 时,若日志只报 Failed to initialize 某指标,优先查 marketSymbol 是否和图表品种对得上。

MQL5 / C++
if (activateTrade(ticket)) {
      class=class="str">"cmt">//--- Activate position
    Print("Existing position activated: Ticket=", ticket);   class=class="str">"cmt">//--- Log activation
} else {
    Print("Failed to activate existing position: Ticket=", ticket); class=class="str">"cmt">//--- Log failure
}
      }
   }
  }

  m_handleRsi = iRSI(m_tradeConfig.marketSymbol, PERIOD_CURRENT, class="num">8, PRICE_CLOSE); class=class="str">"cmt">//--- Initialize RSI
  if (m_handleRsi == INVALID_HANDLE) {                     class=class="str">"cmt">//--- Check RSI
    Print("Failed to initialize RSI indicator");           class=class="str">"cmt">//--- Log failure
    class="kw">return INIT_FAILED;                                    class=class="str">"cmt">//--- Return failure
  }

  m_handleEnvUpper = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, class="num">150, class="num">0, MODE_SMA, PRICE_CLOSE, class="num">0.1); class=class="str">"cmt">//--- Initialize upper Envelopes
  if (m_handleEnvUpper == INVALID_HANDLE) {                class=class="str">"cmt">//--- Check upper Envelopes
    Print("Failed to initialize upper Envelopes indicator"); class=class="str">"cmt">//--- Log failure
    class="kw">return INIT_FAILED;                                    class=class="str">"cmt">//--- Return failure
  }

  m_handleEnvLower = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, class="num">95, class="num">0, MODE_SMA, PRICE_CLOSE, class="num">1.4); class=class="str">"cmt">//--- Initialize lower Envelopes
  if (m_handleEnvLower == INVALID_HANDLE) {                class=class="str">"cmt">//--- Check lower Envelopes
    Print("Failed to initialize lower Envelopes indicator"); class=class="str">"cmt">//--- Log failure
    class="kw">return INIT_FAILED;                                    class=class="str">"cmt">//--- Return failure
  }

  ArraySetAsSeries(m_rsiBuffer, true);                     class=class="str">"cmt">//--- Set RSI buffer
  ArraySetAsSeries(m_envUpperBandBuffer, true);            class=class="str">"cmt">//--- Set upper Envelopes buffer

「EA 生命周期与状态重置的落地写法」

MT5 的 EA 骨架里,OnInit 只做一件事:把全局 trader 实例 new 出来并转交初始化。下面这行把参数一股脑塞进构造函数,返回 trader.initialize() 的结果,INIT_SUCCEEDED 才会让 EA 正式跑起来。 ArraySetAsSeries(m_envLowerBandBuffer, true); 这句把下轨缓冲设为时间序列(最新价在索引 0),Envelopes 类指标必须这么处理,否则画出来的带子会左右颠倒。 OnDeinit 里先判 trader != NULL 再 delete 并置空,是防止重复释放导致 MT5 终端崩溃的硬习惯。日志打 'EA deinitialized' 方便你回看后台到底有没有干净退出。 activateTrade(ulong ticket) 是持仓激活入口:一进来先把 currentState 置 INACTIVE、ArrayResize(activeTickets,0) 清空 ticket 数组、tradeLossTracker 归 0.0,等于把上一次交易的所有痕迹抹掉再重来。外汇和贵金属波动大、杠杆高,这种重置漏一项就可能让旧亏损混入新单,风险不小。 复制下面这段代码到你的 EA 里,挂 EURUSD 的 M5 图表,改完参数直接看终端日志会不会依次冒出 initialized / deinitialized,能验证生命周期对不对。

MQL5 / C++
ArraySetAsSeries(m_envLowerBandBuffer, true);              class=class="str">"cmt">//--- Set lower Envelopes buffer

Print("EA initialized successfully");                        class=class="str">"cmt">//--- Log success
class="kw">return INIT_SUCCEEDED;                                        class=class="str">"cmt">//--- Return success
class=class="str">"cmt">//--- Initialization End
}
class=class="str">"cmt">//--- Global Instance
MarketZoneTrader *trader = NULL;                              class=class="str">"cmt">//--- Declare trader instance
class="type">int OnInit() {
  class=class="str">"cmt">//--- EA Initialization Start
  trader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, maxOrders, restrictMaxOrders, zoneTargetPoints, zoneSizePoints); class=class="str">"cmt">//--- Create trader instance
  class="kw">return trader.initialize();                                 class=class="str">"cmt">//--- Initialize EA
  class=class="str">"cmt">//--- EA Initialization End
}
class="type">void OnDeinit(class="kw">const class="type">int reason) {
  class=class="str">"cmt">//--- EA Deinitialization Start
  if (trader != NULL) {                                       class=class="str">"cmt">//--- Check trader existence
    class="kw">delete trader;                                            class=class="str">"cmt">//--- Delete trader
    trader = NULL;                                            class=class="str">"cmt">//--- Clear pointer
    Print("EA deinitialized");                                class=class="str">"cmt">//--- Log deinitialization
  }
  class=class="str">"cmt">//--- EA Deinitialization End
}
class=class="str">"cmt">//--- Position Management
class="type">bool activateTrade(class="type">class="kw">ulong ticket) {
  class=class="str">"cmt">//--- Position Activation Start
  m_tradeConfig.currentState = INACTIVE;                      class=class="str">"cmt">//--- Set state to inactive
  ArrayResize(m_tradeConfig.activeTickets, class="num">0);                class=class="str">"cmt">//--- Clear tickets
  m_lossTracker.tradeLossTracker = class="num">0.0;                       class=class="str">"cmt">//--- Reset loss tracker
  if (!configureTrade(ticket)) {                              class=class="str">"cmt">//--- Configure trade

◍ 挂单激活后区间与状态的初始化

开仓成功并把 ticket 存好之后,系统要根据方向把回收区间(zone)的上下边界算出来。买方向的锚点是成交价 openPrice,zoneHigh 直接等于 openPrice,zoneLow 再减去 zoneRecoverySpan;卖方向反过来,zoneLow 等于 openPrice,zoneHigh 加上 zoneRecoverySpan。 成交量累加变量也在这里归位:买方向把 accumulatedBuyVolume 设为 initialVolume,accumulatedSellVolume 清 0;卖方向对称处理。随后无论多空,都用 zoneProfitSpan 向外推出 zoneTargetHigh 和 zoneTargetLow,并把 currentState 切到 RUNNING,函数返回 true。 行情处理入口 processTick() 先 NormalizeDouble 取 ask / bid 到当前品种小数位。若 isNewBar() 为假直接 return,只在每根 K 线起步才往下走;接着 CopyBuffer 拉 RSI 缓冲(0 号线、从 0 取 3 根),失败就 Print 报错并回退。外汇与贵金属杠杆高,这类状态机任一步错配都可能让仓位失控,上 MT5 用策略测试器单步跟一遍最稳妥。

MQL5 / C++
   class="kw">return class="kw">false;                                                 class=class="str">"cmt">//--- Return failure
   }
   storeTradeTicket(ticket);                                       class=class="str">"cmt">//--- Store ticket
   if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                class=class="str">"cmt">//--- Handle buy position
      m_zoneBounds.zoneHigh = m_tradeConfig.openPrice;             class=class="str">"cmt">//--- Set zone high
      m_zoneBounds.zoneLow = m_zoneBounds.zoneHigh - m_tradeConfig.zoneRecoverySpan; class=class="str">"cmt">//--- Set zone low
      m_tradeConfig.accumulatedBuyVolume = m_tradeConfig.initialVolume; class=class="str">"cmt">//--- Set buy volume
      m_tradeConfig.accumulatedSellVolume = class="num">0.0;                   class=class="str">"cmt">//--- Reset sell volume
   } else {                                                        class=class="str">"cmt">//--- Handle sell position
      m_zoneBounds.zoneLow = m_tradeConfig.openPrice;              class=class="str">"cmt">//--- Set zone low
      m_zoneBounds.zoneHigh = m_zoneBounds.zoneLow + m_tradeConfig.zoneRecoverySpan; class=class="str">"cmt">//--- Set zone high
      m_tradeConfig.accumulatedSellVolume = m_tradeConfig.initialVolume; class=class="str">"cmt">//--- Set sell volume
      m_tradeConfig.accumulatedBuyVolume = class="num">0.0;                    class=class="str">"cmt">//--- Reset buy volume
   }
   m_zoneBounds.zoneTargetHigh = m_zoneBounds.zoneHigh + m_tradeConfig.zoneProfitSpan; class=class="str">"cmt">//--- Set target high
   m_zoneBounds.zoneTargetLow = m_zoneBounds.zoneLow - m_tradeConfig.zoneProfitSpan; class=class="str">"cmt">//--- Set target low
   m_tradeConfig.currentState = RUNNING;                           class=class="str">"cmt">//--- Set state to running
   class="kw">return true;                                                    class=class="str">"cmt">//--- Return success
   class=class="str">"cmt">//--- Position Activation End
}
class=class="str">"cmt">//--- Tick Processing
class="type">void processTick() {
   class=class="str">"cmt">//--- Tick Processing Start
   class="type">class="kw">double askPrice = NormalizeDouble(getMarketAsk(), Digits());    class=class="str">"cmt">//--- Get ask price
   class="type">class="kw">double bidPrice = NormalizeDouble(getMarketBid(), Digits());    class=class="str">"cmt">//--- Get bid price

   if (!isNewBar()) class="kw">return;                                        class=class="str">"cmt">//--- Exit if not new bar

   if (!CopyBuffer(m_handleRsi, class="num">0, class="num">0, class="num">3, m_rsiBuffer)) {           class=class="str">"cmt">//--- Load RSI data
      Print("Error loading RSI data. Reverting.");                class=class="str">"cmt">//--- Log RSI failure

envelopes 与 RSI 共振的开单判定

这段逻辑紧接指标句柄校验之后,先通过 CopyBuffer 把上下轨各取 3 根缓冲,任一加载失败就 Print 报错并直接 return,避免脏数据进场。 代码里把 RSI 超买超卖写死为 70 / 30,并用 m_rsiBuffer[1]<30 且 [2]>30 且 [0]<30 捕捉「上一根刚跌破、这根还在下方」的弱势反抽失败形态;只有 askPrice 同时刺破上 envelope 轨,才视为多头确认。 卖侧完全镜像:RSI 在 70 上方钝化、bidPrice 跌破下轨,且受 m_restrictMaxOrders 与 m_maxOrders 约束,PositionsTotal() 超限就放弃发单。外汇与贵金属杠杆高,这类共振信号也只在概率上倾向顺势,实盘前请在 MT5 策略测试器用 2020—2023 年 H1 数据跑一遍样本外验证。

MQL5 / C++
  class="kw">return;                                                                           class=class="str">"cmt">//--- Exit
   }

   if (!CopyBuffer(m_handleEnvUpper, class="num">0, class="num">0, class="num">3, m_envUpperBandBuffer)) { class=class="str">"cmt">//--- Load upper Envelopes
      Print("Error loading upper envelopes data. Reverting.");                 class=class="str">"cmt">//--- Log failure
      class="kw">return;                                                                   class=class="str">"cmt">//--- Exit
   }

   if (!CopyBuffer(m_handleEnvLower, class="num">1, class="num">0, class="num">3, m_envLowerBandBuffer)) { class=class="str">"cmt">//--- Load lower Envelopes
      Print("Error loading lower envelopes data. Reverting.");                 class=class="str">"cmt">//--- Log failure
      class="kw">return;                                                                   class=class="str">"cmt">//--- Exit
   }

   class="type">int ticket = class="num">0;                                                              class=class="str">"cmt">//--- Initialize ticket

   class="kw">const class="type">int rsiOverbought = class="num">70;                                                class=class="str">"cmt">//--- Set RSI overbought level
   class="kw">const class="type">int rsiOversold = class="num">30;                                                  class=class="str">"cmt">//--- Set RSI oversold level

   if (m_rsiBuffer[class="num">1] < rsiOversold && m_rsiBuffer[class="num">2] > rsiOversold && m_rsiBuffer[class="num">0] < rsiOversold) { class=class="str">"cmt">//--- Check buy signal
      if (askPrice > m_envUpperBandBuffer[class="num">0]) {                                 class=class="str">"cmt">//--- Confirm price above upper Envelopes
         if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) {          class=class="str">"cmt">//--- Check position limit
            ticket = openOrder(ORDER_TYPE_BUY, class="num">0, class="num">0);                           class=class="str">"cmt">//--- Open buy order
         }
      }
   } else if (m_rsiBuffer[class="num">1] > rsiOverbought && m_rsiBuffer[class="num">2] < rsiOverbought && m_rsiBuffer[class="num">0] > rsiOverbought) { class=class="str">"cmt">//--- Check sell signal
      if (bidPrice < m_envLowerBandBuffer[class="num">0]) {                                 class=class="str">"cmt">//--- Confirm price below lower Envelopes
         if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) {          class=class="str">"cmt">//--- Check position limit

「挂单激活与tick流转的衔接逻辑」

这段代码片段展示了 EA 在 OnTick 主循环里如何把开仓 ticket 与持仓激活串起来。当 openOrder 返回大于 0 的 ticket 时,说明 sell 单已递交,紧接着调用 activateTrade(ticket) 尝试激活持仓,并打印对应日志;若激活失败也会留下失败记录,方便在 MT5 专家日志里直接排查。 OnTick 本身只做一件事:确认 trader 对象非空后调用 processTick(),把每笔报价交给策略核心处理。这种把'收单—激活—tick分发'拆开写的做法,能让你在 MT5 策略测试器里单步跟踪订单生命周期,而不是把所有逻辑糊在 OnTick 里。 evaluateMarketTick 则按状态机拦截:当前状态为 INACTIVE 直接 return,为 TERMINATING 时调用 finalizePosition 平仓并退出。finalizePosition 先把状态置为 TERMINATING,初始化 TradeMetrics(operationSuccess=true, 浮盈0.0, 滑点0.0),再 closeActiveTrades 统一清场——外汇与贵金属杠杆高,这类显式终止态能降低半开仓导致的风控盲区。

MQL5 / C++
ticket = openOrder(ORDER_TYPE_SELL, class="num">0, class="num">0);      class=class="str">"cmt">//--- Open sell order
   }
  }
 }

 if (ticket > class="num">0) {                                              class=class="str">"cmt">//--- Check if trade opened
    if (activateTrade(ticket)) {                                class=class="str">"cmt">//--- Activate position
       Print("New position activated: Ticket=", ticket);        class=class="str">"cmt">//--- Log activation
    } else {
       Print("Failed to activate new position: Ticket=", ticket); class=class="str">"cmt">//--- Log failure
    }
  }
  class=class="str">"cmt">//--- Tick Processing End
}
class="type">void OnTick() {
  class=class="str">"cmt">//--- Tick Handling Start
  if (trader != NULL) {                                         class=class="str">"cmt">//--- Check trader existence
   trader.processTick();                                        class=class="str">"cmt">//--- Process tick
  }
  class=class="str">"cmt">//--- Tick Handling End
}
class=class="str">"cmt">//--- Market Tick Evaluation
class="type">void evaluateMarketTick() {
  class=class="str">"cmt">//--- Tick Evaluation Start
  if (m_tradeConfig.currentState == INACTIVE) class="kw">return;           class=class="str">"cmt">//--- Exit if inactive
  if (m_tradeConfig.currentState == TERMINATING) {              class=class="str">"cmt">//--- Check terminating state
   finalizePosition();                                          class=class="str">"cmt">//--- Finalize position
   class="kw">return;                                                      class=class="str">"cmt">//--- Exit
  }
}
class=class="str">"cmt">//--- Position Finalization
class="type">bool finalizePosition() {
  class=class="str">"cmt">//--- Position Finalization Start
  m_tradeConfig.currentState = TERMINATING;                    class=class="str">"cmt">//--- Set terminating state
  TradeMetrics metrics = {true, class="num">0.0, class="num">0.0};                      class=class="str">"cmt">//--- Initialize metrics
  closeActiveTrades(metrics);                                   class=class="str">"cmt">//--- Close all trades
  if (metrics.operationSuccess) {                               class=class="str">"cmt">//--- Check success

◍ 平仓与回补触发的位置判定

下面这段逻辑处理的是多头持仓的收尾与回补分支。当持仓方向为买入时,先取当前买价(Bid),若买价高于预设的目标上界 zoneTargetHigh,则直接调用 finalizePosition() 平仓,并在日志打印具体数值便于复盘。 若买价跌破 zoneLow,则判定为回补触发,按反向(卖出)调用 triggerRecoveryTrade()。这里 zoneTargetHigh 与 zoneLow 都是外部传入的区间边界,实战中建议在 MT5 策略测试器里把这两个值打印出来核对,避免肉眼看 K 线误判。 finalizePosition() 内部会先把 activeTickets 数组清零、状态置为 INACTIVE,再返回操作成功率。回补函数 triggerRecoveryTrade() 则先初始化交易指标结构体,调用 closeActiveTrades() 平掉已有仓位,并用 for 循环最多重试 10 次直至 operationSuccess 为真——外汇与贵金属波动剧烈,这种重试机制可能降低滑点造成的平仓失败概率,但高风险依旧存在。

MQL5 / C++
   ArrayResize(m_tradeConfig.activeTickets, class="num">0);                     class=class="str">"cmt">//--- Clear tickets
   m_tradeConfig.currentState = INACTIVE;                             class=class="str">"cmt">//--- Set inactive state
   Print("Position closed successfully");                             class=class="str">"cmt">//--- Log success
   } else {
      Print("Failed to close position");                              class=class="str">"cmt">//--- Log failure
   }
   class="kw">return metrics.operationSuccess;                                   class=class="str">"cmt">//--- Return status
   class=class="str">"cmt">//--- Position Finalization End
}

class="type">class="kw">double currentPrice;                                                 class=class="str">"cmt">//--- Initialize price
if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                      class=class="str">"cmt">//--- Handle buy position
   currentPrice = getMarketBid();                                     class=class="str">"cmt">//--- Get bid price
   if (currentPrice > m_zoneBounds.zoneTargetHigh) {                  class=class="str">"cmt">//--- Check profit target
      Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); class=class="str">"cmt">//--- Log closure
      finalizePosition();                                             class=class="str">"cmt">//--- Close position
      class="kw">return;                                                         class=class="str">"cmt">//--- Exit
   } else if (currentPrice < m_zoneBounds.zoneLow) {                  class=class="str">"cmt">//--- Check recovery trigger
      Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); class=class="str">"cmt">//--- Log recovery
      triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice);            class=class="str">"cmt">//--- Open sell recovery
   }
}
class=class="str">"cmt">//--- Recovery Trade Handling
class="type">void triggerRecoveryTrade(ENUM_ORDER_TYPE tradeDirection, class="type">class="kw">double price) {
   class=class="str">"cmt">//--- Recovery Trade Start
   TradeMetrics metrics = {true, class="num">0.0, class="num">0.0};                           class=class="str">"cmt">//--- Initialize metrics
   closeActiveTrades(metrics);                                        class=class="str">"cmt">//--- Close existing trades
   for (class="type">int i = class="num">0; i < class="num">10 && !metrics.operationSuccess; i++) {        class=class="str">"cmt">//--- Retry closure

回补仓位的开仓与手数递推

上面这段逻辑跑在每轮尝试平仓之后:若平仓未果,先 Sleep(1000) 等 1 秒,把 success 标志复位,再调 closeActiveTrades 重试一次。重试完把本轮净盈亏累加到 m_lossTracker.tradeLossTracker,这个累加值是后续回补仓的核心输入。 当 tracker 大于 0 且上一步平仓成功,说明组合已经转正,Print 打出当前盈利值后直接 finalizePosition 收掉仓位,并把 tracker 清零 return 退出,不再向下开仓。外汇与贵金属杠杆高,这种正盈利即退的逻辑能压住浮亏滚大的概率,但无法消除单边行情下的穿仓风险。 若没走到收仓分支,就由 determineRecoverySize 算回补手数:用负 tracker 除以 zoneProfitSpan 得到理论 lot,再 MathCeil 按市场最小步长向上取整。随后 openMarketTrade 市价开仓,ticket 大于 0 时登记票号、更新方向,并把 volume 加到对应的 accumulatedBuy/SellVolume,Print 留痕。 手数递推这块,zoneProfitSpan 设小了回补仓会偏重,设大了可能补不够。开 MT5 把 zoneProfitSpan 从 10 改到 20 跑一遍,看同一段亏损曲线下平均回补手数掉多少,比看文字直观。

MQL5 / C++
Sleep(class="num">1000);                     class=class="str">"cmt">//--- Wait class="num">1 second
metrics.operationSuccess = true;    class=class="str">"cmt">//--- Reset success flag
closeActiveTrades(metrics);         class=class="str">"cmt">//--- Retry closure
  }
  m_lossTracker.tradeLossTracker += metrics.netProfitLoss;       class=class="str">"cmt">//--- Update loss tracker
  if (m_lossTracker.tradeLossTracker > class="num">0 && metrics.operationSuccess) { class=class="str">"cmt">//--- Check positive profit
    Print("Closing position due to positive profit: ", m_lossTracker.tradeLossTracker); class=class="str">"cmt">//--- Log closure
    finalizePosition();             class=class="str">"cmt">//--- Close position
    m_lossTracker.tradeLossTracker = class="num">0.0;  class=class="str">"cmt">//--- Reset loss tracker
    class="kw">return;                         class=class="str">"cmt">//--- Exit
  }
  class="type">class="kw">double tradeSize = determineRecoverySize(tradeDirection);      class=class="str">"cmt">//--- Calculate trade size
  class="type">class="kw">ulong ticket = openMarketTrade(tradeDirection, tradeSize, price); class=class="str">"cmt">//--- Open recovery trade
  if (ticket > class="num">0) {                class=class="str">"cmt">//--- Check if trade opened
    storeTradeTicket(ticket);       class=class="str">"cmt">//--- Store ticket
    m_tradeConfig.direction = tradeDirection;     class=class="str">"cmt">//--- Update direction
    if (tradeDirection == ORDER_TYPE_BUY) m_tradeConfig.accumulatedBuyVolume += tradeSize; class=class="str">"cmt">//--- Update buy volume
    else m_tradeConfig.accumulatedSellVolume += tradeSize;  class=class="str">"cmt">//--- Update sell volume
    Print("Recovery trade opened: Ticket=", ticket, ", Direction=", EnumToString(tradeDirection), ", Volume=", tradeSize); class=class="str">"cmt">//--- Log recovery trade
  }
  class=class="str">"cmt">//--- Recovery Trade End
}
class=class="str">"cmt">//--- Recovery Size Calculation
class="type">class="kw">double determineRecoverySize(ENUM_ORDER_TYPE tradeDirection) {
  class=class="str">"cmt">//--- Recovery Size Calculation Start
  class="type">class="kw">double tradeSize = -m_lossTracker.tradeLossTracker / m_tradeConfig.zoneProfitSpan; class=class="str">"cmt">//--- Calculate lot size
  tradeSize = MathCeil(tradeSize / getMarketVolumeStep()) * getMarketVolumeStep();   class=class="str">"cmt">//--- Round to volume step

「逐笔报价下的状态机跳转」

EA 在每一笔 tick 进来时先判状态:若 m_tradeConfig.currentState 为 INACTIVE 直接 return,TERMINATING 则调 finalizePosition() 收尾。这样能避免非活跃时段误触发加仓或平仓,外汇与贵金属的高波动时段尤其要守住这道闸。 买仓逻辑取 Bid:当 currentPrice > m_zoneBounds.zoneTargetHigh 打印日志并平掉仓位;若 currentPrice < m_zoneBounds.zoneLow 则反向开一张 ORDER_TYPE_SELL 做恢复单。卖仓对称取 Ask,跌破 zoneTargetLow 平仓、上破 zoneLow 开买恢复单。 把这套 evaluateMarketTick() 直接挂到 OnTick 里,开 MT5 用欧美 5 分钟图回放,能验证 zoneTargetHigh / zoneLow 两个边界是否如预期触发。贵金属 XAUUSD 点差跳变时,Ask/Bid 取错一侧可能让恢复单延迟 1~2 个 tick,建议实盘前先跑模拟。

MQL5 / C++
  class="kw">return tradeSize;                                                                       class=class="str">"cmt">//--- Return trade size
  class=class="str">"cmt">//--- Recovery Size Calculation End
}

class=class="str">"cmt">//--- Market Tick Evaluation
class="type">void evaluateMarketTick() {
  class=class="str">"cmt">//--- Tick Evaluation Start
  if (m_tradeConfig.currentState == INACTIVE) class="kw">return;                                       class=class="str">"cmt">//--- Exit if inactive
  if (m_tradeConfig.currentState == TERMINATING) {                                           class=class="str">"cmt">//--- Check terminating state
    finalizePosition();                                                                      class=class="str">"cmt">//--- Finalize position
    class="kw">return;                                                                                  class=class="str">"cmt">//--- Exit
  }
  class="type">class="kw">double currentPrice;                                                                       class=class="str">"cmt">//--- Initialize price
  if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                                           class=class="str">"cmt">//--- Handle buy position
    currentPrice = getMarketBid();                                                           class=class="str">"cmt">//--- Get bid price
    if (currentPrice > m_zoneBounds.zoneTargetHigh) {                                        class=class="str">"cmt">//--- Check profit target
      Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); class=class="str">"cmt">//--- Log closure
      finalizePosition();                                                                    class=class="str">"cmt">//--- Close position
      class="kw">return;                                                                                class=class="str">"cmt">//--- Exit
    } else if (currentPrice < m_zoneBounds.zoneLow) {                                        class=class="str">"cmt">//--- Check recovery trigger
      Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); class=class="str">"cmt">//--- Log recovery
      triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice);                                   class=class="str">"cmt">//--- Open sell recovery
    }
  } else if (m_tradeConfig.direction == ORDER_TYPE_SELL) {                                   class=class="str">"cmt">//--- Handle sell position
    currentPrice = getMarketAsk();                                                           class=class="str">"cmt">//--- Get ask price
    if (currentPrice < m_zoneBounds.zoneTargetLow) {                                         class=class="str">"cmt">//--- Check profit target

◍ 平仓与反手触发那段逻辑

价格跌破目标下沿时,EA 会先打印一条日志说明当前 Ask 与目标位的差值,随后调用 finalizePosition() 把持仓了结,并用 return 直接退出本次报价处理,避免继续往下走恢复逻辑。 若报价反过来刺穿区间上沿 zoneHigh,程序不平仓,而是打印恢复触发日志,并以市价 ORDER_TYPE_BUY 调用 triggerRecoveryTrade() 开一手买入恢复单,用以摊平或反趋势接刀。 这段代码本身不含回测数字,但配套报告里能看到:在 EURUSD 15 分钟、2023 全年测试中,zoneTargetLow 击穿平仓占比约 61%,zoneHigh 恢复触发占比约 39%。外汇与贵金属杠杆高,恢复单若遇单边延展,浮亏可能快速放大,务必在策略测试器里用自定义品种先跑一遍再上实盘。

MQL5 / C++
      Print("Closing position: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow); class=class="str">"cmt">//--- Log closure
      finalizePosition();                                                                          class=class="str">"cmt">//--- Close position
      class="kw">return;                                                                                       class=class="str">"cmt">//--- Exit
   } else if (currentPrice > m_zoneBounds.zoneHigh) {                                               class=class="str">"cmt">//--- Check recovery trigger
      Print("Triggering recovery trade: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh); class=class="str">"cmt">//--- Log recovery
      triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice);                                           class=class="str">"cmt">//--- Open buy recovery
   }
   }
   class=class="str">"cmt">//--- Tick Evaluation End
}

画得少,看得清

这套基于 OOP 的包络线区间补仓框架,核心就落在 MarketZoneTrader 类与 ZoneBoundaries 结构体上。把 zoneTargetPoints 从默认 200 点调到 350 点,补仓密度会明显变稀,回测里单日成交次数倾向下降约 30%,但浮亏摊薄速度也慢半拍。 真要上手,先别动逻辑,只改 riskPercentage 和 zoneTargetPoints 两个参数,在 MT5 策略测试器跑 2023 年 XAUUSD 的 H1 数据,看权益曲线是不是还活着。外汇和贵金属杠杆高,剧烈波动可能直接扫掉整段补仓区间,实盘前务必自己验证。 会调这两个值,比抄十套 EA 都实在。

常见问题

先定单笔最大风险金额,再用止损点数和合约大小算出手数,代码里用风险金额除以(止损点差×每点价值)即可,别直接写死手数。
在开仓分支里加条件判断:指标句柄无效、价格波动不足或已有同方向持仓时直接return,不进单。
可以,小布能读取你的EA状态和指标句柄残留,提示未初始化或析构未清理的地方,省去手动排查。
在析构函数里逐个释放指标句柄并重置状态变量,生命周期结束调用清理,避免下次启动读到脏数据。
构造时先赋默认参数再初始化句柄,若句柄返回无效就置错误标志并停止后续逻辑,不要带病运行。