6.2 订单池遍历与 OrderSelect

第 39 节 / 共 43 节 练习题 2 组 课程:MQL4 教程与 MT4 EA 编程
OrdersTotal() 只告诉你“订单池里有多少张单”,不告诉你它们是谁的。要读订单内容,必须先 OrderSelect 选中一条,再调用 OrderTicket()、OrderType()、OrderLots()、OrderMagicNumber() 等属性函数。选中是一次“游标”操作:之后读到的属性都属于最近一次成功选中的那张单。

两种选择方式

  • SELECT_BY_POS + MODE_TRADES:按持仓池位置选择,配合循环遍历所有未平仓订单。
  • SELECT_BY_TICKET:按订单号精确选择,适合下单后复核、按 ticket 平仓。
  • MODE_HISTORY:已平仓历史,配合 OrdersHistoryTotal() 使用。不要用它遍历当前持仓。

最常见的错误是“只按位置选,不检查返回值”。OrderSelect 返回 false 时,后面读到的属性可能是上一张单的残留值,导致平错单。每次选中后都要 `if(!OrderSelect(...)) continue;`。

为什么要过滤品种和魔术号

一个 MT4 账户可以同时挂多个 EA、多个品种。订单池是账户级的,不是图表级的。如果你的 EA 直接 `for(i=0;i

MagicNumber 是 EA 自己写入订单的“身份证”。同一个 EA 在不同图表上运行时,建议用“基础魔术号 + 图表编号”或不同输入参数区分,避免互相干扰。

倒序平仓:一个必须记住的细节

平仓会改变订单池的索引。假设池子里有 A、B、C 三条,你正序处理 i=0 平掉 A,B 会顶到索引 0,C 顶到索引 1;循环 i 自增到 1 时,实际处理的是 C,B 被跳过。倒序从 OrdersTotal()-1 开始,删除后面的元素不会影响前面未处理的索引,所以不会漏单。

同理,遍历中如果需要删除图表对象、数组元素,也优先倒序。这个习惯能避免大量“看起来随机漏一条”的 bug。

完整示例:按魔术号平仓与计数

下面的代码给出两个常用函数:CloseAllByMagic 倒序平仓,CountMyOrders 正序计数。计数不需要删除元素,所以正序没问题;平仓必须倒序。

#property strict
input int MagicNumber = 20261102;
 
bool CloseAllByMagic(int magic) {
    bool allOk = true;
    for(int i = OrdersTotal() - 1; i >= 0; i--) {
        if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
        if(OrderSymbol() != Symbol()) continue;
        if(OrderMagicNumber() != magic) continue;
 
        double price = (OrderType() == OP_BUY) ? Bid : Ask;
        if(!OrderClose(OrderTicket(), OrderLots(), price, 3, clrRed)) {
            Print("平仓失败 ticket=", OrderTicket(),
            " err=", GetLastError());
            allOk = false;
        }
    }
    return allOk;
}
 
int CountMyOrders(int magic) {
    int n = 0;
    for(int i = 0; i < OrdersTotal(); i++) {
        if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
        if(OrderSymbol() == Symbol() &&
        OrderMagicNumber() == magic) n++;
    }
    return n;
}

OrderClose 的第二个参数是手数,第三个是平仓价:多单用 Bid,空单用 Ask。滑点 3 表示允许 3 个点的偏差。平仓失败时一定要打印 ticket 和 GetLastError(),否则你只知道“失败了”,不知道是哪张单、为什么失败。

OrderSelect 之后属性会变

OrderSelect 是一个全局游标。你在函数 A 里选中了 101 号单,调用函数 B 时如果 B 又 OrderSelect 了 102 号,回到 A 后继续读 OrderLots() 读到的是 102 号的手数。解决办法:要么在同一个函数里“选中—读取—使用”一气呵成,要么先把需要的字段存进局部变量,再调用其他函数。

常见错误

  • 正序平仓:删除元素后索引前移,漏掉订单。平仓、删除对象一律倒序。
  • 不检查 OrderSelect 返回值:读到上一张单的属性,平错单或改错止损。
  • 只按 magic 过滤,不按 symbol 过滤:同一魔术号在不同品种上复用时,会跨品种平仓。
  • 把 OrdersTotal() 缓存到循环外:平仓后总数变化,缓存值会让循环越界或漏单。每次循环重新读 OrdersTotal()。
  • 用 MODE_HISTORY 遍历持仓:历史池和持仓池是两套索引,选不到当前订单。

小结

订单池操作的核心是“选中—校验—动作”三步:OrderSelect 选中,检查 Symbol 与 Magic,再执行平仓或修改。遍历方向跟着“是否删除元素”走:只读正序,删除倒序。记住这两条,订单池相关的 bug 会少一大半。

订单属性读取清单

OrderSelect 成功之后,常用的属性有:OrderTicket() 订单号、OrderType() 方向、OrderLots() 手数、OrderOpenPrice() 开仓价、OrderStopLoss() 止损、OrderTakeProfit() 止盈、OrderMagicNumber() 魔术号、OrderSymbol() 品种、OrderProfit() 浮动盈亏、OrderComment() 注释、OrderOpenTime() 开仓时间。不要在 OrderSelect 之前读这些函数,它们返回的是“上一次选中”的残留值。

读取属性时尽量一次读完,存进局部变量。比如先 `double sl=OrderStopLoss(); double tp=OrderTakeProfit();`,再调用可能内部会 OrderSelect 的函数。这样即使后续游标被移动,你手里的值仍然是当前订单的。

部分平仓与分批处理

OrderClose 的第二个参数可以小于 OrderLots(),这就是部分平仓。平掉一部分后,剩下的手数仍在订单池里,订单号不变。部分平仓常见于分批止盈:价格走到第一目标平一半,走到第二目标再平剩下的一半。注意每次部分平仓后重新 OrderSelect 读取剩余手数,不要用平仓前的缓存值计算下一次手数。

部分平仓也会改变订单池索引,所以循环仍要倒序。若一次循环里对同一张单多次部分平仓,建议每次重新选中并检查剩余手数,避免浮点误差导致“手数小于最小手数”的 131 错误。

历史订单与统计

OrdersTotal() 只统计当前持仓;已平仓订单要用 OrdersHistoryTotal() 配合 `OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)`。历史池的索引顺序与持仓池不同,且会随平仓增加。统计当日盈亏时,遍历历史订单,按 OrderCloseTime() 过滤日期,累加 OrderProfit()+OrderSwap()+OrderCommission()。不要用 AccountProfit() 代替,它只反映当前浮动盈亏。

历史订单的 OrderType() 仍然是开仓方向,OrderClosePrice() 是平仓价。想判断一笔交易是盈利还是亏损,用 OrderProfit() 加费用,而不是比较开仓价和平仓价——点差和佣金可能让“价格方向对了”的单子仍然亏损。

魔术号的设计

魔术号是 EA 的身份标识,不是订单的唯一标识。同一策略跑在 EURUSD 与 GBPUSD 两个图表上,如果都用 8888,订单池过滤就会跨品种;虽然还会比较 Symbol(),但同一品种的多图表、多周期仍会冲突。推荐用“基础号 + 图表序号”或“基础号 + 周期编码”,例如 20261102 + 1/2/3,既好读又能区分。

魔术号只影响你自己的过滤逻辑,不改变经纪商的订单处理。不要依赖魔术号做风控隔离,真正的隔离还要加上品种、注释、甚至订单号白名单。

订单池快照与并发

订单池是动态的:你遍历的同时,另一个 EA 可能在下单或平仓,经纪商也可能因为止损触发而移除订单。遍历时不要假设“这秒看到的 6 张单,下一秒还是 6 张”。每次循环重新读 OrdersTotal(),每次动作前重新 OrderSelect,动作失败就跳过,不要用缓存的列表强行操作。

如果需要“先统计再批量处理”,先做一次只读快照,把订单号存进数组;处理时按订单号 SELECT_BY_TICKET 重新确认。这样即使订单池顺序变了,也不会平错单。快照只保存 ticket,不保存手数、价格等易变字段;真正执行时重新读取。

平仓失败怎么排查

OrderClose 返回 false 时,按顺序检查:订单是否已经不存在(4108);平仓价是否用反(多单用 Bid、空单用 Ask);手数是否大于持仓(131);滑点是否太小导致 138;市场是否关闭(146/交易不允许)。把 ticket、方向、手数、平仓价、错误码一起打印,基本能一眼定位。

如果订单是部分平仓,剩余手数可能小于最小手数,下一次平仓会报 131。此时要么一次平完,要么在部分平仓前确认剩余手数仍满足 MODE_MINLOT。不要用“平一半”这种模糊逻辑,先算清楚再下单。

交互演示:订单池遍历:指针怎么走

6 张单里只有 2 张属于本 EA。单步看指针从 i=5 走到 i=0,比较品种与魔术号,最后倒序平仓。

提示:动画默认暂停。点「播放」连续演示,点「单步」自己控制节奏;桌面端也可以用空格播放、方向键单步、R 重置。

练习题

  • 6.2 订单池遍历与 OrderSelect 选择题
    1.遍历当前持仓订单池,OrderSelect 的正确参数组合是? (2分) [选择]
    A. SELECT_BY_POS, MODE_TRADES
    B. SELECT_BY_TICKET, MODE_TRADES
    C. SELECT_BY_POS, MODE_HISTORY
    D. SELECT_BY_TICKET, MODE_HISTORY
    2.平仓遍历时建议使用哪种循环方向? (2分) [选择]
    A. 从 0 到 OrdersTotal()-1
    B. 从 OrdersTotal()-1 到 0
    C. 从 1 到 OrdersTotal()
    D. 任意方向都一样
    3.OrderSelect 成功后,OrderMagicNumber() 返回的是最近一次选中订单的魔术号。 (2分) [判断]
  • 6.2 订单池遍历与 OrderSelect 编程实战(AI 批改)
    1.【实战】写 CloseAllByMagic (10分) [MQL4自动批改]
    写函数 bool CloseAllByMagic(int magic):倒序遍历当前持仓,OrderSelect 失败则 continue;只处理 OrderSymbol()==Symbol() 且 OrderMagicNumber()==magic 的订单;多单用 Bid、空单用 Ask 平仓,滑点 3;平仓失败打印 ticket 与 GetLastError 并返回 false。必须出现 OrdersTotal、OrderSelect、OrderClose、GetLastError。