MQL5自动化交易策略(第十一部分):开发多层级网格交易系统·综合运用
篮子盈利后把止损挪到加权盈亏平衡
网格策略里,当某个货币篮子新开仓且总持仓数大于 1 时,把整组仓位的止盈重算到盈亏平衡线之上,是控制回撤的常见做法。下面这段代码在 newPositionOpened 为真且 CountBasketPositions 返回 >1 时触发,先算加权均价再改 TP。
加权盈亏平衡价由 CalculateBreakevenPrice 按手数加权得出:遍历所有持仓,把每单开仓价乘以手数累加,再除以总手数。买向篮子在新 TP 设为 breakevenPrice + inpBreakevenPts * _Point,卖向则减去同样点数,相当于在打平处外溢几个点作为缓冲。
实际改 TP 时,代码用 PositionModify(ticket, 0, newTP) 把止损留 0(不改动)只动止盈;若失败就 Print 出 ticket 号方便查日志。外汇与贵金属杠杆高,这类自动挪平保逻辑若 inpBreakevenPts 设太小,遇点差扩张可能频繁被扫,建议在 MT5 策略测试器用真实点差回测确认。
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。
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
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 点回测再上真仓。
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 在池子里一张单都不剩,就把它从数组里抠掉并缩容。
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">//--- 数组整体砍掉最后一格,完成缩容 } }
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 仅作教学底稿,实盘前务必自己跑回测、压一遍手数计算逻辑,再谈长期。