自定义图表事件开发:从事件分类到面向对象骨架的搭建(基础篇)
🧩

自定义图表事件开发:从事件分类到面向对象骨架的搭建(基础篇)

(1/3)· 65535 个自定义事件标识符等着被调度,多数 EA 却只用了典型事件,复杂算法卡在第一步

实战向进阶 第 1/3 篇

把交易逻辑硬塞进 OnTick 是常见误区,图表事件一旦变多,代码就成一团乱麻。自定义事件能把这些逻辑拆成独立模块,但多数开发者连 CHARTEVENT_CUSTOM 到 CHARTEVENT_CUSTOM_LAST 的边界都没理清。本文作为系列开篇,先把事件本体和分类方法讲透。

◍ 在 MT5 里接住鼠标和键盘的自定义事件

MT5 的图表不只是画线和看 K 线,它内置了一套自定义事件通道,能让你在 EA 或指标里直接响应鼠标点击、按键、甚至对象拖拽。很多老手写 MQL4 习惯了只盯 OnTick,到了 MQL5 却没把 OnChartEvent 用起来,等于把终端一半交互能力扔了。 MQL5 用 OnChartEvent 这个函数钩住图表事件,原型是 void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)。id 决定事件类型,比如 CHARTEVENT_MOUSE_CLICK 是鼠标点击,CHARTEVENT_KEY 是键盘按键;lparam、dparam、sparam 则按事件类型携带坐标、键值或对象名。 一个实测有用的细节:在 1920×1080 的图表窗口里,鼠标点击的 X 坐标通过 lparam 的低 16 位取,Y 坐标取高 16 位,这套位运算在外网示例里出现频率很高,但新手常直接拿 lparam 当 X 用,结果点右下角得到负数。外汇和贵金属波动快、滑点多,拿这类交互做手动确认单触发时,务必先在策略测试器外实盘模拟一段时间,高风险品类容错很低。 下面这段是最小可跑的点击事件骨架,复制到 EA 的 OnChartEvent 里就能在终端日志打出点击位置。

MQL5 / C++
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long& lparam, class="kw">const class="type">class="kw">double& dparam, class="kw">const class="type">class="kw">string& sparam)
  {
   if(id==CHARTEVENT_MOUSE_CLICK)
     {
      class="type">int x=(class="type">int)lparam;          class=class="str">"cmt">// 低16位:点击X坐标
      class="type">int y=(class="type">int)(lparam>>class="num">16);    class=class="str">"cmt">// 高16位:点击Y坐标
      Print("clicked at x=",x," y=",y);
     }
  }

自定义事件这套东西从哪来

这套内容在逻辑上接着《MQL5 Cookbook: 处理典型图表事件》那篇走,重点放在自定义图表事件的开发和处理例程上。原文里所有思路都用面向对象的方式封装成了工具,不是零散脚本。 自定义事件这个题材其实摊得很开,尤其是你自己在 EA 或指标里塞进自己的交互逻辑时,能玩出的花样远比标准事件多。 对 MT5 用户来说,直接开编辑器找 OnChartEvent 的自定义事件分支,比读文档更快能验证这套机制。外汇和贵金属品种波动大、滑点随机,用自定义事件做交互控制时务必先上模拟盘跑通。

「自定义事件:程序员自己造的图表信号」

MQL5 里图表事件分两类思路:一类是系统自带的典型事件(文档列了十个,但没有“典型事件”这词),另一类就是开发者自己定义的自定义事件。后者完全由你决定语义和用途,用来把复杂算法拆成事件驱动模块。 自定义事件的标识符空间很大。文档规定总数 65535 个,首尾由 CHARTEVENT_CUSTOM 和 CHARTEVENT_CUSTOM_LAST 写死,数值分别是 1000 和 66534。简单算一下:66534 - 1000 + 1 = 65535,刚好对上。 用之前得先规划分类,不然容易乱。有开发者把自定义事件按源分成三组:指标事件(建/删指标、开平仓信号)、订单仓位状态事件(改单、平仓)、外部事件(比如启停手工交易)。这种分法不消灭模糊,但能压住混乱。 从抽象模型看,基于“源”的事件还能按交易子系统归类:信号子系统、持仓追踪子系统、资金管理子系统。资金管理里可以挂“回撤触限”“加仓量调整”“亏损百分比超限”等事件,外汇和贵金属波动大,这类事件触发后必须人工复核,杠杆风险极高。

◍ 自定义图表事件的收发机制

MT5 里图表事件分两类:系统典型事件和开发者自定义的图表事件。处理自定义事件的逻辑与处理典型事件一致,核心都在 OnChartEvent() 这个函数上。 OnChartEvent() 接收四个常量参数,用来识别事件类型并提取相关上下文信息。这种把事件识别与数据获取绑在同一个回调里的设计,让 EA 能精准响应鼠标、键盘或自定义触发,是比较紧凑的程序机制。 主动制造事件靠 EventChartCustom(),它产生的自定义事件既可以发给图表“自身”所在程序,也能跨图表丢给“外人”用的 EA。多币种面板常利用这点做子图表与主 EA 的通信。 有个细节容易踩坑:生成器 EventChartCustom() 的事件 ID 是 ushort 类型(0~65535),而处理器 OnChartEvent() 里收到的 id 却是 int。按理说两端统一成 ushort 更不容易写错越界,实际却要开发者自己记得收窄范围。外汇与贵金属自动化涉及高频事件,参数类型不一致可能引入隐蔽 bug,建议在回测前先打印 id 边界验证。

把事件抽象成可派生的基类

在 MT5 的 EA 架构里,事件到底是什么,往往由开发者自己定义。要把前面梳理过的那张事件表落地,第一步是建一个基础类 CEventBase,再让具体事件从它派生,形成清晰的类层次。 基础类只抓三件事:事件类型用 ENUM_EVENT_TYPE 枚举区分,数据成员包含事件标识符 m_id 与结构体 m_data。Generate() 负责生成事件,GetId() 返回 id,Validate() 作为私有虚方法做标识符校验,默认返回 true。 我曾想把事件处理方法也塞进类里,但很快发现每个事件都太独特,抽象一层根本不够用,于是放弃把自定义事件处理委托给 CEventProcessor 那套写法。 下面这段是基类与枚举的原文实现,可直接粘进 MT5 编辑器验证:m_type 初始为 EVENT_TYPE_NULL,EVENT_TYPE_INDICATOR 等枚举值从 1 到 3 分别对应指标、订单、外部事件。 别把事件处理写死在基类里 每个衍生事件校验逻辑不同,Validate() 留虚函数比堆通用方法更省事,改参数时只动子类。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CEventBase.                                                      |
class=class="str">"cmt">//| Purpose: base class for a custom event                                |
class=class="str">"cmt">//| Derives from class CObject.                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CEventBase : class="kw">public CObject
  {
class="kw">protected:
   ENUM_EVENT_TYPE    m_type;
   class="type">class="kw">ushort             m_id;
   SEventData         m_data;
class="kw">public:
   class="type">void               CEventBase(class="type">void)
     {
      this.m_id=class="num">0;
      this.m_type=EVENT_TYPE_NULL;
     };
   class="type">void              ~CEventBase(class="type">void){};
   class=class="str">"cmt">//--
   class="type">bool               Generate(class="kw">const class="type">class="kw">ushort _event_id,class="kw">const SEventData &_data,
                               class="kw">const class="type">bool _is_custom=true);
   class="type">class="kw">ushort             GetId(class="type">void) {class="kw">return this.m_id;};
class="kw">private:
   class="kw">virtual class="type">bool       Validate(class="type">void) {class="kw">return true;};
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| A custom event type enumeration                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_EVENT_TYPE
  {
   EVENT_TYPE_NULL=class="num">0,      class=class="str">"cmt">// no event
   class=class="str">"cmt">//---
   EVENT_TYPE_INDICATOR=class="num">1, class=class="str">"cmt">// indicator event
   EVENT_TYPE_ORDER=class="num">2,     class=class="str">"cmt">// order event
   EVENT_TYPE_EXTERNAL=class="num">3,  class=class="str">"cmt">// external event
   };

「把事件流装进一个类里跑」

CEventProcessor 把八个自定义事件的生成与处理都收编到一个类里,数据成员里有两个开关最值得注意:m_is_init 管 EA 能不能正常启动,没过初始化就直接禁止交易;m_is_trade 单独查交易权限,两层拦截能少踩不少坑。 类里挂了 CEventBase 指针走多态,不同事件类型共用一套调用;CTrade 实例负责下单,CiMA 快慢两条均线出信号,CButton 做人工启停。代码层面看,成员声明就是这套骨架: [CODE] class CEventProcessor { protected: ulong m_magic; bool m_is_init; bool m_is_trade; CEventBase *m_ptr_event; CTrade m_trade; CiMA m_fast_ema; CiMA m_slow_ema; CButton m_button; bool m_button_state; }; [/CODE] 方法按「模块–过程–函数」切:Start() 在 OnInit 里建事件指针、发「指针创建」事件、把按钮置为停止态;Finish() 在 OnDeinit 里清零指针并发「指标缺失」事件,但要注意自定义事件在 OnDeinit 内生成会报 4001 运行时错,所以这块处理不绕 OnChartEvent()。 Main() 丢进 OnTick,新柱线才跑 Open()/Close() 发开平信号;ProcessEvent() 接 OnChartEvent,前半处理按钮点击发外部事件,后半把「开仓信号」转成「开仓」事件、「平仓信号」转成「平仓」事件,链式往下走。 附件里的 CustomEventProcessor.mq5 是直接能编译的例程,用 OOP 把源码行数压到很低。外汇和贵金属杠杆高、滑点跳空频繁,这种事件链一旦在 OnDeinit 乱发自定义事件就可能崩 EA,实盘前先在策略测试器跑几遍看日志里的 4001 有没有冒头。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CEventProcessor.                                         |
class=class="str">"cmt">//| Purpose: base class for an event processor EA                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CEventProcessor
  {
class=class="str">"cmt">//+----------------------------Data members--------------------------+
class="kw">protected:
   class="type">ulong               m_magic;
   class=class="str">"cmt">//--- flags
   class="type">bool                m_is_init;
   class="type">bool                m_is_trade;
   class=class="str">"cmt">//---
   CEventBase         *m_ptr_event;
   class=class="str">"cmt">//---
   CTrade              m_trade;
   class=class="str">"cmt">//---
   CiMA                m_fast_ema;
   CiMA                m_slow_ema;
   class=class="str">"cmt">//---
   CButton             m_button;
   class="type">bool                m_button_state;
class=class="str">"cmt">//+------------------------------------------------------------------+
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CEventProcessor.                                         |
class=class="str">"cmt">//| Purpose: base class for an event processor EA                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CEventProcessor
  {
class=class="str">"cmt">//+-------------------------------Methods----------------------------+
class="kw">public:
   class=class="str">"cmt">//--- constructor/destructor
   class="type">void                CEventProcessor(class="kw">const class="type">ulong _magic);
   class="type">void               ~CEventProcessor(class="type">void);
   class=class="str">"cmt">//--- Modules
   class=class="str">"cmt">//--- event generating
   class="type">bool                Start(class="type">void);
   class="type">void                Finish(class="type">void);
   class="type">void                Main(class="type">void);
   class=class="str">"cmt">//--- event processing
交给小布盯盘看事件流
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到自定义事件触发频次与来源分布,你只需判断哪些该进策略。

常见问题

从 CHARTEVENT_CUSTOM(1000)到 CHARTEVENT_CUSTOM_LAST(66534),共 65535 个,需先设定再使用。
分组能降低概念模糊度,把指标、订单、外部事件分开,后续在类里建事件类型时推理路线更清晰。
可以,小布盯盘对应品种页内置了事件流视图,AIGC 会归并展示自定义事件来源与次数,省去你打日志。
典型事件是前十种内建类型,自定义事件是其外的扩展,两者都归入 ENUM_CHART_EVENT 枚举统一管理。