第6章 进阶实战:从能跑走向可靠

第 37 节 / 共 43 节 练习题 2 组 课程:MQL4 教程与 MT4 EA 编程
第五章结束时,你已经能写出一个“能编译、能下单”的均线交叉 EA。但能跑不等于可靠:它可能在周末没有 Tick 时停摆,可能把别的 EA 的订单一起平掉,可能因为止损太近连续报 130,也可能在回测里表现很好、上线后立刻回撤。第六章补的就是这六个工程缺口。

为什么“能跑”还不够

教学示例通常只保留最顺利的路径:有 Tick、点差正常、止损合法、订单池里只有自己的单。真实账户里,这些条件随时会变。一个 EA 要长期运行,至少要回答四个问题:没有行情时谁在巡检?订单池里哪些单属于我?下单失败后怎么知道原因?参数是回测出来的,还是拍脑袋定的?

这四个问题对应三种能力:事件组织(OnTick 与 OnTimer 分工)、状态管理(订单池、错误码、面板对象)、验证方法(指标缓冲区、回测与样本外检验)。它们不改变策略本身,却决定策略能不能被信任。

第六章地图

  • 6.1 OnTimer 定时器:让 EA 在没有 Tick 时也能做心跳、移动止损和状态巡检。
  • 6.2 订单池遍历:用 OrderSelect 逐条读取,按品种和魔术号过滤,倒序平仓不漏单。
  • 6.3 错误处理:ResetLastError、GetLastError、止损距离与重试边界。
  • 6.4 自定义指标:缓冲区、prev_calculated、OnCalculate 与 iCustom 的 shift。
  • 6.5 图表对象与面板:ObjectCreate、OnChartEvent、OnDeinit 清理。
  • 6.6 回测与参数优化:样本内、样本外、前推滚动与过拟合。

一个可扩展的 EA 骨架

先把事件入口固定下来:OnInit 只做注册和初始化,OnTick 只处理行情,OnTimer 只做轻量巡检,OnDeinit 负责释放。下面的骨架可以直接编译,后续每一节往对应入口里填逻辑。

#property strict
input int MagicNumber = 20261102;
input double Lots = 0.10;
input int TimerSec = 1;
 
int OnInit() {
    EventSetTimer(TimerSec);
    Print("进阶 EA 启动,定时器 ", TimerSec, " 秒");
    return INIT_SUCCEEDED;
}
 
void OnDeinit(const int reason) {
    EventKillTimer();
    Print("EA 停止,reason=", reason);
}
 
void OnTick() {
    if(!IsTradeAllowed()) return;
    static datetime lastBar = 0;
    if(Time[0] == lastBar) return;
    lastBar = Time[0];
    // 只在新 K 线做一次策略判断,避免同一根 Bar 反复下单
}
 
void OnTimer() {
    if(!IsConnected()) return;
    // 轻量巡检:心跳、面板刷新、订单状态
}

这段骨架有意留空策略主体。先让四个入口都能打印日志、都能安全返回,再逐步加功能,比一开始堆满逻辑更容易定位问题。

事件顺序:一张图记住

EA 加载时先调用 OnInit,然后终端在每次价格变化时调用 OnTick,在定时器到点时调用 OnTimer,两者互不阻塞。EA 被移除、图表关闭或参数改变时调用 OnDeinit。参数改变会触发一次完整的“卸载—重载”,所以不要在 OnInit 里假设全局变量一定还是旧值。

需要特别注意的是:OnTimer 不是“每隔 N 秒一定执行一次”的硬实时保证。终端繁忙、图表切换、网络抖动都会让触发时间漂移。它适合做巡检和节流,不适合做毫秒级抢单。

调试顺序

  1. 先用 Print 确认四个入口都被调用,且 reason 参数符合预期。
  2. 再确认订单池过滤条件:只操作 Symbol() 与 MagicNumber 都匹配的单。
  3. 然后模拟失败:把止损设到最小止损以内,观察 GetLastError 是否按预期返回 130。
  4. 最后才看回测收益。事件与错误路径没跑通之前,收益曲线没有参考价值。

先建立可观测性

工程版 EA 和教学版 EA 最大的差别,不是策略更复杂,而是“出错时能看见”。下单时把 ticket、品种、方向、手数、价格、止损、止盈、魔术号写进订单注释或日志;失败时把函数名、attempt、错误码、当前点差一起打印。这样复盘时不需要猜,直接能在日志里还原当时的上下文。

日志也要有节制。每个 Tick 都 Print 会把日志文件迅速撑大,还会拖慢回测。推荐只在“状态发生变化”时打印:新 K 线、下单成功、下单失败、订单被修改、定时器发现异常。用 PrintFormat 统一格式,例如 `PrintFormat("BUY ticket=%d lots=%.2f price=%.5f err=%d", ticket, Lots, price, err);`,比字符串拼接更容易读。

把策略拆成三层

第一层是信号:什么时候想买、想卖。第二层是风控:手数多大、止损多远、同时最多几单、当天最大亏损多少。第三层是执行:怎么下单、失败怎么处理、订单怎么维护。教学示例常把三层揉在一个 OnTick 里,改一处就牵动全身。工程版应该把三层拆成独立函数,OnTick 只负责“取数据—调信号—交给风控—调执行”。

拆层之后,回测也更好做。你可以单独测试信号层(固定手数和止损),再测试风控层(固定信号),最后组合。出现问题时,能快速判断是信号失效、风控过严,还是执行滑点太大。

上线前的十分钟检查

  1. 把 EA 挂到模拟账户,确认 OnInit、OnTick、OnTimer、OnDeinit 四个入口都有日志。
  2. 手动平掉一张单,确认订单池遍历不会把别的 EA 的单一起处理。
  3. 把止损设到最小止损以内,确认错误码是 130,且日志里有 attempt 与 err。
  4. 切换一次周期,确认 OnDeinit 清理了定时器和面板对象,没有重复日志。
  5. 用策略测试器跑最近三个月,确认最大回撤、交易次数、盈亏比都在可接受范围。

版本与兼容性

MQL4 与 MQL5 的语法相似,但交易函数、指标缓冲区和事件模型差异很大。本课程以 MT4 build 600+ 的 MQL4 为准。使用 `#property strict` 可以让编译器按更严格的规则检查类型和返回值,减少隐式转换带来的 bug。老代码里常见的 `extern` 在新代码中建议改成 `input`,后者会出现在策略测试器的参数列表里,方便优化。

不同经纪商的 StopLevel、FreezeLevel、最小手数、最大手数、佣金模型都不同。不要把这些值写死在代码里,用 MarketInfo 读取;也不要在多个经纪商之间直接复制参数,先用模拟账户跑一遍最小手数验证。兼容性问题的根源往往不是语法,而是交易规则。

常见错误

  • 把 EventSetTimer 写在 OnTick 里:每来一个 Tick 就重设定时器,定时器永远等不到下一次触发。只在 OnInit 里注册一次。
  • OnDeinit 忘记 EventKillTimer:切换周期或参数后,旧定时器可能残留,日志出现重复输出。
  • 在 OnInit 里下单:此时报价、指标和订单环境都还没稳定,应该等第一个 Tick。
  • 把策略主体同时写在 OnTick 和 OnTimer:两个入口都会下单,仓位翻倍。

小结

第六章的关键词是“边界”:事件边界、订单边界、错误边界、数据边界。每一节都会给你一个可以单独编译的小例子,再说明它在真实账户里最容易坏在哪里。学完这一章,你应该能把一个教学 EA 改造成能长期观察、能解释失败原因、能复盘的工程版本。

练习题

  • 第6章 进阶实战:从能跑走向可靠 选择题
    1.EA 中注册定时器的正确位置通常是? (2分) [选择]
    A. OnInit
    B. OnTick
    C. OnTimer
    D. OnDeinit
    2.OnDeinit 中释放定时器应调用哪个函数? (2分) [选择]
    A. EventSetTimer
    B. EventKillTimer
    C. Sleep
    D. RefreshRates
    3.OnTimer 的触发间隔是硬实时保证,误差不会超过 1 毫秒。 (2分) [判断]
  • 第6章 进阶实战:从能跑走向可靠 编程实战(AI 批改)
    1.【实战】写出带定时器与安全退出的 EA 骨架 (10分) [MQL4自动批改]
    写一个 EA 骨架:OnInit 中注册 1 秒定时器并返回 INIT_SUCCEEDED;OnDeinit 中关闭定时器;OnTick 中先判断 IsTradeAllowed,再用 Time[0] 与静态变量 lastBar 保证每根 K 线只执行一次;OnTimer 中判断 IsConnected 后只打印心跳。代码必须出现 EventSetTimer、EventKillTimer、OnTick、OnTimer 四个关键字。