事件处理函数概述
终端或测试代理通过调用特定函数将控制权转移给 MQL 程序(即执行这些程序),这些函数由 MQL 开发者在应用程序代码中定义,用于处理预定义事件。此类函数必须有一个指定的原型,包括函数名、参数列表(数量、类型和顺序)以及返回类型。
每个函数的名称都与事件的含义相对应,并添加前缀On。例如,OnStart是启动 (Start) 脚本和服务的主函数;当脚本放置在图表上或服务实例启动时,终端会调用该函数。
就本书而言,我们将用相同名称指代一个事件及其相应的处理程序。
下表列出了所有事件类型以及支持这些事件类型的程序( 指标、
EA 交易、
脚本、
服务)。对这些事件的详细说明将在相应程序类型的章节中展开阐述。许多因素会引发初始化和反初始化事件,例如:将程序放置在图表上、更改程序设置、更改图表的交易品种/时间框架(或模板、配置文件)、更改账户以及其他情况(请参阅 各类程序启动和停止的特征一章)。
程序类型 事件/处理程序 |
说明 |
||||
---|---|---|---|---|---|
- |
- |
● |
● |
启动/执行 |
|
+ |
+ |
- |
- |
加载后初始化(请参见 各类程序启动和停止的特征章节中的详细说明) |
|
+ |
+ |
- |
- |
停止和卸载前初始化 |
|
- |
+ |
- |
- |
获取新的价格(分时报价) |
|
● |
- |
- |
- |
由于收到新价格或同步旧价格而触发的指标重新计算请求 |
|
+ |
+ |
- |
- |
以指定频率触发计时器 |
|
- |
+ |
- |
- |
在服务器上完成交易操作 |
|
- |
+ |
- |
- |
更改交易账户状态(订单、交易、头寸) |
|
+ |
+ |
- |
- |
在订单簿中更改 |
|
+ |
+ |
- |
- |
图表上的用户或 MQL 程序操作 |
|
- |
+ |
- |
- |
单次测试周期结束 |
|
- |
+ |
- |
- |
优化前初始化 |
|
- |
+ |
- |
- |
优化后反初始化 |
|
- |
+ |
- |
- |
从测试代理接收优化数据 |
必需的处理程序以符号“●”标识,可选的处理程序以“+”标记。
虽然处理程序函数主要由运行时环境调用,但你也可在从你自己的源代码中调用它们。例如,当 EA 交易需要在启动后立即基于现有报价进行计算,并且即使在没有分时报价(例如在周末)的情况下也需如此,可以在退出 OnInit之前调用OnTick。或者,将计算分离到一个单独的函数中,并从 OnInit和OnTick 中调用它,这也是合乎逻辑的。但需快速执行初始化函数的工作,如果计算耗时较长,应该通过 计时器执行该计算。
所有 MQL 程序(库除外)必须包含至少一个事件处理程序。否则,编译器将生成“未找到事件处理函数”错误。
在没有设置其他类型的 #property 指令的情况下,某些处理程序函数的存在决定了程序的类型。例如,拥有OnCalculate处理程序会导致生成指标(即使它位于另一文件夹中,例如脚本或 EA 交易)。存在 OnStart处理程序(如果没有OnCalculate 处理程序)意味着创建一个脚本。同时,如果指标除有 OnCalculate外,还存在 OnStart,我们会得到一个编译器警告“在非脚本程序中定义了 OnStart 函数”。
本书包含两个文件:AllInOne.mq5和AllInOne.mqh。头文件描述了所有主要事件处理程序几乎为空的模板。除了将处理程序的名称输出到日志以外,它们不包含其他内容。我们将在特定类型的 MQL 程序的章节中考虑使用每个处理程序的语法和详情。此文件的意义在于提供一个实验领域,根据某些处理程序和特性指令 (#property) 的存在情况来编译不同类型的程序。
某些组合可能导致编译错误或警告。
如果编译成功,程序加载后将自动通过以下代码行记录生成的程序类型:
const string type = |
关于枚举 ENUM_PROGRAM_TYPE 和函数 MQLInfoInteger,我们在 程序类型和许可证学习。
AllInOne.mq5文件(包括 AllInOne.mqh)最初存放于 MQL5Book/Scripts/p5/ 目录中,但可复制至任意其他文件夹,包括Navigator 中的相邻分支(例如 EA 交易或指标的文件夹)。在该文件的注释中,保留了用于连接某些程序汇编配置的选项。默认情况下,如果不编辑该文件,就会生成 EA 交易。
//+------------------------------------------------------------------+
|
如果将该程序附加到图表上,我们将在日志中得到一条记录:
EnumToString((ENUM_PROGRAM_TYPE)MQLInfoInteger(MQL_PROGRAM_TYPE))=PROGRAM_EXPERT / ok
|
此外,如果市场处于交易时段,很可能会从 OnTick处理程序生成一系列记录。
如果你用不同的名称复制该 mq5 文件,例如,取消对 #property service指令的注释,编译器将生成服务,但会返回一些警告。
no OnStart function defined in the script
|
其中第一个警告是关于缺少OnStart 函数的,这实际上很重要,因为创建服务实例时,系统不会调用任何函数,仅初始化全局变量。但由于这个原因,日志(终端中的Experts选项卡)仍会显示 PROGRAM_SERVICE 类型。但通常,在服务以及脚本中都会假定存在 OnStart函数。
另外两个警告出现是因为我们的头文件包含了适用于各种情况的处理程序,编译器提醒我们 OnInit和OnDeinit 是没有意义的(终端不会调用它们,甚至不会将它们包含在程序的二进制镜像中)。当然,在实际程序中不应该有这样的警告,即应调用所有处理程序,同时通过预处理指令进行条件编译,从源代码中移除所有冗余内容(无论是物理删除还是逻辑删除)。
如果创建了 AllInOne.mq5 的另一个副本,同时不仅激活了 #property service指令,还激活了#define _OnStart OnStart 宏,则在编译了副本后,你会获得一个完全可用的服务。该副本在启动后,不仅会显示其类型名称,还会显示被触发的处理程序 OnStart的名称。
如果需要启用或禁用标准处理程序OnStart,这个宏是必需的。在 AllInOne.mqh文本中,此函数的描述如下:
void _OnStart() // "extra" underline makes the function customized
|
以下划线开头的名称表明它不是标准处理程序,而只是具有相似原型的用户自定义函数。当我们包含宏时,在编译过程中,编译器会将 _OnStart替换为OnStart,从而将其转换为标准处理程序。如果我们显式命名了 OnStart函数,则根据确定 MQL 程序类型的特征的优先级(请参阅 各类程序的特征章节),你无法得到 EA 交易模板(因为 OnStart将程序标识为脚本或服务)。
同理,需要使用 _OnCalculate1或 _OnCalculate2 宏进行自定义编译,是为了可以选择“隐藏”具有标准名称 OnCalculate 的处理程序:否则,如果该处理程序存在,我们将始终获得指标。
如果你在程序的下一个副本中激活#define _OnCalculate1 OnCalculate宏,则会得到指标示例(即使为空,不执行任何操作)。正如我们稍后将会看到的,对于指标而言,OnCalculate处理程序有两种不同形式,因此以编号名称(_OnCalculate1 和_OnCalculate2)呈现。如果你在图表上运行指标,可以在日志中看到 OnCalculate事件(在分时报价到达时)和 OnChartEvent 事件(例如在鼠标点击时)的名称。
在编译指标时,编译器将生成两条警告:
no indicator window property is defined, indicator_chart_window is applied
|
这是因为指标作为数据可视化工具,其代码中需要一些特定设置(当前示例没有这些设置)。不过现阶段只是初步了解不同类型程序,这一点并不重要。但在后续章节中,我们将讲解如何在指标中描述它们的特性和数组,这些特性和数组决定了图表可视化内容和可视化形式。届时这些警告信息将消失。
当一个新事件发生时,系统必须将其传递给在相应图表上运行的所有 MQL 程序。由于 MQL 程序的单线程执行模型(请参阅 线程章节),可能会出现这样的情况:当前事件尚未处理完毕时,下一个事件就已到达。对于这种情况,终端会为每个交互式 MQL 程序维护一个事件队列。其中的所有事件会按照接收顺序依次进行处理。
事件队列的大小有限制。因此,程序编写不合理可能会因操作缓慢而导致其事件队列溢出。发生溢出时,新事件将被丢弃,不会进入队列排队。
如果不能足够快速地处理事件,可能会对用户体验或数据质量产生负面影响(试想在记录 市场深度 变化时错过了几条消息)。为解决这一问题,可以寻找更高效的算法,或者让多个相互关联的 MQL 程序并行运行(例如,将计算分配给一个指标,而在 EA 交易中仅读取已准备好的数据)。
需要记住的是,终端不会将所有事件都放入队列,而是有选择性地进行操作。某些事件类型的处理遵循“队列中同类型事件不超过一个”原则。例如,如果队列中已经有一个 OnTick事件,或者正在处理该事件,则新的OnTick 事件不会被加入队列。如果队列中已经存在OnTimer事件或图表更改事件,则新的同类型事件也同样会被丢弃(忽略)。这种情况仅针对程序的特定实例。其他不那么“繁忙”的程序仍会接收到这条消息。
我们并未提供此类事件类型的完整列表,因为终端开发者可能会更改这种跳过“重叠”事件的优化方式。
这种根据传入事件来组织程序运行的模式被称为事件驱动。也可以称为异步,因为在程序队列中对事件进行排队和事件提取(及处理)发生在不同的时刻(理想情况下,两者之间的间隔极短,但现实并非总是如此)。然而,在四种 MQL 程序中,只有指标和 EA 交易完全遵循这种模式。脚本和服务实际上只有主函数,调用主函数时,必须快速执行并完成所需操作,或者启动一个无限循环以维持某些活动(例如从网络读取数据),直到用户停止操作。我们已经见过这样的循环示例:
while(!IsStopped())
|
在这种循环中,务必记得以一定周期使用Sleep,以便与其他程序共享 CPU 资源。周期值的选择取决于所实施活动的预期强度。
这种模式可以称为循环式或同步式,甚至可视为实时,因为你可以选择休眠周期来提供恒定的数据处理频率,例如:
int rhythm = 100; // 100 ms, 10 times per sec
|
当然,“有用代码”必须在分配的时间范围内完成执行。
相比之下,在事件模式下,我们无法预知何时执行下一段代码(处理程序)。例如,在快速波动的市场中,在新闻发布期间,分时报价可能会成批到达,而在夜间则可能几秒甚至更长时间都没有变动。极端情况下,周五晚间最后一笔分时报价后,某些金融工具的下一次价格变动可能要到周一早盘才会触发,这意味着OnTick事件可能两天都不会出现。换句话说,事件(以及事件处理程序的启动时刻)既无规律性,也无明确时间表。
但如有必要,可以将这两种模式结合使用。尤其是,计时器事件 (OnTimer) 提供了规律性,开发者可以在循环中定期为图表生成 自定义事件 (例如闪烁警告标签)。