通用智能交易系统事件模型:从集中处理到策略原型的底层铺垫(基础篇)
⚙️

通用智能交易系统事件模型:从集中处理到策略原型的底层铺垫(基础篇)

(1/3)· 多数 EA 散落各处处理事件,本文先把集中事件模型与基类讲清,后续才好直接套用

含代码示例实战向 第 1/3 篇
把 OnTick、OnTimer、OnTradeTransaction 拆在多处写的 EA,后期改逻辑基本等于重写。事件一多,顺序和遗漏就靠人脑兜底,回测时根本追不清哪段先跑。先把事件收口到一处,是少踩坑的第一步。

◍ 事件模型下的EA骨架从哪搭

在 MT5 里写通用智能交易系统,核心不是先堆指标,而是先定事件模型:把行情 tick、定时器、成交回执、持仓更新都当成独立事件源,EA 只在事件触发时跑对应分支。这样能把策略逻辑和底层接口解耦,后续换品种或加过滤条件不用重写主循环。 原文给出的系列示例首发于 2016 年 4 月 20 日,至今在源平台累计阅读 5710、评论 41,说明这种事件驱动写法在 MQL5 社区是被反复验证过的实用范式,而不是玩具代码。 开 MT5 新建 EA 时,直接看 OnInit/OnTick/OnTradeTransaction 三个系统函数,先想清楚每个事件你只做一件什么事,比一上来写几百行判断要干净得多。外汇与贵金属杠杆高,事件漏处理可能造成滑点放大,建议先在策略测试器用历史数据跑通事件分支再上模拟盘。

「这套框架到底分几块」

MQL5 这套事件驱动框架并不是把逻辑堆在主函数里,而是按职责切成几条线。先看目录就能知道,它用集中处理的事件模型来收口,类型枚举叫 ENUM_MARKET_EVENT_TYPE,所有市场事件都走这一层。 除了行情本身,它还用 MarketEvent 结构体去处理其他交易对象上发生的事件,比如账户、仓位状态变化。新柱形事件单独拎出来,配了一套新的报价和柱形检测算法,这点和老式 OnTick 里手动判断时间轴不一样。 底层给了 CPositionMT5 类当平台基类,把独立算法从策略里剥开。策略侧只留一个 CStrategy 原型类,你之后写 EA 基本就是继承它填逻辑。整篇剩下的章节会顺着这条线展开,开 MT5 新建个空 EA 把这几个类名搜一遍,能直接看到官方源里的定义位置。

CStrategy 引擎把事件和模型缝进一个壳

上篇已经把通用 EA 的四种方法拆开讲过:两个开仓、两个平仓,不同组合就定义出一种交易模型,比如只做空、只做多、或只管理已有仓位。但模型本身只是骨架,真正跑起来还得有事件驱动。 第二部分要落地的,是一套集中式事件模型,思路和 MT5 把系统事件摊在一处的默认机制不一样。好处后面会展开,但核心点是:CStrategy 把事件和模型绑成一个可继承的框架,你写的自定义 EA 直接继承它就行,不用从零搭轮。 配套还有一个 CPosition 类,专门管未平仓头寸的操作——平仓、改止盈止损都归它。它把交易动作从平台细节里抽出来,你调方法时不依赖具体交易品种的运行环境。 注意一条硬规则:策略层不提供按头寸搜索、也不让你直接碰系统事件。换句话说,你不用操心事件该由哪个处理器消费、执行顺序怎么排,CStrategy 已经包了。自定义 EA 只写自己的交易逻辑即可。 这套引擎就是给普通用户用的,算法细节可跳过。你只要记住 CStrategy 暴露的函数签名和调用约定,开 MT5 新建一个继承 CStrategy 的 EA,就能把精力全放在信号和仓位上。外汇和贵金属波动剧烈,杠杆风险高,任何框架都不替你兜底亏损。

◍ 用枚举把散落的事件收口到一处

MT5 自带一堆事件:报价跳动有 OnTick,深度变化有 OnBookEvent,还有 Timer、TradeTransaction 这类系统事件,每个都对应一个 On* 处理函数。EA 若把开仓、平仓逻辑分塞进不同事件回调,代码会越写越碎,可读性随之下降。 更务实的做法是把交易决策集中到一个调度点。问题在于,平台并非所有事件都给回调——比如「当前图表新柱开盘」就没有原生通知,只能 EA 自己用时间或成交量去探。我们定义的 ENUM_MARKET_EVENT_TYPE 就是把系统事件和这类自造事件统一编号:MARKET_EVENT_TICK 是报价到来,MARKET_EVENT_BAR_OPEN 是新柱开始,MARKET_EVENT_TIMER 是计时器,MARKET_EVENT_BOOK_EVENT 是深度变动。 假设通用 EA 有 InitBuy、InitSell、SupportBuy、SupportSell 四套逻辑,只要把它们都接同一个事件枚举做参数,就能在 CallExpertMagic 里按传入的事件 ID 分流。下面这段简化框架里,OnTick 与 OnBookEvent 只负责把事件喂给集中处理器,真正的买卖判断不分散在回调中。 把这段代码贴进 MT5 跑一下,EA 会在日志打印每次被调方法的名字和事件标识,你就能直观看到哪些事件真正触发了策略——外汇与贵金属波动剧烈,这类集中模型也只是降低逻辑耦合,不消除爆仓风险,参数请先在模拟盘验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  EA的tick函数                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   class=class="str">"cmt">// 处理新的报价到来事件
   class=class="str">"cmt">// ...
  }
class="type">void OnBookEvent(const class="type">class="kw">string& symbol)
{
   class=class="str">"cmt">// 这里我们处理定单簿价格变化
   class=class="str">"cmt">// ...
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 确定市场事件类型                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_MARKET_EVENT_TYPE
  {
   MARKET_EVENT_TICK,           class=class="str">"cmt">// 新报价到来
   MARKET_EVENT_BAR_OPEN,       class=class="str">"cmt">// 新柱形开始
   MARKET_EVENT_TIMER,          class=class="str">"cmt">// 激活计时器
   MARKET_EVENT_BOOK_EVENT      class=class="str">"cmt">// 市场深度变化(包括新报价到来)
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                 ExampExp.mq5 |
class=class="str">"cmt">//|          Copyright class="num">2015, Vasiliy Sokolov. |
class=class="str">"cmt">//|                     [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Strategy\Series.mqh>
class="type">ulong ExpertMagic=class="num">12345; class=class="str">"cmt">// EA的编号
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 确定市场事件类型                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_MARKET_EVENT_TYPE
  {
   MARKET_EVENT_TICK,           class=class="str">"cmt">//当前货币对的新报价到来
   MARKET_EVENT_BAR_OPEN,       class=class="str">"cmt">// 当前货币对新柱形开始
   MARKET_EVENT_TIMER,          class=class="str">"cmt">// 激活计时器
   MARKET_EVENT_BOOK_EVENT      class=class="str">"cmt">// 市场变化深度(包括报价到来)
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 通用EA的原型                                                      |

「把事件类型接进专家类的四个钩子」

在 MT5 的 EA 骨架里,先把买卖的初始化与持仓维护拆成四个独立方法,比塞进一个 OnTick 里更易排查信号来源。下面这段类声明定义了 InitBuy / InitSell / SupportBuy / SupportSell,全部接收 ENUM_MARKET_EVENT_TYPE 的引用,方便上层把具体事件 ID 透传进来。 四个方法的实现目前都只做一件事:用 printf 把函数名和事件枚举转成字符串打出来。EnumToString 在 MT5 里能把枚举值还原成可读文本,实盘跑起来后在专家日志里能看到类似 'InitBuy EventID: MARKET_EVENT_TICK' 这样的行,确认事件路由没接错。 OnInit 里只干了一行关键活——EventSetTimer(1),也就是每 1 秒触发一次 OnTimer。外汇与贵金属波动快、跳空频繁,这种 1 秒粒度的定时器适合做轻量事件轮询,但高频调用仍可能拖慢老机型,建议先在策略测试器里用 2023 年全年 EURUSD 数据跑一遍确认无卡顿。 类外还声明了 event_type、Expert 实例、last_time 和 CTime Time,这些是后续把时间过滤和事件判定补进来的占位符,现在留空不影响编译。

MQL5 / C++
class CExpert
  {
class="kw">public:
   class="type">void              InitBuy(ENUM_MARKET_EVENT_TYPE &event_id);
   class="type">void              InitSell(ENUM_MARKET_EVENT_TYPE &event_id);
   class="type">void              SupportBuy(ENUM_MARKET_EVENT_TYPE &event_id);
   class="type">void              SupportSell(ENUM_MARKET_EVENT_TYPE &event_id);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CExpert::InitBuy(ENUM_MARKET_EVENT_TYPE &event_id)
  {
   printf(__FUNCTION__+" EventID: "+EnumToString(event_id));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CExpert::InitSell(ENUM_MARKET_EVENT_TYPE &event_id)
  {
   printf(__FUNCTION__+" EventID: "+EnumToString(event_id));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CExpert::SupportBuy(ENUM_MARKET_EVENT_TYPE &event_id)
  {
   printf(__FUNCTION__+" EventID: "+EnumToString(event_id));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CExpert::SupportSell(ENUM_MARKET_EVENT_TYPE &event_id)
  {
   printf(__FUNCTION__+" EventID: "+EnumToString(event_id));
  }
ENUM_MARKET_EVENT_TYPE event_type;
CExpert Expert;
class="type">class="kw">datetime last_time;
CTime Time;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   EventSetTimer(class="num">1);
   class="kw">return INIT_SUCCEEDED;
  }

把事件分发交给统一入口

EA 的事件驱动骨架,核心是把不同触发源归一化后再跑策略逻辑。OnTick 里先标 TICK 事件并调用逻辑,再用 last_time 与 Time[0] 比对,若新 K 线开启则补发 BAR_OPEN 事件,避免每跳重复初始化。 OnTimer 与 OnBookEvent 都直接置为 MARKET_EVENT_TIMER 并调同一函数,深度簿变动和定时器共用一套处理路径,逻辑收敛后更好维护。 OnDeinit 只做 EventKillTimer(),卸载时清掉定时器,防止残留句柄拖慢终端。 CallExpertLogic 按 InitBuy、InitSell、SupportBuy、SupportSell 顺序派发,回测日志显示 2015.11.30 14:13:39.981 在 Si-12.15 M1 上四个子模块同时收到 BAR_OPEN,随后 40.015 的 TICK 只触发 InitSell,说明事件类型确实控制了分支执行。 外汇与贵金属品种波动剧烈、杠杆风险高,上述结构仅降低代码耦合,不预示任何胜率。开 MT5 把这段挂到 EA 里,打印 EventID 就能验证事件分发是否符合预期。

MQL5 / C++
class="type">void OnDeinit(const class="type">int reason)
  {
   EventKillTimer();
  }
class="type">void OnTick()
  {
   event_type=MARKET_EVENT_TICK;
   CallExpertLogic(event_type);
   if(last_time!=Time[class="num">0])
     {
      event_type=MARKET_EVENT_BAR_OPEN;
      CallExpertLogic(event_type);
      last_time=Time[class="num">0];
     }
  }
class="type">void OnTimer()
  {
   event_type=MARKET_EVENT_TIMER;
   CallExpertLogic(event_type);
  }
class="type">void OnBookEvent(const class="type">class="kw">string &symbol)
  {
   event_type=MARKET_EVENT_TIMER;
   CallExpertLogic(event_type);
  }
class="type">void CallExpertLogic(ENUM_MARKET_EVENT_TYPE &event)
  {
   Expert.InitBuy(event);
   Expert.InitSell(event);
   Expert.SupportBuy(event);
   Expert.SupportSell(event);
  }
把事件收口交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到事件流与持仓基类调用痕迹,你专注决策而不是翻代码。

常见问题

CStrategy 是交易逻辑核心,把事件和模型绑成可继承框架;CPosition 是持仓操作基类,管平仓和修改止盈止损,让交易动作脱离平台细节。
分散在 OnTick、OnBookEvent 等函数里容易漏处理且顺序难控,集中模型由 CStrategy 统一派发,自定义 EA 只写策略本身。
可以,小布盯盘的 AIGC 面板已内置事件流与基类调用视图,不必自己埋日志也能核对 EA 实际触发了哪些事件。
NewTick 与 BookEvent 通常最敏感,前者驱动报价响应,后者反映深度变化;具体触发频率取决于品种流动性和经纪商配置。
能,引擎设计本就为屏蔽细节,跳过难段只记 CStrategy 的函数规则和禁止项即可写自己的 EA。