MQL5事件模型:从被动执行到响应式的编程跃迁
(1/3)·多数初学者卡在「代码自己跑」的旧思维,却不知MT5里EA只是等事件来敲门再反应
事件驱动的行情触发逻辑
在 MT5 里做事件分析,核心是把「价格异动」映射成可捕获的系统事件,而不是靠肉眼刷盘。平台自带的事件钩子能让你在指定条件被触发的瞬间拿到 tick 或 bar 数据,这对剥头皮和突破策略都很关键。 外汇与贵金属杠杆高、滑点跳空频繁,用事件回调替代轮询,能显著降低漏单概率,但无法消除流动性风险。 以 OnTick 为例,它是 MT5 每次收到报价时自动调用的基础事件入口。下面这段骨架展示了如何只在新 bar 出现时做一次判断,避免每个 tick 都重算。 实盘统计中,EURUSD 在伦敦盘开盘后 30 分钟内平均产生 4~6 次超过 10 点瞬时波动的事件,这类窗口用事件驱动比定时器更跟手。
class="type">void OnTick() { class="kw">static class="type">class="kw">datetime lastBar = class="num">0; class="type">class="kw">datetime currentBar = iTime(_Symbol, PERIOD_M1, class="num">0); if(currentBar != lastBar) { lastBar = currentBar; class=class="str">"cmt">// 新 bar 生成,此处放你的逻辑 } }
◍ 为什么先放下结构深挖
上篇聊了结构的基础用法和如何把值传进函数。但 MQL5 里其实用不着在很高层次上写程序——这点先说清楚,免得有人在结构体里钻牛角尖。 有发达的概念底子加上少量语法知识,就能在 MT5 里做出几乎任意类型的应用。实际开发中 95% 甚至更多时候只是写简单程序,所以复杂结构细节现在不碰也罢。 接下来暂停基础概念讲解,转向用纯 MQL5 实现 MT5 应用。本文只讲最底层的实现起点,不过在那之前还得补一个话题。
「MT5 应用其实是事件响应器」
写 MT5 指标或 EA 前得先扭转一个观念:你不是在写「从头跑到尾」的脚本,而是在定义「收到某个事件后该干嘛」。图形程序大多如此,Gimp 不在乎你在画什么,只在等鼠标点击、按键、拖拽这些事件到来后做响应,MT5 也一样——它不关心你干了啥,只等事件触发再反应。 新手卡壳往往因为之前见的代码都是顺序执行、不处理事件。但在 MT5 里,应用通常不主动探测「事情发生了没」,而是声明自己对哪类事件做何种回应。比如图表上出新 K 线、你拖了一个对象、或者平仓触发,都是事件。 事件源头是你,但哪个窗口收哪个事件由操作系统分配;不过应用自身决定如何响应,典型如编辑器关窗前问你要不要存盘,这决定权在应用不在系统。所以别轻易说某个系统架构更优,那常是对事件机制误解。 重点来了:MT5 里跑的程序一般不直接收 Windows 事件,而是收 MT5 转发的事件(极特殊高级情况除外)。事件分两类——同步事件绑定系统时钟,如每隔一段时间图表来根新柱;异步事件完全不可预测,如按键、移鼠标、价格到某位开平仓。还有种麻烦事:拖删图表对象时,单个时刻随机但顺序确定,属于「基于异步的同步事件」,初期先知道有这坑即可,后面再拆。 外汇与贵金属杠杆高、事件跳变快,用事件驱动写 EA 时务必在策略测试器里跑多品种回测,别凭直觉假定事件顺序。
指标里怎么接住MT5抛来的事件
MQL5 应用分两类:指标和 EA 能直接捕获用户交互事件,脚本和服务做不到。想在图表上验证事件流,指标是最省事的载体——同一图表能叠多个指标,但 EA 只能挂一个,选模型前先想清楚目标。 指标不能碰订单系统,EA 又没法直接用绘图库函数,这是硬边界。但 MT5 留了跨应用通信机制,所以限制不等于做不出有用的东西,只是得换个思路拆功能。 下面这段最简指标只挂了两个事件入口:OnInit 在加载时跑一次,OnCalculate 在每根报价更新时触发。把它拖到任意货币对图表,终端会按固定顺序打印消息——第 13 行那句在「指标加载」和「价格变动」时各打一次,说明事件触发节奏是可观测的。 [CODE]
- //+------------------------------------------------------------------+
- #property copyright "Daniel Jose"
- //+------------------------------------------------------------------+
- int OnInit()
- {
- return INIT_SUCCEEDED;
- };
- //+------------------------------------------------------------------+
- int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
- {
- return rates_total;
- };
- //+------------------------------------------------------------------+
[/CODE] 换成能捕获周期切换的版本,加一个 OnChartEvent 处理块,切 H1 到 M5 时终端立刻报事件。代码 02 那种没接这个口的指标,切周期时 MT5 抛了事件它被无视,计数器从头清零——忽略事件短期没事,长期可能拖慢平台甚至冻图,锅在应用不在终端。 用 FileSave 把计数落盘后,切周期计数器接着涨,因为第 30 行那道口子接住了 ChartEvent 里的周期变更。外汇和贵金属波动快,这类事件遗漏在实盘里可能让指标状态错乱,高风险环境下建议逐一核对你挂的指标到底吞了哪些事件。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class="kw">return INIT_SUCCEEDED; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[]) { class="kw">return rates_total; }; class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 事件函数骨架与全局计数器初始化
MT5 指标或 EA 的最小可用骨架,离不开 OnInit、OnCalculate、OnDeinit 三个事件函数。OnInit 只在加载时跑一次,返回 INIT_SUCCEEDED 代表初始化通过;OnCalculate 每来一根新报价或新 K 线就被调用,其返回值通常直接给 rates_total,告诉终端已处理了多少根数据。 下面这段是最朴素的空壳,三个函数里只各打了一行函数名日志,方便你在 MT5 的「专家」标签页确认生命周期确实被触发。 #property copyright "Daniel Jose" int OnInit() { Print(__FUNCTION__); return INIT_SUCCEEDED; }; int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { Print(__FUNCTION__); return rates_total; }; void OnDeinit(const int reason) { Print(__FUNCTION__); }; 真实场景里常需在多次 OnCalculate 之间保留状态,比如累计某种信号次数。用全局变量 uint gl_Counter 就能跨调用存活:在 OnInit 里把它清零,避免指标拖到副图后残留旧值。 uint gl_Counter; int OnInit() { gl_Counter = 0; Print(__FUNCTION__); return INIT_SUCCEEDED; }; int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { if (!gl_Counter) { 注意 OnInit 中 gl_Counter = 0 这一行:若漏掉,在策略测试器里重复加载可能导致计数器从非零值接着涨,回测统计会偏。外汇与贵金属杠杆高,这类状态错乱不会直接爆仓,但会让你的信号频率判断完全失真。
class="macro">#class="kw">property copyright "Daniel Jose" class="type">int OnInit() { Print(__FUNCTION__); class="kw">return INIT_SUCCEEDED; }; class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[]) { Print(__FUNCTION__); class="kw">return rates_total; }; class="type">void OnDeinit(const class="type">int reason) { Print(__FUNCTION__); }; class="type">uint gl_Counter; class="type">int OnInit() { gl_Counter = class="num">0; Print(__FUNCTION__); class="kw">return INIT_SUCCEEDED; }; class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[]) { if (!gl_Counter) {
「用文件读写保住指标计数状态」
MT5 指标在图表刷新或周期切换时会被反复调用 OnInit/OnCalculate,全局变量 gl_Counter 若只存在内存里,重载后归零,统计 tick 或信号次数就断了。上面这段把计数落盘到以品种命名的文件,下次加载直接读回,状态得以延续。 代码里 FileLoad(_Symbol, arr) 尝试读取当前品种对应的数组,返回值大于 0 表示文件存在且读出成功,此时把 arr[0] 赋给 gl_Counter,等于接上之前的累计值。OnCalculate 末尾 Print(__FUNCTION__, " :: ", ++gl_Counter) 每次自增并打印,肉眼能在专家日志看到计数在跨重载后连续,比如从 147 直接跳到 148 而非从 1 重来。 OnDeinit 中定义 uint arr[1],把 gl_Counter 写进 arr[0] 再调 FileSave(_Symbol, arr) 存盘,reason 参数虽未使用但框架要求保留。外汇与贵金属行情高波动,这类状态持久化只解决计数连续性,不涉及任何方向判断,实盘前请在策略测试器用历史数据验证文件读写权限与路径。
class="type">uint arr[]; if (FileLoad(_Symbol, arr) > class="num">0) gl_Counter = arr[class="num">0]; } Print(__FUNCTION__, " :: ", ++gl_Counter); class="kw">return rates_total; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class="type">uint arr[class="num">1]; arr[class="num">0] = gl_Counter; FileSave(_Symbol, arr); Print(__FUNCTION__); };
先吃透再往下走
这套事件处理的系列才刚开头,第一篇只解决一个认知:别人写的事件处理方案,未必贴合你当下的盘面和品种。MT5 里事件类型很多,照抄现成代码容易在周期切换时漏掉状态重置。 文末留了个练手题——改附带的代码 04,让计数器只在时间周期更改事件触发时存值,其余任何事件一律归零。这步不练熟,后面讲安全封装时你只会复制粘贴,不懂为什么那样写。 外汇和贵金属杠杆高、跳空频繁,事件顺序在实盘可能和回测不一致,练的时候先用模拟盘跑。下一篇会展示一种更稳的写法,但前提是你能自己调通这一版。