通用智能交易系统事件模型:从集中处理到策略原型的底层铺垫(基础篇)
(1/3)· 多数 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 会在日志打印每次被调方法的名字和事件标识,你就能直观看到哪些事件真正触发了策略——外汇与贵金属波动剧烈,这类集中模型也只是降低逻辑耦合,不消除爆仓风险,参数请先在模拟盘验证。
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,这些是后续把时间过滤和事件判定补进来的占位符,现在留空不影响编译。
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 就能验证事件分发是否符合预期。
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); }