6.3 错误处理与 GetLastError

第 40 节 / 共 43 节 练习题 2 组 课程:MQL4 教程与 MT4 EA 编程
OrderSend 失败时只返回 -1 或 0,真正的原因在 GetLastError()。不读错误码,你只能看到“下单失败”四个字;读了错误码,你才知道是该改止损距离、减手数、等上下文,还是直接放弃。错误处理的第一步不是“重试”,而是“分类”。

先清空,再调用,再读取

MQL4 的错误码是线程级的“最后一次错误”。如果不清空,上一次的旧错误可能被误读。推荐固定三步:

  1. 调用前 ResetLastError()。
  2. 执行可能失败的操作,例如 OrderSend、OrderModify、OrderClose。
  3. 失败后立刻 int err = GetLastError();,并打印到日志。

不要在失败后隔很多行再读 GetLastError(),中间调用的其他函数可能覆盖错误码。也不要用 `if(GetLastError() != 0)` 判断成功——成功时错误码也可能是上一次遗留的非零值,应该先看返回值(ticket > 0)。

常见错误码

  • 130 ERR_INVALID_STOPS:止损/止盈太接近市价,小于 MODE_STOPLEVEL。改距离,不是重试。
  • 131 ERR_INVALID_TRADE_VOLUME:手数不合法,小于最小手数或不符合步长。用 MODE_MINLOT、MODE_LOTSTEP 校正。
  • 134 ERR_NOT_ENOUGH_MONEY:可用保证金不足。减手数,不要死循环重试。
  • 138 ERR_REQUOTE:报价变化。RefreshRates() 后重发,通常 1–2 次即可。
  • 146 ERR_TRADE_CONTEXT_BUSY:交易上下文忙。等待几百毫秒再试,不要立刻狂刷。
  • 4108 ERR_INVALID_TICKET:订单号不存在或已平仓。OrderSelect 失败时常见。

分类原则:参数类错误(130/131/134)修参数,重试没有意义;瞬态类错误(138/146)可以有限重试;订单类错误(4108)先确认订单是否还在。重试次数建议 3 次以内,并在每次之间 Sleep 或记录下一次尝试时间。

止损距离:最容易被忽略的 130

MQL4 的止损是“价格”,不是“点数”。假设 Ask=1.10000,Point=0.00001,止损 8 点就是 1.09920;如果经纪商要求最小止损 20 点,这个价格会被拒绝。正确做法是先读 MarketInfo(Symbol(), MODE_STOPLEVEL),再把点数补到 minStop+5,最后 NormalizeDouble 到 Digits。

空单方向相反:止损在入场价上方,止盈在下方;多单止损在下方,止盈在上方。方向写反时,常见错误码也是 130。

完整示例:带分类重试的下单函数

下面的 SendBuy 先校正止损距离,再最多尝试 3 次。130/131/134 直接返回,138/146 等待后刷新报价再试。每次失败都打印 attempt 与 err,方便回测日志定位。

#property strict
input int MagicNumber = 20261102;
 
int SendBuy(double lots, int slPts, int tpPts, int magic) {
    if(!IsTradeAllowed()) {
        Print("交易不允许,放弃下单");
        return -1;
    }
    RefreshRates();
    int minStop = (int)MarketInfo(Symbol(), MODE_STOPLEVEL);
    if(slPts < minStop) slPts = minStop + 5;
    if(tpPts < minStop) tpPts = minStop + 5;
 
    double price = Ask;
    double sl = NormalizeDouble(price - slPts * Point, Digits);
    double tp = NormalizeDouble(price + tpPts * Point, Digits);
 
    for(int attempt = 1; attempt <= 3; attempt++) {
        ResetLastError();
        int ticket = OrderSend(Symbol(), OP_BUY, lots, price, 3,
        sl, tp, "Adv6", magic, 0, clrBlue);
        if(ticket > 0) return ticket;
 
        int err = GetLastError();
        PrintFormat("下单失败 attempt=%d err=%d", attempt, err);
        if(err == 130 || err == 131 || err == 134) return -1;
        if(err == 146 || err == 138) {
            Sleep(500);
            RefreshRates();
            price = Ask;
            sl = NormalizeDouble(price - slPts * Point, Digits);
            tp = NormalizeDouble(price + tpPts * Point, Digits);
            continue;
        }
        return -1;
    }
    return -1;
}

注意:OrderSend 的 price 参数对市价单是 Ask/Bid,对挂单是挂单价。重试时价格会变,所以要重新 RefreshRates() 并重算 sl/tp,不能沿用第一次的绝对价格。

重试的边界

重试不是越多越好。每一次重试都可能在下一次 Tick 再次触发,形成“失败—重试—再失败”的雪崩。工程上建议:

  • 同一个信号最多重试 3 次,超过就记录并等待下一根 K 线。
  • 用静态变量记录上次尝试时间,避免每个 Tick 都重试。
  • 区分“可重试”和“不可重试”,不可重试的错误立即停止并报警。
  • 回测里把失败次数、错误码分布打印出来,不要只看净利润。

常见错误

  • 只看 OrderSend 返回值,不看 GetLastError:知道失败,不知道为什么。
  • 130 也重试:参数没改,重试一百次还是 130,只是刷日志。
  • 146 立刻重试:上下文还没释放,越试越忙。先 Sleep 或等下一个 Tick。
  • 忘记 NormalizeDouble:价格小数位过多,部分经纪商会拒绝或四舍五入到意外价位。
  • 在指标里调用 OrderSend:指标不能交易,应该把信号写给 EA。

小结

错误处理是一条决策树:先看返回值,再读错误码,再按错误码分类。参数错误修参数,瞬态错误有限重试,订单错误先确认状态。把这条树写进代码,你的 EA 才能在下单失败时给出可解释的行为,而不是沉默或刷屏。

日志怎么写才有用

失败日志至少包含六个字段:函数名、attempt、错误码、品种、方向/手数、关键价格。例如 `PrintFormat("OrderSend BUY fail attempt=%d err=%d sym=%s lots=%.2f ask=%.5f sl=%.5f", attempt, err, Symbol(), lots, Ask, sl);`。只写“下单失败”四个字,等于没写。回测日志还可以加上 `TimeCurrent()` 和当前 K 线时间,方便定位是哪个时间段出的问题。

日志级别也要控制。成功下单打印一行 INFO;可重试的 138/146 打印 WARN;参数错误 130/131/134 打印 ERROR。不要用 Print 输出整个订单结构,日志会变得不可读。生产环境建议把日志写到文件,并按天轮转;MQL4 的文件操作要用 FILE_COMMON 或 relative path,注意不要与同目录的其他 EA 冲突。

重试状态机

比“for 循环重试三次”更稳的做法是状态机:用全局变量记录 `retryCount`、`nextRetryTime`、`lastError`。OnTick 或 OnTimer 发现 `TimeCurrent() >= nextRetryTime` 时才尝试,失败后设置下一次时间。这样不会在一个 Tick 里连续发三次请求,也不会因为 Sleep 阻塞事件循环。

状态机还要有“放弃条件”:重试次数达到上限、错误码属于不可重试类别、当天亏损达到上限、点差超过阈值。满足任一条件就回到空闲状态,等待下一个信号。把状态写进日志,复盘时能清楚看到“为什么没有再试”。

IsTradeContextBusy 与 146

146 表示交易上下文忙,常见于多个 EA 同时下单、终端正在处理上一个请求、或网络延迟较高。IsTradeContextBusy() 可以提前判断,但它不是万能的:检查通过后、OrderSend 发出前,上下文仍可能被占用。所以即使先调用了 IsTradeContextBusy(),OrderSend 仍要检查返回值和 146。

处理 146 的正确顺序是:等待(Sleep 或状态机延时)→ RefreshRates() → 重新计算价格与止损 → 再发送。不要原样重发旧价格,行情可能已经变了,旧价格会触发 138 或滑点过大。

止损止盈的方向检查

多单:止损价 < 开仓价 < 止盈价;空单:止盈价 < 开仓价 < 止损价。写反方向时,除了 130,还可能出现“止损一开仓就触发”的异常。下单前用两行代码自检:`if(cmd==OP_BUY && !(slprice)) return -1;`,空单同理。这个检查比事后看错误码更便宜。

还要检查距离是否满足 MODE_STOPLEVEL 和 MODE_FREEZELEVEL。前者是最小止损距离,后者是冻结距离(价格太接近时不允许修改)。不同经纪商、不同品种的数值不同,不要写死 20 点。

资金不足与手数校正

134 不是“重试就能解决”的错误。它表示可用保证金不足以支撑当前手数。正确做法是先读 AccountFreeMargin()、MarketInfo(Symbol(), MODE_MARGINREQUIRED)(部分版本)或按杠杆估算,再把手数降到可用范围内。手数还要满足 MODE_MINLOT、MODE_MAXLOT、MODE_LOTSTEP:先按步长向下取整,再夹在最小/最大之间,最后 NormalizeDouble 到 2 位。

不要用“每次失败减 0.01”这种试探式降手数,既慢又可能在下一次 Tick 重复。把手数计算做成独立函数,输入是风险百分比和止损点数,输出是合法手数;下单失败时只记录,不盲目递归。

错误码与日志级别

建议把错误码分成三档:INFO(138 重试成功)、WARN(146 等待后重试)、ERROR(130/131/134/4108 不可重试)。日志级别不是为了好看,而是为了在几千行日志里快速筛选。回测时只打印 WARN/ERROR,实盘时再打开 INFO,能显著减少日志体积。

错误码不是越全越好。你不需要记住所有 4xxx 错误,只要把最常见的十几类覆盖到,剩下的统一走“记录并停止”分支。未知错误继续重试,往往比停止更危险。

交互演示:130 与 146:错误码怎么处置

第一次下单因止损 8 点小于最低 20 点报 130;修正到 25 点后又遇到 146 上下文忙;等待后第三次成功。

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

练习题

  • 6.3 错误处理与 GetLastError 选择题
    1.OrderSend 返回 -1 后,读取失败原因应调用? (2分) [选择]
    A. GetLastError()
    B. OrderTicket()
    C. MarketInfo()
    D. RefreshRates()
    2.错误码 130 通常表示? (2分) [选择]
    A. 资金不足
    B. 止损/止盈距离不合法
    C. 交易上下文忙
    D. 订单号无效
    3.遇到 130 错误时,应该立即原样重试,通常第二次就会成功。 (2分) [判断]
  • 6.3 错误处理与 GetLastError 编程实战(AI 批改)
    1.【实战】写带分类重试的 SendBuy (10分) [MQL4自动批改]
    写函数 int SendBuy(double lots,int slPts,int tpPts,int magic):先判断 IsTradeAllowed,RefreshRates;读取 MODE_STOPLEVEL,若 slPts/tpPts 小于它则补到 minStop+5;最多重试 3 次,每次 ResetLastError 后 OrderSend;ticket>0 返回 ticket;130/131/134 直接返回 -1;138/146 则 Sleep(500)、RefreshRates 并重算价格后续试;其它错误返回 -1。必须出现 ResetLastError、GetLastError、OrderSend、MODE_STOPLEVEL。