📘

「事件驱动下的中级过滤逻辑」

「事件驱动下的中级过滤逻辑」

在 MT5 的事件处理框架里,从基础回调过渡到中级用法,核心是把零散的 OnTick / OnTimer 事件做成可复用的状态机,而不是在每个事件里重复写判断。2026 年 3 月 11 日官方示例贴中,这类中级事件写法被标记为第 2 篇,说明社区对「事件分层」的需求在爬升。 外汇与贵金属市场高杠杆、滑点随机,直接裸跑事件回调容易在流动性断层时误触发。建议先用静态变量锁住上一次事件时间戳,再用差值过滤掉 200ms 内的重复 tick 噪声,这部分在示例里是用全局 ulong 变量实现的。 下面这段是原文示例里的骨架,保留了事件去重的最简形态,你可以直接丢进 MT5 的 EA 里验证:同一行情下重复打印次数会明显下降。

MQL5 / C++
class="type">class="kw">ulong lastTickTime=class="num">0;
class="type">int OnInit(){ class="kw">return(INIT_SUCCEEDED); }
class="type">void OnTick(){
   class="type">class="kw">ulong now=GetTickCount64();
   if(now-lastTickTime<class="num">200) class="kw">return;
   lastTickTime=now;
   Print("filtered tick at ", now);
}

为什么 MT5 必须走事件驱动

MetaTrader 5 本质是个带图形界面的平台,鼠标点击、图表重绘、定时器触发全都是事件。若用传统轮询方式处理 GUI,光是盯盘面板的状态同步就能拖垮逻辑可读性。 事件驱动编程把“用户做了什么”和“程序该响应什么”解耦,开发者不再手写消息循环,MT5 内核替你派发事件,你只写回调。对写指标和 EA 来说,这直接砍掉了大量界面相关胶水代码。 上一节我们用一个极简指标打印事件名,已经看到 OnChartEvent 能捕获鼠标和键盘消息。当时留了个不复杂的练习:自己想办法把特定事件过滤出来。花点时间的人基本都能绕通,因为骨架本来就轻。 接下来会放两种解法并拆优缺点。先记住一点:外汇和贵金属杠杆高、滑点随机,任何基于事件的逻辑都只是概率响应,别当成确定性信号。

◍ 用 Deinit 事件拦住周期切换的计数清零

MT5 切图表周期时,平台会先触发 Deinit 把图上指标全拔掉再重绘重挂,OnDeinit 拿到的 reason 参数就藏着“为什么被拔”的信息。想让第 04 行声明的 gl_Counter 只在周期变化时保住数值、其他情况归零,关键点就是读这个 reason。 查文档可知 REASON_CHARTCHANGE 常量对应周期(或品种)变更事件。在 OnDeinit 里加一句判断:若 reason == REASON_CHARTCHANGE 就把 gl_Counter 写进数组留存,否则置零。这样同一品种反复切周期时计数倾向连续,普通重初始化则从头算。 这套写法的好处是逻辑干净,能顺带扩展成“某周期内某事件触发几次”的统计;缺点是依赖文件暂存数值,同一品种同会话里狂切周期可能撞上竞争条件——数值错乱不说,debug 起来极折磨。外汇和贵金属市场高波动,这类计数若用于辅助判断也只作概率参考,别当确定性信号。 下面是把判断塞进原框架后的关键改动,其余代码与之前完全一致:

MQL5 / C++
class=class="str">"cmt">// 原代码 class="num">01 中 OnDeinit 片段改写示例
class="type">void OnDeinit(const class="type">int reason)
{
   class="type">uint arr[class="num">1];
   
   if(reason == REASON_CHARTCHANGE)
      arr[class="num">0] = gl_Counter; class=class="str">"cmt">// 仅周期/品种变更时保留计数
   else
      arr[class="num">0] = class="num">0;          class=class="str">"cmt">// 其余情况重置
   class=class="str">"cmt">// 后续 FileSave 逻辑略
}

「用 OnDeinit 把计数器落盘」

EA 卸载时最容易丢掉运行期统计,这段逻辑在 OnDeinit 里用 FileSave 把关键数值写回文件,方便下次加载续算。 注意第 34 行:仅当卸载原因是 REASON_CHARTCHANGE 时才保留 gl_Counter,其他情况(如手动移除、品种关闭)直接写 0,避免脏数据污染后续会话。 实操上,你可以把这段直接贴进自己的 EA 模板,把 gl_Counter 换成你关心的成交次数或信号触发数,开 MT5 切一次图表验证文件是否生成。外汇与贵金属杠杆高,统计口径错一处就可能误导仓位,验证后再上实盘。

MQL5 / C++
class="type">void OnDeinit(const class="type">int reason)
{
   class="type">uint arr[class="num">1];

   arr[class="num">0] = (reason == REASON_CHARTCHANGE ? gl_Counter : class="num">0);

   FileSave(_Symbol, arr);

   Print(__FUNCTION__);
};

用终端全局变量替你管计数器

第二种思路是把计数器状态交给 MT5 自己保管,而不是靠文件落地。好处是省了我们维护持久化文件的麻烦;代价是控制权交出去后,逻辑会稍微绕一点——因为变量生命周期由平台说了算,不是我们代码完全主控。 这里动用到的是终端全局变量(Global Terminal Variables),在 MQL5 里用 GlobalVariable* 系列函数访问。它原生只能存 double,但借助 union 可以把 uint 计数器塞进 double 的二进制布局里,从而变相存整数。 按 F3 或走菜单打开全局变量窗口,若指标在跑,就能看到以当前交易品种命名的变量;关掉指标通常列表就空了——这资源实际被交易者用得极少。 代码里第 04–08 行声明全局 union,第 12 行 ZeroMemory 清场,第 14 行尝试读取以 _Symbol 为名的全局变量,没有就第 15 行用 GlobalVariableTemp 建一个临时项,失败则返回 INIT_FAILED 让指标卸载。第 18 行 GlobalVariableSet 写回值。 临时全局变量只在 MT5 进程存活期间存在,平台一关就被清掉;若改用非 Temp 方式创建,平台也会在一段时间后自动删。对外汇、贵金属这类高波动品种做跨周期计数时,这种「关平台即丢」的特性可能反而合适,但务必清楚高风险:平台异常退出会丢状态。 OnDeinit 里第 34 行往后区分退出原因:切周期就存值,其他情况删变量。第 40 行删临时变量纯属演示——你可以改条件自己试,看全局变量窗口怎么变。

MQL5 / C++
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">04. union u_01
class="num">05. {
class="num">06.    class="type">class="kw">double  d_value;
class="num">07.    class="type">uint    u_Counter;
class="num">08. }gl_Union;
class="num">09. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">10. class="type">int OnInit()
class="num">11. {
class="num">12.    ZeroMemory(gl_Union);
class="num">13.
class="num">14.    if (!GlobalVariableGet(_Symbol, gl_Union.d_value))
class="num">15.       if (!GlobalVariableTemp(_Symbol))
class="num">16.          class="kw">return INIT_FAILED;
class="num">17.
class="num">18.    GlobalVariableSet(_Symbol, gl_Union.d_value);
class="num">19.
class="num">20.    Print(__FUNCTION__);
class="num">21.
class="num">22.    class="kw">return INIT_SUCCEEDED;
class="num">23. };
class="num">24. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">25. 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="num">26. {

◍ 用全局变量在图表切换时保住状态

指标在 MT5 里被移除或图表周期切换时,OnDeinit 会带着 reason 参数触发。这里用 switch 区分原因:当 reason 等于 REASON_CHARTCHANGE(数值 3),说明只是图表属性变了而非彻底卸载,就把联合体里的 d_value 通过 GlobalVariableSet(_Symbol, ...) 写进终端全局变量,下次加载还能读回来。 其他情况走 default 分支,直接 GlobalVariableDel(_Symbol) 把该交易品种的全局变量删掉,避免残留脏数据占用终端命名空间。最后一行 Print(__FUNCTION__) 只是把函数名打到日志,方便你确认卸载流程确实跑过。 实际验证时开一个 EURUSD 的 H1 指标,切到 M5 再切回 H1,终端全局变量列表里应该能看到 EURUSD 这一项被保留;手动从导航栏删指标,则该项消失。外汇与贵金属品种波动大,全局变量不代替历史缓存,重连后若终端重启会清空,别拿来当跨会话持久层。

MQL5 / C++
class="num">27.    Print(__FUNCTION__, " :: ", ++gl_Union.u_Counter);
class="num">28.
class="num">29.    class="kw">return rates_total;
class="num">30. };
class="num">31. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">32. class="type">void OnDeinit(const class="type">int reason)
class="num">33. {
class="num">34.    class="kw">switch(reason)
class="num">35.    {
class="num">36.       case REASON_CHARTCHANGE:
class="num">37.          GlobalVariableSet(_Symbol, gl_Union.d_value);
class="num">38.          class="kw">break;
class="num">39.       class="kw">default:
class="num">40.          GlobalVariableDel(_Symbol);
class="num">41.    }
class="num">42.
class="num">43.    Print(__FUNCTION__);
class="num">44. };
class="num">45. class=class="str">"cmt">//+------------------------------------------------------------------+

「别急着下结论」

这一节原本是系列文章的收尾,作者没把代码逐行讲透,而是把「怎么钓鱼」的素材丢给你:前一篇讲结构、这篇讲继承与指标雏形,两者要结合着看才可能真正吃透。附录里的 Anexo.zip 只有 1.66 KB,装的是本篇演示用的基础代码,不是成品指标。 下一篇才会做真正的指标,但前提是你得先弄明白 MT5 里事件是怎么生成的、终端能帮你省多少事。不然很多接口和钩子对你就是关着的门。 外汇和贵金属这类品种杠杆高、滑点凶,拿这些示例直接上实盘大概率被扫。先把 zip 里的代码在策略测试器里跑一遍,改一个继承参数看看输出差异,比急着拼指标有用。

常见问题

用图表卸载事件把计数器写入终端全局变量,切回时再读回来,状态就不会丢。
普通初始化在周期切换时会重跑,要走事件驱动在卸载事件里落盘计数,才能跨框架保留。
可以,小布能替你跨图表和周期维护这类状态,你只管看它给出的过滤信号就行。
在,全局变量存在终端层,图表关闭或切换也不清,适合做计数持久化。
周期切换拦图表卸载事件做落盘,指标移除同样走卸载事件把计数写全局变量即可。