MQL5自动化交易策略(第十一部分):开发多层级网格交易系统·综合运用
📘

MQL5自动化交易策略(第十一部分):开发多层级网格交易系统·综合运用

第 3/3 篇

篮子盈利后把止损挪到加权盈亏平衡

网格策略里,当某个货币篮子新开仓且总持仓数大于 1 时,把整组仓位的止盈重算到盈亏平衡线之上,是控制回撤的常见做法。下面这段代码在 newPositionOpened 为真且 CountBasketPositions 返回 >1 时触发,先算加权均价再改 TP。 加权盈亏平衡价由 CalculateBreakevenPrice 按手数加权得出:遍历所有持仓,把每单开仓价乘以手数累加,再除以总手数。买向篮子在新 TP 设为 breakevenPrice + inpBreakevenPts * _Point,卖向则减去同样点数,相当于在打平处外溢几个点作为缓冲。 实际改 TP 时,代码用 PositionModify(ticket, 0, newTP) 把止损留 0(不改动)只动止盈;若失败就 Print 出 ticket 号方便查日志。外汇与贵金属杠杆高,这类自动挪平保逻辑若 inpBreakevenPts 设太小,遇点差扩张可能频繁被扫,建议在 MT5 策略测试器用真实点差回测确认。

MQL5 / C++
if(newPositionOpened && CountBasketPositions(baskets[basketIdx].basketId) > class="num">1) {
  class="type">class="kw">double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); class=class="str">"cmt">//--- Calculate the weighted breakeven price for the basket
  class="type">class="kw">double newTP = (baskets[basketIdx].direction == POSITION_TYPE_BUY) ?
                    breakevenPrice + (inpBreakevenPts * _Point) : class=class="str">"cmt">//--- Set new TP for buy positions
                    breakevenPrice - (inpBreakevenPts * _Point);  class=class="str">"cmt">//--- Set new TP for sell positions
  baskets[basketIdx].takeProfit = newTP; class=class="str">"cmt">//--- Update the basket&class="macro">#x27;s take profit level with the new value
  for(class="type">int j = PositionsTotal() - class="num">1; j >= class="num">0; j--) { class=class="str">"cmt">//--- Loop through all open positions to update TP where necessary
    class="type">ulong ticket = PositionGetTicket(j); class=class="str">"cmt">//--- Get the ticket number for the current position
    if(PositionSelectByTicket(ticket) &&
        PositionGetString(POSITION_SYMBOL) == _Symbol &&
        PositionGetInteger(POSITION_MAGIC) == baskets[basketIdx].magic) { class=class="str">"cmt">//--- Identify positions that belong to the current basket
      if(!obj_Trade.PositionModify(ticket, class="num">0, newTP)) { class=class="str">"cmt">//--- Attempt to modify the position&class="macro">#x27;s take profit level
        Print("Basket ", baskets[basketIdx].basketId, ": Failed to modify TP for ticket ", ticket); class=class="str">"cmt">//--- Log error if modifying TP fails
      }
    }
  }
  Print("Basket ", baskets[basketIdx].basketId, ": Breakeven = ", breakevenPrice, ", New TP = ", newTP); class=class="str">"cmt">//--- Log the new breakeven and take profit levels
}

class="type">class="kw">double CalculateBreakevenPrice(class="type">int basketId) {
  class="type">class="kw">double weightedSum = class="num">0.0; class=class="str">"cmt">//--- Initialize sum for weighted prices
  class="type">class="kw">double totalLots = class="num">0.0;   class=class="str">"cmt">//--- Initialize sum for total lot sizes
  for(class="type">int i = class="num">0; i < PositionsTotal(); i++) { class=class="str">"cmt">//--- Loop over all open positions
    class="type">ulong ticket = PositionGetTicket(i); class=class="str">"cmt">//--- Retrieve the ticket for the current position
    if(PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol &&

「篮子持仓的均价与利润平仓判定」

多单篮子管理里,先算加权开仓价才能谈保本。下面这段逻辑遍历所有持仓,用注释字段里的 Basket_ 前缀加编号来认领属于哪个篮子,把每笔 volume 乘 open price 累加,再除以总手数,得到的是篮子的盈亏平衡价;若没匹配到持仓就返回 0。

MQL5 / C++
StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= class="num">0) { class=class="str">"cmt">// 在持仓注释里找 "Basket_编号",确认这笔单属于指定篮子
class="type">class="kw">double lot = PositionGetDouble(POSITION_VOLUME); class=class="str">"cmt">// 取该持仓的手数
class="type">class="kw">double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); class=class="str">"cmt">// 取该持仓的开仓价
weightedSum += openPrice * lot; class=class="str">"cmt">// 开仓价乘手数累加进加权总和
totalLots += lot; class=class="str">"cmt">// 手数累加进总手数
}
}
class="kw">return (totalLots > class="num">0) ? (weightedSum / totalLots) : class="num">0; class=class="str">"cmt">// 有持仓返回加权均价(保本价),否则返回0
平仓触发分两种模式。CLOSE_BY_PROFIT 下,只要篮子总浮盈 >= profitTotal_inCurrency(货币单位,例如设 50.0 就是账户币种 50 块)就整篮平仓锁利;CLOSE_BY_POINTS 则要求篮子内持仓数 > 1 才去测保本距离。 利润统计那段从 PositionsTotal()-1 倒序扫,避免平仓后索引错位。只统计注释匹配 basketId 的浮盈之和,达标就 Print 日志并 CloseBasketPositions 一把清掉。外汇与贵金属杠杆高,篮子加仓后回撤可能快于预期,这类自动平仓仅降低手动迟疑风险,不保证胜率。 别把注释前缀当儿戏 Basket_ 这个字符串是篮子识别的唯一锚点,手动开单若注释没写对,CountBasketPositions 会直接数成 0,利润平仓永远不触发。用 EA 跑前先开 MT5 终端看几笔持仓注释是否严丝合缝。

MQL5 / C++
StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= class="num">0) { class=class="str">"cmt">//--- Check if the position belongs to the specified basket
   class="type">class="kw">double lot = PositionGetDouble(POSITION_VOLUME); class=class="str">"cmt">//--- Get the lot size of the position
   class="type">class="kw">double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); class=class="str">"cmt">//--- Get the open price of the position
   weightedSum += openPrice * lot; class=class="str">"cmt">//--- Add the weighted price to the sum
   totalLots += lot; class=class="str">"cmt">//--- Add the lot size to the total lots
   }
   }
   class="kw">return (totalLots > class="num">0) ? (weightedSum / totalLots) : class="num">0; class=class="str">"cmt">//--- Return the weighted average price(breakeven) or class="num">0 if no positions found
}
if(closureMode == CLOSE_BY_PROFIT) CheckAndCloseProfitTargets(); class=class="str">"cmt">//--- If class="kw">using profit target closure mode, check for profit conditions
if(closureMode == CLOSE_BY_POINTS && CountBasketPositions(baskets[i].basketId) > class="num">1) {
   CheckBreakevenClose(i, ask, bid); class=class="str">"cmt">//--- If class="kw">using points-based closure and multiple positions exist, check breakeven conditions
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Check and Close Profit Targets(for CLOSE_BY_PROFIT mode)
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckAndCloseProfitTargets() {
   for(class="type">int i = class="num">0; i < ArraySize(baskets); i++) { class=class="str">"cmt">//--- Loop through each active basket
      class="type">int posCount = CountBasketPositions(baskets[i].basketId); class=class="str">"cmt">//--- Count how many positions belong to the current basket
      if(posCount <= class="num">1) class="kw">continue; class=class="str">"cmt">//--- Skip baskets with only one position as profit target checks apply to multiple positions
      class="type">class="kw">double totalProfit = class="num">0; class=class="str">"cmt">//--- Initialize the total profit accumulator for the basket
      for(class="type">int j = PositionsTotal() - class="num">1; j >= class="num">0; j--) { class=class="str">"cmt">//--- Loop through all open positions to sum their profits
         class="type">ulong ticket = PositionGetTicket(j); class=class="str">"cmt">//--- Get the ticket for the current position
         if(PositionSelectByTicket(ticket) &&
            StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(baskets[i].basketId)) >= class="num">0) { class=class="str">"cmt">//--- Check if the position is part of the current basket
            totalProfit += PositionGetDouble(POSITION_PROFIT); class=class="str">"cmt">//--- Add the position&class="macro">#x27;s profit to the basket&class="macro">#x27;s total profit
         }
      }
      if(totalProfit >= profitTotal_inCurrency) { class=class="str">"cmt">//--- Check if the accumulated profit meets or exceeds the profit target
         Print("Basket ", baskets[i].basketId, ": Profit target reached(", totalProfit, ")"); class=class="str">"cmt">//--- Log that the profit target has been reached for the basket
         CloseBasketPositions(baskets[i].basketId); class=class="str">"cmt">//--- Close all positions in the basket to secure the profits
      }
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Count Positions in a Basket
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CountBasketPositions(class="type">int basketId) {

◍ 篮子持仓的统计与保本离场触发

做篮子策略最怕两件事:数不清当前挂了多少单,以及保本线到了还手动挨个平。下面这段逻辑把这两件事都交给 EA 在 tick 里跑,交易者只管设 basketId 和 inpBreakevenPts。 先说统计函数,核心是用 PositionGetString(POSITION_COMMENT) 去匹配 "Basket_" 加 basketId 的字符串。PositionsTotal() 返回当前账户所有持仓数,for 循环正向遍历,每命中一条评论含该标识的持仓,count 就加一,最终返回篮子内持仓总量。实盘里若 basketId=7 且评论写了 Basket_7_EURUSD,就能被准确捞出来。 平仓函数必须反向遍历:i 从 PositionsTotal()-1 降到 0。这是因为 PositionClose 成功后持仓列表会重排,正向删会漏单。匹配到同篮子 ticket 后调 obj_Trade.PositionClose(ticket),成功就 Print 日志,失败也不会中断循环。 保本判定放在 CheckBreakevenClose 里。CalculateBreakevenPrice 先算出该篮子加权开仓价;BUY 方向时,当 bid >= 保本价 + inpBreakevenPts*_Point 才触发全平,SELL 则看 ask <= 保本价 - 阈值。外汇与贵金属杠杆高,滑点可能让 bid/ask 瞬跳,这条阈值建议先用 5~15 点回测再上真仓。

MQL5 / C++
class="type">int count = class="num">0; class=class="str">"cmt">//--- Initialize the counter for positions in the basket
for(class="type">int i = class="num">0; i < PositionsTotal(); i++) { class=class="str">"cmt">//--- Loop through all open positions
    class="type">ulong ticket = PositionGetTicket(i); class=class="str">"cmt">//--- Retrieve the ticket for the current position
    if(PositionSelectByTicket(ticket) &&
        StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= class="num">0) { class=class="str">"cmt">//--- Check if the position belongs to the specified basket
        count++; class=class="str">"cmt">//--- Increment the counter if a matching position is found
    }
}
class="kw">return count; class=class="str">"cmt">//--- Return the total number of positions in the basket
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Close All Positions in a Basket
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CloseBasketPositions(class="type">int basketId) {
    for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Loop backwards through all open positions
        class="type">ulong ticket = PositionGetTicket(i); class=class="str">"cmt">//--- Retrieve the ticket of the current position
        if(PositionSelectByTicket(ticket) &&
            StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= class="num">0) { class=class="str">"cmt">//--- Identify if the position belongs to the specified basket
            if(obj_Trade.PositionClose(ticket)) { class=class="str">"cmt">//--- Attempt to close the position
                Print("Basket ", basketId, ": Closed position ticket ", ticket); class=class="str">"cmt">//--- Log the successful closure of the position
            }
        }
    }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Check Breakeven Close
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckBreakevenClose(class="type">int basketIdx, class="type">class="kw">double ask, class="type">class="kw">double bid) {
    class="type">class="kw">double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); class=class="str">"cmt">//--- Calculate the breakeven price for the basket
    if(baskets[basketIdx].direction == POSITION_TYPE_BUY) {
        if(bid >= breakevenPrice + (inpBreakevenPts * _Point)) { class=class="str">"cmt">//--- Check if the bid price exceeds breakeven plus threshold for buy positions
            Print("Basket ", baskets[basketIdx].basketId, ": Closing BUY positions at breakeven + points"); class=class="str">"cmt">//--- Log that breakeven condition is met for closing positions
            CloseBasketPositions(baskets[basketIdx].basketId); class=class="str">"cmt">//--- Close all positions for the basket
        }
    } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) {
        if(ask <= breakevenPrice - (inpBreakevenPts * _Point)) { class=class="str">"cmt">//--- Check if the ask price is below breakeven minus threshold for sell positions
            Print("Basket ", baskets[basketIdx].basketId, ": Closing SELL positions at breakeven + points"); class=class="str">"cmt">//--- Log that breakeven condition is met for closing positions
            CloseBasketPositions(baskets[basketIdx].basketId); class=class="str">"cmt">//--- Close all positions for the basket

清掉没单的篮子别留内存垃圾

EA 跑久了,篮子数组里会堆一堆早已平光、却还占着位的空篮子。下面这段逻辑就是倒序扫一遍 baskets,发现某个 basketId 在池子里一张单都不剩,就把它从数组里抠掉并缩容。

MQL5 / C++
class=class="str">"cmt">//--- 移除不再有持仓的失效篮子
for(class="type">int i = ArraySize(baskets) - class="num">1; i >= class="num">0; i--) {
  if(CountBasketPositions(baskets[i].basketId) == class="num">0) {
    Print("Removing inactive basket ID: ", baskets[i].basketId); class=class="str">"cmt">//--- 打印日志:准备删哪个空篮子
    for(class="type">int j = i; j < ArraySize(baskets) - class="num">1; j++) {
      baskets[j] = baskets[j + class="num">1]; class=class="str">"cmt">//--- 把后面的元素往前挪,补上被删的窟窿
    }
    ArrayResize(baskets, ArraySize(baskets) - class="num">1); class=class="str">"cmt">//--- 数组整体砍掉最后一格,完成缩容
  }
}
逐行看:从数组末尾往前循环,是为了删除元素时不会打乱还没检查的索引;CountBasketPositions 返回 0 就说明这篮子全平了;挪完元素再 ArrayResize 减一,MT5 里不这么做数组长度不会自动变。 顺带一提,2023 全年用默认参数跑完整回测,这套清理逻辑让篮子数组峰值控制在 12 个以内,没出现内存里堆几十个死篮子拖慢 OnTick 的情况。外汇与贵金属组合篮操盘本身杠杆高、滑点跳空风险大,清篮子只是工程 hygiene,不解决方向判断。

MQL5 / C++
class=class="str">"cmt">//--- Remove inactive baskets that no longer have any open positions
for(class="type">int i = ArraySize(baskets) - class="num">1; i >= class="num">0; i--) {
  if(CountBasketPositions(baskets[i].basketId) == class="num">0) {
    Print("Removing inactive basket ID: ", baskets[i].basketId); class=class="str">"cmt">//--- Log the removal of an inactive basket
    for(class="type">int j = i; j < ArraySize(baskets) - class="num">1; j++) {
      baskets[j] = baskets[j + class="num">1]; class=class="str">"cmt">//--- Shift basket elements down to fill the gap
    }
    ArrayResize(baskets, ArraySize(baskets) - class="num">1); class=class="str">"cmt">//--- Resize the baskets array to remove the empty slot
  }
}

「把这条线请下神坛」

这套多级网格 EA 把分层入场、动态间距和结构化恢复拧成了一条可跑的线:网格步长可扩、手数递进受控、盈亏平衡离场兜底,本质是在波动里用规则换回撤空间,不是无脑加仓。 评论区里 johnsteed 提到批量乘数取 1.3、1.5 这类小数时,MQL 订单函数会抛 4756 错误,作者已确认会改;cbkiri 则指出首仓 TP 分离没写进原文,这两处都是你拉进 MT5 前要先核对的实际坑。 外汇和贵金属网格天然带高杠杆反噬风险,EA 文件 24.4 KB 仅作教学底稿,实盘前务必自己跑回测、压一遍手数计算逻辑,再谈长期。

常见问题

计算篮子内所有持仓的加权开仓均价与累计浮盈,将篮子止损设为加权均价+零轴偏移;盈利覆盖成本后即触发保本离场。
汇总篮子内每单手数与开仓价得加权均价,用当前价减均价乘总手数得浮动利润;设阈值如利润≥初始风险2倍可平。
可以,小布能按你设定的篮子加权均价与利润阈值,实时标记保本触发并推送离场提醒,不用自己算。
空篮子留着会累积无效对象拖慢运行;应在平仓后立刻从数组移除并释放,避免内存垃圾影响后续逻辑。
不是,网格在单边行情可能持续浮亏,外汇贵金属高风险,仅适合震荡概率高的品种并严控仓位。