MQL5自动化交易策略(第四部分):构建多层级区域恢复系统·进阶篇
📘

MQL5自动化交易策略(第四部分):构建多层级区域恢复系统·进阶篇

第 2/3 篇

「回补引擎的结构体与初始化拆解」

做回补策略的骨架,先得把持仓状态塞进一个结构体里。下面这段 MQL5 声明了回补区(zone)、目标区(target)以及追踪止损所需的字段,外汇和贵金属波动大,zoneSize 用点数乘 _Point 才能跨品种通用,否则 EURUSD 和 XAUUSD 的尺度会完全错位。 Initialize 是唯一入口:传入初始手数、zone 点数、target 点数、加仓倍数、交易品种、首单方向和价格。它会把 zoneSize 和 targetSize 换算成绝对价格距离,并把 isRecovery 置 false、tickets 数组清空,最后调 CalculateZones 算出上下边界。 注意 trailingStop 和 initialEntryPrice 也在这里落定,前者初始为 0 代表不拖尾,后者锁定首单价格供后续移动止损判定。开 MT5 把结构体抄进 EA,改 zonePts=50、targetPts=100 这类参数,就能直观看到 zoneHigh/zoneLow 随报价刷新。

MQL5 / C++
class="type">class="kw">double zoneSize;                    class=class="str">"cmt">//--- Distance in points defining the recovery zone size.
class="type">class="kw">double targetSize;                  class=class="str">"cmt">//--- Distance in points defining the profit target range.
class="type">class="kw">double multiplier;                  class=class="str">"cmt">//--- Lot size multiplier for recovery trades.
class="type">class="kw">string symbol;                      class=class="str">"cmt">//--- Trading symbol.
ENUM_ORDER_TYPE lastOrderType;      class=class="str">"cmt">//--- Type of the last order(BUY or SELL).
class="type">class="kw">double lastOrderPrice;              class=class="str">"cmt">//--- Price of the last executed order.
class="type">class="kw">double zoneHigh;                    class=class="str">"cmt">//--- Upper boundary of the recovery zone.
class="type">class="kw">double zoneLow;                     class=class="str">"cmt">//--- Lower boundary of the recovery zone.
class="type">class="kw">double zoneTargetHigh;              class=class="str">"cmt">//--- Upper boundary of the target range.
class="type">class="kw">double zoneTargetLow;               class=class="str">"cmt">//--- Lower boundary of the target range.
class="type">bool isRecovery;                    class=class="str">"cmt">//--- Whether the recovery is active.
class="type">ulong tickets[];                    class=class="str">"cmt">//--- Array to store tickets of positions associated with this recovery.
class="type">class="kw">double trailingStop;                class=class="str">"cmt">//--- Trailing stop level
class="type">class="kw">double initialEntryPrice;           class=class="str">"cmt">//--- Initial entry price for trailing stop calculation
class=class="str">"cmt">//---
};
class=class="str">"cmt">//--- Initialize position recovery
class="type">void Initialize(class="type">class="kw">double lot, class="type">class="kw">double zonePts, class="type">class="kw">double targetPts, class="type">class="kw">double lotMultiplier, class="type">class="kw">string _symbol, ENUM_ORDER_TYPE type, class="type">class="kw">double price) {
   initialLotSize = lot;            class=class="str">"cmt">//--- Assign initial lot size.
   currentLotSize = lot;            class=class="str">"cmt">//--- Set current lot size equal to initial lot size.
   zoneSize = zonePts * _Point;     class=class="str">"cmt">//--- Calculate zone size in points.
   targetSize = targetPts * _Point;class=class="str">"cmt">//--- Calculate target size in points.
   multiplier = lotMultiplier;      class=class="str">"cmt">//--- Assign lot size multiplier.
   symbol = _symbol;                class=class="str">"cmt">//--- Assign the trading symbol.
   lastOrderType = type;            class=class="str">"cmt">//--- Set the type of the last order.
   lastOrderPrice = price;          class=class="str">"cmt">//--- Record the price of the last executed order.
   isRecovery = false;              class=class="str">"cmt">//--- Set recovery as inactive initially.
   ArrayResize(tickets, class="num">0);         class=class="str">"cmt">//--- Initialize the tickets array.
   trailingStop = class="num">0;                class=class="str">"cmt">//--- Initialize trailing stop
   initialEntryPrice = price;       class=class="str">"cmt">//--- Set initial entry price
   CalculateZones();                class=class="str">"cmt">//--- Calculate recovery and target zones.
}

class=class="str">"cmt">//--- Calculate dynamic zones and targets
class="type">void CalculateZones() {

按上一单方向重建价格区间

网格或加仓类 EA 里,恢复区间必须跟着上一笔成交单的方向走。若末单是 BUY,上边界直接钉在成交价,下边界再减一个 zoneSize;反向单则把下边界钉在成交价,上边界加 zoneSize。 目标区间(zoneTargetHigh / zoneTargetLow)分别在恢复区上下各扩 targetSize,用来判断价格脱离成本区后是否触发平仓或反手。这两个 size 参数建议在 MT5 策略测试器里用 5~30 点(取决于品种点值)做敏感性扫描,欧元兑美元与 XAUUSD 的合适区间往往差出一个数量级。 开仓函数 OpenTrade 在 BUY 分支里用 trade.Buy 下市价单,把 lastOrderType 记为 ORDER_TYPE_BUY,并用 SymbolInfoDouble 抓 SYMBOL_BID 存进 lastOrderPrice。随后 ArrayResize 把 tickets 数组加一位,写入新单 ticket,再跑一次 CalculateZones 重算区间,同时把 isRecovery 置 false——说明首单不参与恢复逻辑。 外汇与贵金属杠杆高、滑点跳空频繁,这类靠末单价格硬锚区间的做法在重大数据行情中可能连续穿透,实盘前务必用历史tick复盘验证。

MQL5 / C++
  if (lastOrderType == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- If the last order was a BUY...
        zoneHigh = lastOrderPrice;           class=class="str">"cmt">//--- Set upper boundary at the last order price.
        zoneLow = zoneHigh - zoneSize;       class=class="str">"cmt">//--- Set lower boundary below the last order price.
        zoneTargetHigh = zoneHigh + targetSize; class=class="str">"cmt">//--- Define target range above recovery zone.
        zoneTargetLow = zoneLow - targetSize;   class=class="str">"cmt">//--- Define target range below recovery zone.
  } else if (lastOrderType == ORDER_TYPE_SELL) { class=class="str">"cmt">//--- If the last order was a SELL...
        zoneLow = lastOrderPrice;                    class=class="str">"cmt">//--- Set lower boundary at the last order price.
        zoneHigh = zoneLow + zoneSize;               class=class="str">"cmt">//--- Set upper boundary above the last order price.
        zoneTargetLow = zoneLow - targetSize;        class=class="str">"cmt">//--- Define target range below recovery zone.
        zoneTargetHigh = zoneHigh + targetSize;      class=class="str">"cmt">//--- Define target range above recovery zone.
  }
}
class=class="str">"cmt">//--- Open a trade with comments for position type
class="type">bool OpenTrade(ENUM_ORDER_TYPE type, class="type">class="kw">string comment) {
   if (type == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- For a BUY order...
      if (trade.Buy(currentLotSize, symbol, class="num">0, class="num">0, class="num">0, comment)) { class=class="str">"cmt">//--- Attempt to place a BUY trade.
         lastOrderType = ORDER_TYPE_BUY;                            class=class="str">"cmt">//--- Update the last order type.
         lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID);      class=class="str">"cmt">//--- Record the current price.
         ArrayResize(tickets, ArraySize(tickets) + class="num">1);               class=class="str">"cmt">//--- Resize the tickets array.
         tickets[ArraySize(tickets) - class="num">1] = trade.ResultOrder();      class=class="str">"cmt">//--- Store the new ticket.
         CalculateZones();                                           class=class="str">"cmt">//--- Recalculate zones.
         isRecovery = false;                                         class=class="str">"cmt">//--- Ensure recovery is inactive for initial trade.
         Print("Opened BUY Position, Ticket: ", tickets[ArraySize(tickets) - class="num">1]);
         class="kw">return true;                                                class=class="str">"cmt">//--- Return success.
      }
   } else if (type == ORDER_TYPE_SELL) { class=class="str">"cmt">//--- For a SELL order...
      if (trade.Sell(currentLotSize, symbol, class="num">0, class="num">0, class="num">0, comment)) { class=class="str">"cmt">//--- Attempt to place a SELL trade.
         lastOrderType = ORDER_TYPE_SELL;                            class=class="str">"cmt">//--- Update the last order type.

◍ 逆势加仓的触发与手数回滚

下面这段逻辑管的是「价格跌破/涨破区域后怎么补单」。以 BUY 单为例,当 currentPrice 落到 zoneLow 之下,先把 currentLotSize 暂存到 previousLotSize,再乘上 multiplier 试探性放大仓位;若 OpenTrade 返回 true 才把 isRecovery 置为 true,否则把手数退回原值,避免在 MT5 实盘里因下单失败而默默膨胀风险敞口。 SELL 单的镜像处理完全对称:价格≥zoneHigh 时试图开 BUY recovery,同样先暂存再乘倍数,失败就还原。外汇与贵金属杠杆高,这类马丁类补单若 multiplier 设成 2.0,第 4 次补单手数就是初始的 8 倍,回撤可能非线性放大,参数务必先在策略测试器跑过。 代码里没有对 zoneLow/zoneHigh 做空值保护,若 CalculateZones() 在某轮没算出有效区间,比较运算会拿 0 或 EMPTY_VALUE 参与,可能误触发补单。开 MT5 把这段直接贴进 EA,打印 tickets 数组末位,就能看到每次补单的订单号是否按预期递增。

MQL5 / C++
lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID);  class=class="str">"cmt">//--- Record the current price.
ArrayResize(tickets, ArraySize(tickets) + class="num">1);              class=class="str">"cmt">//--- Resize the tickets array.
tickets[ArraySize(tickets) - class="num">1] = trade.ResultOrder();     class=class="str">"cmt">//--- Store the new ticket.
CalculateZones();                                          class=class="str">"cmt">//--- Recalculate zones.
isRecovery = false;                                        class=class="str">"cmt">//--- Ensure recovery is inactive for initial trade.
Print("Opened SELL Position, Ticket: ", tickets[ArraySize(tickets) - class="num">1]);
class="kw">return true;                                               class=class="str">"cmt">//--- Return success.
  }
  }
  class="kw">return false; class=class="str">"cmt">//--- If the trade was not placed, class="kw">return false.
}

class=class="str">"cmt">//--- Manage zone recovery
class="type">void ManageZones() {
  class="type">class="kw">double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); class=class="str">"cmt">//--- Get the current price.
  if (lastOrderType == ORDER_TYPE_BUY && currentPrice <= zoneLow) { class=class="str">"cmt">//--- If price drops below the recovery zone for a BUY...
    class="type">class="kw">double previousLotSize = currentLotSize;                          class=class="str">"cmt">//--- Store the current lot size temporarily.
    currentLotSize *= multiplier;                                     class=class="str">"cmt">//--- Tentatively increase lot size.
    if (OpenTrade(ORDER_TYPE_SELL, "Recovery Position")) {             class=class="str">"cmt">//--- Attempt to open a SELL recovery trade.
      isRecovery = true;                                              class=class="str">"cmt">//--- Mark recovery as active if trade is successful.
    } else {
      currentLotSize = previousLotSize;                               class=class="str">"cmt">//--- Revert the lot size if the trade fails.
    }
  } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice >= zoneHigh) { class=class="str">"cmt">//--- If price rises above the recovery zone for a SELL...
    class="type">class="kw">double previousLotSize = currentLotSize;                          class=class="str">"cmt">//--- Store the current lot size temporarily.
    currentLotSize *= multiplier;                                     class=class="str">"cmt">//--- Tentatively increase lot size.
    if (OpenTrade(ORDER_TYPE_BUY, "Recovery Position")) {              class=class="str">"cmt">//--- Attempt to open a BUY recovery trade.

「成交回退与触价平仓的逻辑衔接」

上面这段接续了开仓后的状态机:若本轮交易成功,就把 isRecovery 标记为 true,表示恢复逻辑已激活;若失败,currentLotSize 回退到 previousLotSize,不放大风险敞口。外汇与贵金属杠杆高,这种回退能避免连亏时仓位失控。 触价平仓由 CheckCloseAtTargets 驱动:取 SYMBOL_BID 现价,买单看是否 ≥ zoneTargetHigh,卖单看是否 ≤ zoneTargetLow,达标就调 ClosePositionsAtTarget。注意用的是 BID 而非 ASK,卖单触底平仓才不会被点差拖慢。 ClosePositionsAtTarget 从 tickets 数组尾部倒序遍历,每单最多重试 10 次调 trade.PositionClose。成功就打印并 ArrayRemove 剔票,失败进 else 分支继续耗 retries。实盘里服务器拒单常见,10 次重试比一次性尝试更稳。

MQL5 / C++
isRecovery = true;                                      class=class="str">"cmt">//--- Mark recovery as active if trade is successful.
   } else {
      currentLotSize = previousLotSize;                    class=class="str">"cmt">//--- Revert the lot size if the trade fails.
   }
   }
}
class=class="str">"cmt">//--- Check and close trades at targets
class="type">void CheckCloseAtTargets() {
   class="type">class="kw">double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); class=class="str">"cmt">//--- Get the current price.
   if (lastOrderType == ORDER_TYPE_BUY && currentPrice >= zoneTargetHigh) { class=class="str">"cmt">//--- If price reaches the target for a BUY...
      ClosePositionsAtTarget();                            class=class="str">"cmt">//--- Close positions that meet the target criteria.
   } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice <= zoneTargetLow) { class=class="str">"cmt">//--- If price reaches the target for a SELL...
      ClosePositionsAtTarget();                            class=class="str">"cmt">//--- Close positions that meet the target criteria.
   }
}
class=class="str">"cmt">//--- Close positions that have reached the target
class="type">void ClosePositionsAtTarget() {
   for (class="type">int i = ArraySize(tickets) - class="num">1; i >= class="num">0; i--) {     class=class="str">"cmt">//--- Iterate through all tickets.
      class="type">ulong ticket = tickets[i];                           class=class="str">"cmt">//--- Get the position ticket.
      class="type">int retries = class="num">10;                                    class=class="str">"cmt">//--- Set retry count.
      while (retries > class="num">0) {                                class=class="str">"cmt">//--- Retry until successful or retries exhausted.
         if (trade.PositionClose(ticket)) {                class=class="str">"cmt">//--- Attempt to close the position.
            Print("CLOSED # ", ticket, " Trailed and closed: ", (trailingStop != class="num">0));
            ArrayRemove(tickets, i);                       class=class="str">"cmt">//--- Remove the ticket from the array on success.
            retries = class="num">0;                                   class=class="str">"cmt">//--- Exit the loop on success.
         } else {

初始仓的追踪止损触发逻辑

下面这段函数只服务于「恢复序列里仅剩一张初始仓」的场景:当输入开关打开且 tickets 数组长度为 1 时,才进入追踪止损判断,避免对加仓单误用。 以 BUY 为例,触发抬高止损的前提是当前 BID 价超过「开仓价 + (最小盈利点 + 追踪距离点) × _Point」。只有算出的新止损比已记录的 trailingStop 更高,才更新并在日志打印新值;若之后价格回落到 trailingStop 及以下,就调用 ClosePositionsAtTarget 平仓。SELL 方向对称处理,用 ASK 下行突破来移动止损。 把这段代码直接丢进 MT5 的 EA 里,把 inputMinimumProfitPts 设成 50、inputTrailingStopPts 设成 30,能在 5 位报价黄金上直观看到:价格朝盈利方向走 80 点后止损才开始跟随,回撤 30 点即砍仓。外汇与贵金属杠杆高,追踪止损只降风险不保收益,实盘前请用策略测试器跑至少 3 个月 tick 数据。

MQL5 / C++
class="type">void ApplyTrailingStop() {
  if (inputTrailingStopEnabled && ArraySize(tickets) == class="num">1) { class=class="str">"cmt">// Ensure trailing stop is enabled and there is only one position(initial position)
    class="type">ulong ticket = tickets[class="num">0]; class=class="str">"cmt">// Get the ticket of the initial position
    class="type">class="kw">double entryPrice = GetPositionEntryPrice(ticket); class=class="str">"cmt">// Get the entry price of the position by ticket
    class="type">class="kw">double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); class=class="str">"cmt">// Get the current price
    class="type">class="kw">double newTrailingStop;
    if (lastOrderType == ORDER_TYPE_BUY) {
      if (currentPrice > entryPrice + (inputMinimumProfitPts + inputTrailingStopPts) * _Point) {
        newTrailingStop = currentPrice - inputTrailingStopPts * _Point; class=class="str">"cmt">// Calculate new trailing stop for BUY
        if (newTrailingStop > trailingStop) {
          trailingStop = newTrailingStop; class=class="str">"cmt">// Update trailing stop if the new one is higher
          Print("Trailing BUY Position, Ticket: ", ticket, " New Trailing Stop: ", trailingStop);
        }
      }
      if (trailingStop != class="num">0 && currentPrice <= trailingStop) {
        Print("Trailed and closing BUY Position, Ticket: ", ticket);
        ClosePositionsAtTarget(); class=class="str">"cmt">// Close position if the price falls below the trailing stop
      }
    } else if (lastOrderType == ORDER_TYPE_SELL) {
      if (currentPrice < entryPrice - (inputMinimumProfitPts + inputTrailingStopPts) * _Point) {
        newTrailingStop = currentPrice + inputTrailingStopPts * _Point; class=class="str">"cmt">// Calculate new trailing stop for SELL
        if (newTrailingStop < trailingStop) {
          trailingStop = newTrailingStop; class=class="str">"cmt">// Update trailing stop if the new one is lower

◍ 回撤复位与RSI句柄的收口逻辑

上面这段把篮子策略的『清零』动作和初始化收尾放在了一起。Reset() 不只是把 currentLotSize 退回 inputlot,还顺手把 lastOrderType、lastOrderPrice、trailingStop、initialEntryPrice 全部归零,并用 ArrayResize(tickets,0) 清空持仓票号数组——也就是说,只要整组仓位平掉,恢复模式就彻底失活,不会带着旧状态进入下一轮。 GetPositionEntryPrice() 用 PositionSelectByTicket 按票号取开仓价,失败返回 0.0 并打印告警。实盘里若返回 0.0,大概率是票号已失效或持仓已被外部平掉,别拿它当有效入场价去算加仓间距。 OnInit() 里创建 RSI 句柄后立刻判 INVALID_HANDLE,失败直接 INIT_FAILED 退出,不让 EA 带着空句柄跑。rsiBuffer 用 ArraySetAsSeries 设成时间序列,索引 0 才是当前 K 线。外汇与贵金属杠杆高,这类恢复策略在单边行情中浮亏可能快速放大,上 MT5 前先用策略测试器跑历史段验证复位触发条件。

MQL5 / C++
class="type">class="kw">double GetPositionEntryPrice(class="type">ulong ticket) {
  if (PositionSelectByTicket(ticket)) {
    class="kw">return PositionGetDouble(POSITION_PRICE_OPEN);
  } else {
    Print("Failed to select position by ticket: ", ticket);
    class="kw">return class="num">0.0;
  }
}
class="type">void Reset() {
  currentLotSize = inputlot;
  lastOrderType = -class="num">1;
  lastOrderPrice = class="num">0.0;
  isRecovery = false;
  ArrayResize(tickets, class="num">0);
  trailingStop = class="num">0;
  initialEntryPrice = class="num">0.0;
  Print("Strategy BASKET reset after closing trades.");
}
PositionRecovery recoveryArray[];
class="type">int OnInit() {
  rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, rsiPeriod, PRICE_CLOSE);
  if (rsiHandle == INVALID_HANDLE) {
    Print("Failed to create RSI handle. Error: ", GetLastError());
    class="kw">return(INIT_FAILED);
  }
  ArraySetAsSeries(rsiBuffer, true);
  Print("Multi-Zone Recovery Strategy initialized.");
  class="kw">return(INIT_SUCCEEDED);
}

常见问题

需填价格区间上下沿、层级索引、手数步长与方向标记;建议先用打印日志核对首层赋值是否溢出。
用上一单开仓价加减固定回补距生成新区间,层级号+1;务必校验新区间不与已成交层重叠。
可以。把策略逻辑贴给小布,它能标出触价条件与手数回滚公式的矛盾点,并给修正建议。
多在异步成交未回执时就清了挂单;应在成交回退确认状态后再解除触价平仓挂单。
常因复位时没重算 RSI 周期句柄;触发后需显式释放旧句柄并按新参数重新创建。