Appearance
MQL 程序的执行一般原则
根据功能和特点的不同,所有 MQL 程序大致可以分为几类。
大多数程序,比如智能交易系统(EA)、指标和脚本,都是在图表环境中运行的。也就是说,只有在通过导航器树中的“附加到图表”上下文菜单命令,或者从导航器中拖放到图表上,将它们附加到一个打开的图表上之后,这些程序才会开始执行。
相比之下,服务程序不能放置在图表上,因为它们是为在后台执行长时间的循环操作而设计的。例如,在一个服务程序中,你可以创建一个自定义交易品种,然后获取其数据,并使用网络函数在一个无限循环中不断更新数据。服务程序的另一个合理应用场景是监控交易账户和网络连接,作为向用户通知通信问题的解决方案的一部分。
需要注意的是,指标和智能交易系统在终端的不同工作会话之间会保存在图表上。换句话说,例如,如果用户在图表上运行了一个指标,然后在没有明确删除它的情况下关闭了 MetaTrader 5,那么下次终端启动时,该指标会连同图表一起恢复,包括其所有设置。
顺便说一下,将指标和智能交易系统链接到图表是模板(详见文档)的基础。用户可以创建一组要在图表上使用的程序,对它们进行配置,然后将这组程序保存为一个扩展名为 tpl 的特殊文件。这可以通过上下文菜单命令“模板 -> 保存”来完成。之后,你可以将该模板应用到任何新图表上(命令“模板 -> 加载”),并运行所有链接的程序。模板默认存储在 MQL5/Profiles/Templates/ 目录中。
附加到图表上的另一个结果是,关闭一个图表会导致卸载放置在该图表上的所有 MQL 程序。不过,MetaTrader 5 会以特定的方式保存所有关闭的图表(至少保存一段时间),因此,如果图表是意外关闭的,可以使用“文件 -> 打开远程图表”命令将其连同所有程序(以及图形对象)一起恢复。
如果由于某种原因终端无法加载图表文件,MQL 程序的整个状态(设置和位置)将会丢失。基本上,这同样适用于图形对象——程序可以根据自身需要添加图形对象,并期望这些对象位于图表上。请对图表进行备份。每个图表都是一个扩展名为 chr 的文件。此类文件默认存储在 MQL5/Profiles/Charts/Default/ 目录中。这是安装平台时创建的标准配置文件。你可以使用菜单命令“文件 -> 配置文件”创建其他配置文件,然后在它们之间进行切换(详见文档)。
如果有必要,你可以使用上下文菜单命令“智能交易系统列表”(在图表窗口中单击鼠标右键调用)来停止一个智能交易系统,并将其从图表上移除。该命令会打开“智能交易系统”对话框,其中列出了终端中正在运行的所有智能交易系统。在这个列表中,选择一个你不再需要的智能交易系统,然后按下“移除”按钮。
指标也可以通过类似的上下文菜单命令“指标列表”来明确移除。该命令会打开一个对话框,其中列出了当前图表上正在运行的指标,你可以在其中选择一个特定的指标,然后点击“移除”按钮。此外,大多数指标会在图表上显示各种图形结构,比如线条和柱状图,这些图形结构也可以使用相关的上下文菜单命令删除。
与指标和智能交易系统不同,脚本不会永久附加到图表上。在标准模式下,如果脚本执行的是一次性操作,那么在分配给它的任务完成后,脚本会自动从图表上移除。如果一个脚本有一个用于周期性重复操作的循环,那么它当然会继续运行,直到以某种方式中断循环,但不会超过会话结束的时间。关闭终端会导致脚本与图表分离。重新启动 MetaTrader 5 后,脚本不会在图表上恢复。
请注意,如果你将图表切换到另一个交易品种或时间框架,在该图表上运行的脚本将会被卸载。但是指标和智能交易系统会继续运行,不过,它们会被重新初始化。它们的初始化规则是不同的。这些细节将在“各种类型程序的启动和停止特点”部分进行讨论。
在图表上只能放置一个智能交易系统、一个脚本,但可以放置任意数量的指标。智能交易系统、脚本和所有指标将并行(同时)运行。
至于服务程序,它们已创建并正在运行的实例在终端加载后会自动恢复。可以使用导航器窗口中“服务程序”部分的上下文菜单来停止或删除服务程序实例。
下表以总结的形式概述了上述属性:
程序类型 | 与图表的链接 | 图表上的数量 | 会话恢复情况 |
---|---|---|---|
指标 | 必需 | 多个 | 随图表或模板恢复 |
智能交易系统 | 必需 | 最多 1 个 | 随图表或模板恢复 |
脚本 | 必需 | 最多 1 个 | 不支持 |
服务程序 | 不支持 | 0 个 | 随终端恢复 |
所有 MQL 程序都在客户端终端中执行,因此只有在终端打开时才能运行。要想持续通过程序监控账户,请使用虚拟专用服务器(VPS)。
各类 MQL 程序的设计
程序类型是 MQL5 中的一个基本属性。与 C++ 或其他通用编程语言不同,在那些语言中任何程序都可以朝着任意方向开发,比如添加图形界面或通过网络从服务器上传数据等,而 MQL 程序则根据其用途被划分为特定的几类。例如,带有可视化功能的技术时间序列分析是通过指标来实现的,但指标无法进行交易。反过来,交易 API 函数可供智能交易系统(EA)使用,但智能交易系统没有指标缓冲区(用于绘制线条的数组)。
因此,在解决特定的应用问题时,开发者应该将问题分解成多个部分,并且每个部分的功能应与某一特定类型程序的专长相契合。当然,在简单的情况下,一个单一的 MQL 程序就足够了,但有时最优的技术解决方案并不明显。例如,你要如何实现砖形图(Renko chart)的绘制呢:是作为一个指标来实现,还是作为由服务程序生成的自定义交易品种,又或者是直接在交易智能交易系统中进行特定的计算呢?所有这些选择都是有可能的。
MQL 程序的类型由几个因素来表征。
首先,每种类型的程序在 MQL5 工作目录中都有一个单独的文件夹。我们在第 1 部分的引言中已经提到过这一事实,并列出了这些文件夹。所以,对于指标、智能交易系统、脚本和服务程序,指定的文件夹分别是 Indicators、Experts、Scripts 和 Services。MQL5 文件夹中的 Libraries 子文件夹是为库文件预留的。在每个文件夹中,你都可以组织任意配置的嵌套文件夹树。
二进制文件(扩展名为 ex5 的成品程序)—— 这是编译 mq5 文件的结果 —— 与源 mq5 文件在同一目录中生成。不过,我们还应该提到 MetaEditor 中的项目(扩展名为 mqproj 的文件),我们将在“项目”一章中对其进行分析。当开发一个项目时,成品会在项目旁边的一个目录中创建。当在 MetaEditor 中使用 MQL5 向导创建一个程序时(命令“文件 -> 新建”),源文件默认会放置在与程序类型相对应的文件夹中。如果你不小心将一个程序复制到了错误的目录中,也不会有什么严重的后果:它不会,比如说,从一个智能交易系统变成一个指标,反之亦然。你可以直接在编辑器中、在导航器窗口内或者在外部文件管理器中将其移动到期望的位置。在导航器中,每种程序类型都用一个特殊的图标显示。
一个程序在 MQL5 目录中特定类型子文件夹中的位置,并不能决定这个特定 MQL 程序的类型。程序类型是根据可执行文件的内容来确定的,而可执行文件反过来又是由编译器根据源代码中的属性指令和语句形成的。
按程序类型划分的文件夹层次结构是为了方便使用。建议遵循这一结构,除非是一组相关的项目(包含不同类型的程序),对于这类项目,将它们存储在一个单独的目录中会更合理。
其次,每种类型的程序的特点是支持一组有限的、特定的系统事件,这些事件会激活程序。我们将在单独的部分中看到事件处理函数的概述。要在一个程序中接收特定类型的事件,有必要描述一个具有预定义原型(名称、参数列表、返回值)的处理函数。
例如,我们已经看到在脚本和服务程序中,工作是在 OnStart 函数中开始的,并且由于它是那里唯一的函数,所以它可以被称为主要的“入口点”,终端通过这个入口点将控制权转移到应用程序代码中。在其他类型的程序中,情况会稍微复杂一些。一般来说,我们注意到一种程序类型由一组特定的处理函数来表征,其中一些可能是必需的,而一些是可选的(但同时,对于其他类型的程序来说是不可接受的)。特别是,一个指标需要 OnCalculate 函数(没有它,指标将无法编译,编译器会生成一个错误)。然而,这个函数在智能交易系统中并不使用。
第三,一些类型的程序需要特殊的 #property 指令。在“程序的一般属性”一章中,我们已经看到了可以在所有类型程序中使用的指令。然而,还有其他一些专门的指令。例如,在我们提到的服务程序任务中,我们遇到了 #property service 指令,它使程序成为一个服务程序。没有它,即使将程序放在 Services 文件夹中,它也无法在后台运行。
同样,#property library 指令在创建库文件时起着决定性的作用。所有这些指令属性将在相应类型程序的部分中进行讨论。
在按以下顺序(从上到下,直到找到第一个匹配项)确定 MQL 程序类型时,会考虑指令和事件处理函数的组合:
- 指标:存在 OnCalculate 处理函数
- 库文件:#property library 指令
- 脚本:存在 OnStart 处理函数且不存在 #property service 指令
- 服务程序:存在 OnStart 处理函数且有 #property service 指令
- 智能交易系统:存在任何其他处理函数
这些属性对编译器有什么影响的示例将在“事件处理函数概述”部分给出。
对于上述所有要点,还应该考虑另一点。程序类型是由主要的编译模块决定的:一个扩展名为 mq5 的文件,在这个文件中可以使用 #include 指令包含来自其他目录的其他源文件。以这种方式包含的所有函数与直接存在于主 mq5 文件中的函数处于同一级别进行考虑。
另一方面,#property 指令只有放在主编译 mq5 文件中时才会起作用。如果这些指令出现在使用 #include 包含在程序中的文件中,它们将被忽略。
主 mq5 文件实际上并不一定要包含事件处理函数。将部分或全部算法放在 mqh 头文件中,然后将它们包含在一个或多个程序中是完全可以接受的。例如,我们可以在一个 mqh 文件中实现带有一组有用操作的 OnStart 处理函数,并通过 #include 在两个单独的程序(一个脚本和一个服务程序)中使用它。
同时,我们要注意,将通用的算法片段分离到头文件中的动机并不只是因为存在通用的事件处理函数。例如,你可以在一个指标和一个智能交易系统中使用相同的计算公式,而将它们的事件处理函数留在主程序模块中。
虽然习惯上把包含文件称为头文件并给它们 mqh 扩展名,但从技术上讲这并不是必需的。在一个 mq5 文件中包含另一个 mq5 文件或者,比如说,一个 txt 文件也是完全可以接受的(尽管不推荐)。它们可能包含一些旧代码,或者可以说,用常量对某些数组进行初始化。包含另一个 mq5 文件并不会使它成为主文件。
你应该确保只有特定程序类型所特有的事件处理函数进入程序,并且这些函数之间没有重复(如你所知,函数是通过名称和参数列表的组合来识别的:只有当参数集不同时才允许函数重载)。这通常是通过使用各种预处理器指令来实现的。例如,在我们的某个程序中包含一个第三方 mq5 脚本文件之前,定义宏 #define OnStart OnStartPrevious,我们实际上会将其中描述的 OnStart 函数变成 OnStartPrevious,并且我们可以从自己的事件处理函数中像往常一样调用它。
然而,只有在由于某些原因无法修改所包含的 mq5 文件的源代码的特殊情况下,这种方法才有意义,特别是当无法通过将感兴趣的算法选择到单独头文件中的函数或类中来对其进行结构化时。
根据与用户的交互原则,MQL 程序可以分为交互式和实用型两类。
交互式程序 —— 指标和智能交易系统 —— 可以处理在软件环境中因用户操作而产生的事件,比如按下键盘上的按钮、移动鼠标、改变窗口大小,以及许多其他事件,例如与接收报价数据或定时器操作相关的事件。
实用型程序 —— 服务程序和脚本 —— 只根据在启动时设置的输入变量运行,并且不对系统中的事件做出响应。
除了所有这些类型的程序之外,还有库文件。它们总是作为另一种类型的 MQL 程序(四种主要类型之一)的一部分来执行,因此没有任何独特的特征或行为。特别是,它们不能直接从终端接收事件,也没有自己的线程(见下一部分)。同一个库文件可以连接到许多程序,并且这种连接是在每个父程序启动时动态发生的。在关于库文件的部分中,我们将学习如何描述库文件的导出 API 并将其导入到父程序中。
线程
简单来说,一个程序可以被看作是开发者为计算机编写的一系列语句。计算机中执行语句的主要部件是中央处理器。现代计算机通常配备有多核处理器,这相当于拥有多个处理器。然而,用户可能希望并行运行的程序数量实际上是没有限制的。因此,程序的数量总是比可用的核心/处理器数量多很多倍。正因为如此,每个核心实际上会在几个不同的程序之间分配其工作时间:它会分配1毫秒来执行一个程序的语句,然后再分配1毫秒给另一个程序的语句,接着是第三个程序,依此类推,循环往复。由于这种切换发生得非常快,用户并不会察觉到,感觉所有程序似乎都是并行且同时执行的。
为了使处理器能够暂停一个程序语句的执行,然后从之前的位置继续执行(在它悄然切换去执行其他“并行”程序的语句之后),它必须能够以某种方式保存和恢复每个程序的中间状态:当前的语句、变量,可能还有打开的文件、网络连接等等。程序正常运行所需的所有这些资源和数据的集合,以及它在语句序列中的当前位置,被称为程序的执行上下文。实际上,操作系统的设计目的就是根据用户(或其他程序)的请求为每个程序创建这样的上下文。每个这样的活动上下文被称为一个线程。许多程序自身需要多个线程,因为它们的功能涉及并行维持多项活动。MetaTrader 5 也需要多个线程来加载多个交易品种的报价数据、绘制图表以及响应用户操作。此外,MQL 程序也会被分配单独的线程。
MQL 程序执行环境为每个程序分配的线程不超过一个。智能交易系统(EA)、脚本和服务程序每个都严格分配一个线程。至于指标,对于在一种金融工具上运行的所有指标会分配一个线程。而且,同一个线程负责显示相应交易品种的图表,所以不建议用繁重的计算占用这个线程。否则,用户界面将变得无响应:用户的操作会被延迟处理,甚至窗口会变得毫无反应。所有其他类型的 MQL 程序的线程并不与界面绑定,因此可以让处理器承担任何复杂的任务。
线程的一个重要属性源于它的定义和用途:它只支持按顺序一个接一个地执行指定的语句。在一个线程中,同一时间只能执行一条语句。如果在程序中编写了一个无限循环,线程就会卡在这条指令上,永远无法执行其下面的指令。长时间的计算也会产生类似无限循环的效果:它们会占用处理器资源,阻止执行用户可能期望得到结果的其他操作。这就是为什么指标中的高效计算对于图形界面的顺畅运行很重要。
然而,在其他类型的 MQL 程序中,应该注意线程的安排。在接下来的部分中,我们将熟悉作为 MQL 程序入口点的特殊事件处理函数。单线程模型意味着在处理一个事件的过程中,程序对可能同时发生的其他事件是无感知的。因此,终端会为每个程序组织一个事件队列。我们将在下一部分更详细地讨论这一点。
为了在实践中体验单线程的影响,我们将在“指标的局限性和优势”部分(IndBarIndex.mq5)看一个简单的例子。我们选择指标来举例是因为它们不仅每个交易品种共享一个线程,而且还直接在图表上显示结果,这使得潜在的问题最为明显。
事件处理函数概述
将控制权转移给 MQL 程序(即程序的执行)是通过终端或测试代理调用特殊函数来实现的,MQL 开发者在其应用代码中定义这些特殊函数以处理预定义的事件。这类函数必须具有指定的原型,包括函数名、参数列表(参数的数量、类型和顺序)以及返回类型。
每个函数的名称都与事件的含义相对应,并加上前缀“On”。例如,OnStart 是脚本和服务程序“启动”的主函数;当脚本被放置到图表上或服务程序实例启动时,终端会调用该函数。
在本书中,我们将用相同的名称来指代一个事件及其相应的处理函数。
下表列出了所有的事件类型以及支持这些事件的程序(indicator_small 表示指标,expert_small 表示智能交易系统,script_small 表示脚本,services_small 表示服务程序)。关于这些事件的详细描述在相应程序类型的部分给出。许多因素都可能导致初始化和反初始化事件的发生:将程序放置到图表上、更改其设置、更改图表的交易品种/时间框架(或模板、配置文件)、更改账户等等(详见“各类程序的启动和停止特点”一章)。
程序类型 | 事件/处理函数 | 指标 | 智能交易系统 | 脚本 | 服务程序 | 描述 |
---|---|---|---|---|---|---|
OnStart | - | - | ● | ● | 启动/执行 | |
OnInit | + | + | - | - | 加载后的初始化(详见“各类程序的启动和停止特点”部分) | |
OnDeinit | + | + | - | - | 停止和卸载前的反初始化 | |
OnTick | - | + | - | - | 获得新的价格(报价) | |
OnCalculate | ● | - | - | - | 由于收到新价格或同步旧价格而请求重新计算指标 | |
OnTimer | + | + | - | - | 以指定频率激活定时器 | |
OnTrade | - | + | - | - | 服务器上交易操作完成 | |
OnTradeTransaction | - | + | - | - | 交易账户状态(订单、交易、头寸)改变 | |
OnBookEvent | + | + | - | - | 订单簿变化 | |
OnChartEvent | + | + | - | - | 用户或 MQL 程序在图表上的操作 | |
OnTester | - | + | - | - | 单次测试通过结束 | |
OnTesterInit | - | + | - | - | 优化前的初始化 | |
OnTesterDeinit | - | + | - | - | 优化后的反初始化 | |
OnTesterPass | - | + | - | - | 从测试代理接收优化数据 |
必需的处理函数用符号“●”标记,可选的处理函数用符号“+”标记。
虽然处理函数主要是为了由运行时调用,但你也可以从自己的源代码中调用它们。例如,如果一个智能交易系统在启动后需要立即根据可用的报价进行一些计算,甚至在没有报价数据(比如在周末)的情况下也需要计算,你可以在离开 OnInit 函数之前调用 OnTick 函数。或者,将计算分离到一个单独的函数中,并从 OnInit 和 OnTick 函数中都调用它,这也是合理的做法。然而,最好快速完成初始化函数的工作,如果计算过程较长,应该在定时器上执行。
所有 MQL 程序(库文件除外)必须至少有一个事件处理函数。否则,编译器会生成“未找到事件处理函数”的错误。
在没有设置其他类型的 #property 指令的情况下,某些处理函数的存在决定了程序的类型。例如,有 OnCalculate 处理函数会生成一个指标(即使它位于其他文件夹中,比如脚本或智能交易系统文件夹)。有 OnStart 处理函数(如果没有 OnCalculate 处理函数)意味着创建一个脚本。同时,如果一个指标除了 OnCalculate 处理函数之外还有 OnStart 处理函数,我们会得到编译器警告“在非脚本程序中定义了 OnStart 函数”。
本书包含两个文件:AllInOne.mq5 和 AllInOne.mqh。头文件描述了几乎为空的所有主要事件处理函数的模板。它们除了将处理函数的名称输出到日志中外,不包含其他内容。我们将在关于特定类型 MQL 程序的部分中考虑每个处理函数的语法和使用细节。这个文件的意义在于提供一个实验平台,根据某些处理函数和属性指令(#property)的存在来编译不同类型的程序。
某些组合可能会导致错误或警告。
如果编译成功,那么在加载生成的程序后,会自动记录其程序类型,使用以下代码行:
c++
const string type =
PRTF(EnumToString((ENUM_PROGRAM_TYPE)MQLInfoInteger(MQL_PROGRAM_TYPE)));
我们在“程序类型和许可证”部分学习了枚举类型 ENUM_PROGRAM_TYPE 和函数 MQLInfoInteger。
包含 AllInOne.mqh 的文件 AllInOne.mq5 最初位于目录 MQL5Book/Scripts/p5/ 中,但它可以被复制到任何其他文件夹,包括导航器的相邻分支(例如,复制到智能交易系统或指标的文件夹中)。在文件内部的注释中,留下了连接某些程序组装配置的选项。默认情况下,如果不编辑该文件,你将得到一个智能交易系统。
c++
//+------------------------------------------------------------------+
//| 取消注释以下行以得到服务程序
//| 注意:还需激活 #define _OnStart OnStart
//+------------------------------------------------------------------+
//#property service
//+------------------------------------------------------------------+
//| 取消注释以下行以得到库文件
//+------------------------------------------------------------------+
//#property library
//+------------------------------------------------------------------+
//| 取消注释以下行以得到脚本或
//| 服务程序(必须启用 #property service)
//+------------------------------------------------------------------+
//#define _OnStart OnStart
//+------------------------------------------------------------------+
//| 取消注释以下两行之一以得到指标
//+------------------------------------------------------------------+
//#define _OnCalculate1 OnCalculate
//#define _OnCalculate2 OnCalculate
#include <MQL5Book/AllInOne.mqh>
如果我们将程序附加到图表上,我们将在日志中得到一条记录:
c++
EnumToString((ENUM_PROGRAM_TYPE)MQLInfoInteger(MQL_PROGRAM_TYPE))=PROGRAM_EXPERT / ok
OnInit
OnChartEvent
OnTick
OnTick
OnTick
...
此外,如果市场开盘,很可能会从 OnTick 处理函数生成一系列记录。
如果你用不同的名称复制 mq5 文件,例如,取消注释指令 #property service,编译器将生成服务程序,但会返回一些警告。
no OnStart function defined in the script
OnInit function is useless for scripts
OnDeinit function is useless for scripts
其中第一条关于缺少 OnStart 函数的警告实际上很重要,因为当创建一个服务程序实例时,其中不会调用任何函数,而只会初始化全局变量。然而,由于这个原因,日志(终端中的“智能交易系统”选项卡)仍然会打印出 PROGRAM_SERVICE 类型。但通常情况下,在服务程序以及脚本中,都假定存在 OnStart 函数。
另外两条警告是因为我们的头文件包含了所有情况下的处理函数,编译器提醒我们 OnInit 和 OnDeinit 是无意义的(终端不会调用它们,甚至不会将它们包含在程序的二进制镜像中)。当然,在实际程序中不应该出现这样的警告,也就是说,所有的处理函数都应该被用到,并且应该从源代码中物理或逻辑地删除所有多余的内容,使用预处理器指令进行条件编译。
如果你再创建一个 AllInOne.mq5 的副本,不仅激活 #property service 指令,还激活 #define _OnStart OnStart 宏,编译后你将得到一个完全可用的服务程序。启动时,它不仅会显示其类型名称,还会显示触发的处理函数 OnStart 的名称。
需要这个宏是为了能够根据需要启用/禁用标准处理函数 OnStart。在 AllInOne.mqh 文本中,这个函数描述如下:
c++
void _OnStart() // “额外”的下划线使该函数为自定义函数
{
Print(__FUNCTION__);
}
以下划线开头的名称使其不是标准的处理函数,而只是一个具有类似原型的用户定义函数。当我们包含一个宏时,在编译期间编译器会将 _OnStart 替换为 OnStart,结果就变成了一个标准的处理函数。如果我们直接命名为 OnStart 函数,那么根据确定 MQL 程序类型的特征优先级(详见“各类 MQL 程序的特点”部分),就无法得到一个智能交易系统模板(因为 OnStart 会将程序识别为脚本或服务程序)。
类似地,使用宏 _OnCalculate1 或 _OnCalculate2 进行自定义编译,是为了可以选择“隐藏”具有标准名称 OnCalculate 的处理函数:否则,如果它存在,我们总是会得到一个指标。
如果在程序的下一个副本中激活宏 #define _OnCalculate1 OnCalculate,你将得到一个指标示例(尽管它是空的,什么也不做)。正如我们稍后将看到的,对于指标来说,OnCalculate 处理函数有两种不同的形式,因此它们以编号名称(_OnCalculate1 和 _OnCalculate2)呈现。如果你在图表上运行这个指标,你可以在日志中看到 OnCalculate 事件(在报价数据到达时)和 OnChartEvent 事件(例如,鼠标点击时)的名称。
编译指标时,编译器会生成两条警告:
c++
no indicator window property is defined, indicator_chart_window is applied
no indicator plot defined for indicator
这是因为指标作为数据可视化工具,在其代码中需要一些特定的设置,而这里没有这些设置。在对不同类型程序的初步了解阶段,这并不重要。但在接下来的内容中,我们将学习如何在指标中描述它们的属性和数组,这些属性和数组决定了在图表上应该可视化什么以及如何可视化。然后这些警告就会消失。
事件队列
当一个新事件发生时,它必须被传送到在相应图表上运行的所有 MQL 程序。由于 MQL 程序的单线程执行模型(详见“线程”部分),可能会出现当下一个事件到达时,前一个事件仍在处理中的情况。对于这种情况,终端会为每个交互式 MQL 程序维护一个事件队列。其中的所有事件会按照接收顺序依次处理。
事件队列的大小是有限的。因此,编写不合理的程序可能会由于操作缓慢而导致其队列溢出。队列溢出时,新事件将被丢弃,不会进入队列。
不能足够快地处理事件会对用户体验或数据质量产生负面影响(想象一下你记录市场深度变化却跳过了几条消息)。为了解决这个问题,你可以寻找更高效的算法,或者使用几个相互连接的 MQL 程序并行运行(例如,将计算任务分配给一个指标,而智能交易系统只读取现成的数据)。
应该记住,终端不会将所有事件都放入队列,而是有选择地进行操作。某些类型的事件是根据“队列中同类型事件不超过一个”的原则进行处理的。例如,如果队列中已经有 OnTick 事件,或者正在处理 OnTick 事件,那么新的 OnTick 事件不会进入队列。如果队列中已经有 OnTimer 事件或图表更改事件,那么这些类型的新事件也会被丢弃(忽略)。这里说的是特定的程序实例。其他不那么“繁忙”的程序将接收这条消息。
我们不提供这类事件类型的完整列表,因为通过跳过“重叠”事件进行的这种优化可能会被终端开发者更改。
组织程序响应传入事件的工作方式被称为事件驱动。它也可以被称为异步的,因为事件在程序队列中的入队和出队(以及处理)发生在不同的时刻(理想情况下,间隔极小,但理想情况并不总是能够实现)。然而,在四种类型的 MQL 程序中,只有指标和智能交易系统完全遵循这种方式。脚本和服务程序实际上只有主函数,当调用主函数时,它必须要么快速执行所需的操作并完成,要么启动一个无限循环以维持某些活动(例如,从网络读取数据),直到用户停止。我们已经看到了这样的循环示例:
c++
while(!IsStopped())
{
useful code
...
Sleep(...);
}
在这样的循环中,重要的是不要忘记以一定的周期使用 Sleep 函数,以便与其他程序共享 CPU 资源。周期值是根据所实现活动的估计强度来选择的。
这种方法可以称为循环的或同步的,甚至可以称为实时的,因为你可以选择睡眠周期来提供恒定的数据处理频率,例如:
c++
int rhythm = 100; // 100 毫秒,每秒 10 次
while(!IsStopped())
{
const int start = (int)GetTickCount();
useful code
...
Sleep(rhythm - ((int)GetTickCount() - start));
}
当然,“有用的代码”必须在规定的时间内完成。
相比之下,使用事件驱动的方法时,无法提前知道下一次代码片段(处理函数)何时会运行。例如,在快速变化的市场中,在新闻发布期间,报价数据可能会成批到来,而在夜间可能会有整整几秒都没有报价数据。在极端情况下,周五晚上最后一次报价数据之后,某些金融工具的下一次价格变化可能要到周一早上才会广播,因此 OnTick 事件会有两天都不会出现。换句话说,在事件(以及事件处理函数的激活时刻)中没有规律性,没有明确的时间表。
但如果有必要,你可以将两种方式结合起来。特别是,定时器事件(OnTimer)提供了规律性,开发者可以在循环中定期为图表生成自定义事件(例如,闪烁警告标签)。
各类程序的启动和停止特点
在编程中,“初始化”这个术语在许多不同的上下文中被使用。在 MQL5 中,它也存在一些模糊性。在“初始化”部分,我们已经用这个词来表示设置变量的初始值。然后我们讨论了指标和智能交易系统中的初始化事件 OnInit。尽管这两种初始化的含义相似(使程序进入工作状态),但它们实际上意味着为启动 MQL 程序做准备的不同阶段:系统层面和应用层面。
一个已完成的 MQL 程序的生命周期可以由以下主要步骤来表示:
- 加载:将程序从文件读取到终端的内存中,这包括指令、预定义的数据(字面量)、资源和库文件。这就是 #property 指令发挥作用的地方。
- 为全局变量分配内存并设置其初始值:这是由运行时执行的系统初始化。回想一下,在“初始化”部分,当我们逐步在调试器下研究程序的启动时,我们看到栈上有 @global_initializations 条目。这就是这个步骤的代码块,它是由编译器隐式创建的。如果程序使用类/结构的全局对象,它们的构造函数将在这个阶段被调用。
- 调用 OnInit 事件处理函数(如果存在):它执行更高级别的应用初始化,因此每个程序根据需要独立执行此操作。例如,它可以为对象数组进行动态内存分配,由于某种原因,对于这些对象数组,你需要使用参数化构造函数而不是默认构造函数。如我们所知,为数组自动分配内存时只使用默认构造函数,因此它们不能在前一步(步骤 2)中进行初始化。它也可以是打开文件、调用内置的 API 函数以启用必要的图表模式等等。
- 一个循环,直到用户关闭程序或终端,或者执行任何其他需要重新初始化的操作(见下文):当相应的事件发生时,调用其他处理函数。
- 检测到用户或通过编程方式尝试关闭程序时,调用 OnDeinit 事件处理函数(如果存在)(相应的函数 ExpertRemove 仅在智能交易系统和脚本中可用)。
- 最终处理:释放已分配的内存以及程序员在 OnDeinit 中认为无需释放的其他资源。如果程序使用面向对象编程(OOP),这里将调用全局和静态对象的析构函数。
- 卸载程序。
脚本和服务程序按其本质没有 OnInit 和 OnDeinit 处理函数,因此对于它们来说不存在步骤 3 和 5,并且步骤 4 退化为单个 OnStart 函数的调用。
系统初始化(步骤 2)与加载是不可分割的,也就是说,它总是在加载之后进行。最终处理总是在卸载之前进行。然而,指标和智能交易系统在不同情况下的加载和卸载阶段有所不同。因此,OnInit 和 OnDeinit 的调用(步骤 3 和 5)是参考点,通过这些点可以为智能交易系统和指标提供一致的应用初始化和反初始化。
指标和智能交易系统在以下情况下进行加载:
情况 | 指标 | 智能交易系统 |
---|---|---|
用户在图表上启动程序 | + | + |
启动终端(如果程序在上次关闭终端之前在图表上运行) | + | + |
加载模板(如果模板包含附加到图表的程序) | + | + |
更改配置文件(如果程序附加到配置文件中的一个图表) | + | + |
成功重新编译后,如果程序附加到图表 | + | + |
更改活动账户 | + | + |
更改指标所附加图表的交易品种或周期 | + | - |
更改指标的输入参数 | + | - |
连接到账户(授权),即使账户号码未更改 | - | + |
可以用更简洁的形式表述以下规则:智能交易系统不会经历完整的生命周期,也就是说,当图表的交易品种/时间框架发生变化以及输入参数改变时,它们不会重新加载。
因此,在卸载程序时也可以观察到类似的不对称性。卸载指标和智能交易系统的原因如下:
情况 | 指标 | 智能交易系统 |
---|---|---|
从图表中移除程序 | + | + |
关闭终端(当程序附加到图表时) | + | + |
在程序正在运行的图表上加载模板 | + | + |
关闭程序正在运行的图表 | + | + |
更改配置文件,如果程序附加到配置文件的一个图表 | + | + |
更改终端连接的账户 | + | + |
更改指标所附加图表的交易品种和/或周期 | + | - |
更改指标的输入参数 | + | - |
将另一个或同一个智能交易系统附加到当前已有智能交易系统正在运行的图表上 | - | + |
调用 ExpertRemove 函数 | - | + |
可以使用函数 UninitializeReason 或标志 _UninitReason 在程序中找到反初始化的原因(详见“检查 MQL 程序停止的状态和原因”部分)。
请注意,当你更改图表的交易品种或时间框架,以及更改输入参数时,智能交易系统会保留在内存中,也就是说,不会执行步骤 6 - 7(最终处理和卸载)以及步骤 1 - 2(加载和初始内存分配),因此全局和静态变量的值不会被重置。在这种情况下,OnDeinit 和 OnInit 处理函数会分别在旧的和新的交易品种/时间框架(或在旧的和新的设置)上依次调用。
智能交易系统中全局变量未被清除的一个结果是,用于在 OnInit 处理函数中进行分析的反初始化代码 _UninitReason 保持不变。新的代码只会在下一个事件发生时,就在调用 OnDeinit 之前,写入该变量中。
在 OnInit 函数结束之前为智能交易系统接收到的所有事件都会被跳过。
当 MQL 程序首次启动时,设置对话框会在步骤 1 和 2 之间显示。当更改输入参数时,根据程序类型的不同,设置对话框会以不同的方式插入到一般循环中:对于指标,它仍然会在步骤 2 之前出现,而对于智能交易系统,则会在步骤 3 之前出现。
本书附带了一个名为 LifeCycle.mq5 的指标和智能交易系统模板。它会在 OnInit/OnDeinit 处理函数中记录全局初始化/最终处理步骤。将程序放置在图表上,看看针对各种用户操作(加载/卸载、更改参数、切换交易品种/时间框架)会发生哪些事件。
脚本只有在添加到图表上时才会被加载。如果一个脚本在循环中运行,重新编译它不会导致其重新启动。
服务程序使用终端界面中的上下文菜单命令进行加载和卸载。当重新编译已经在运行的服务程序时,它会被重新启动。回想一下,服务程序的活动实例在终端启动时会自动加载,在终端关闭时会自动卸载。
在接下来的两个部分中,我们将从事件处理函数的层面来考虑不同 MQL 程序的启动特点。
指标和智能交易系统的参考事件:OnInit 和 OnDeinit
在交互式 MQL 程序(指标和智能交易系统)中,环境会生成两个事件,分别用于准备启动(OnInit)和停止(OnDeinit)。脚本和服务程序中不存在这类事件,因为它们不接受异步事件:在控制权传递给它们唯一的事件处理函数 OnStart 之后,直到工作结束,脚本/服务程序线程的执行上下文都处于 MQL 程序的代码中。相反,对于指标和智能交易系统而言,正常的工作流程假定环境会多次调用它们特定的事件处理函数(我们将在指标和智能交易系统的相关部分讨论这些函数),并且每次程序执行必要操作后,都会将控制权交还给终端,以空闲等待新事件。
int OnInit()
OnInit 函数是同名事件的处理函数,该事件在加载智能交易系统或指标后生成。此函数可根据需要进行定义。
该函数必须返回 ENUM_INIT_RETCODE 枚举类型中的一个值。
标识符 | 描述 |
---|---|
INIT_SUCCEEDED | 初始化成功,程序可以继续执行;对应值为 0 |
INIT_FAILED | 初始化失败,由于致命错误(例如,无法创建文件或辅助指标),程序无法继续执行;值为 1 |
INIT_PARAMETERS_INCORRECT | 输入参数集不正确,程序无法执行 |
INIT_AGENT_NOT_SUITABLE | 在测试器中工作的特定代码:由于某些原因,此代理不适合进行测试(例如,内存不足、不支持 OpenCL 等) |
如果 OnInit 返回任何非零返回码,则意味着初始化失败,随后会生成 Deinit 事件,反初始化原因代码为 REASON_INITFAILED(见下文)。
OnInit 函数可以声明为返回类型为 void:在这种情况下,初始化始终被视为成功。
在 OnInit 处理函数中,重要的是检查是否存在所有必要的环境信息,如果信息不可用,则将准备操作推迟到下一个报价数据或定时器到达事件。关键在于,当终端启动时,OnInit 事件通常在与服务器建立连接之前触发,因此许多金融工具和交易账户的属性仍然未知。特别是,特定交易品种的一个点值可能会返回为零。
void OnDeinit(const int reason)
当智能交易系统或指标进行反初始化时,会调用 OnDeinit 函数(如果已定义)。此函数是可选的。
reason 参数包含反初始化原因代码。可能的值如下表所示:
常量 | 值 | 描述 |
---|---|---|
REASON_PROGRAM | 0 | 通过调用 ExpertRemove 函数停止智能交易系统的操作 |
REASON_REMOVE | 1 | 程序从图表中移除 |
REASON_RECOMPILE | 2 | 程序重新编译 |
REASON_CHARTCHANGE | 3 | 图表的交易品种或周期发生更改 |
REASON_CHARTCLOSE | 4 | 图表关闭 |
REASON_PARAMETERS | 5 | 输入参数发生更改 |
REASON_ACCOUNT | 6 | 激活了另一个账户,或者由于账户设置更改而重新连接到交易服务器 |
REASON_TEMPLATE | 7 | 应用了不同的图表模板 |
REASON_INITFAILED | 8 | OnInit 处理函数返回非零值 |
REASON_CLOSE | 9 | 终端关闭 |
如果 MQL 程序中设置了停止标志 _StopFlag,则可以在程序的任何位置使用 UninitializeReason 函数获取相同的代码。
AllInOne.mqh 文件中有一个 Finalizer 类,它允许通过调用 UninitializeReason 在析构函数中“挂钩”反初始化代码。我们必须在 OnDeinit 处理函数中获得相同的值。
cpp
class Finalizer
{
static const Finalizer f;
public:
~Finalizer()
{
PRTF(EnumToString((ENUM_DEINIT_REASON)UninitializeReason()));
}
};
static const Finalizer Finalizer::f;
为了方便使用 EnumToString 将代码转换为字符串表示形式(原因名称),在 Uninit.mqh 文件中描述了包含上述表格中常量的枚举类型 ENUM_DEINIT_REASON。日志中将显示如下条目:
OnDeinit DEINIT_REASON_REMOVE
EnumToString((ENUM_DEINIT_REASON)UninitializeReason())=DEINIT_REASON_REMOVE / ok
当更改指标所在图表的交易品种或时间框架时,指标会被卸载并重新加载。在这种情况下,旧副本中 OnDeinit 事件的触发和新副本中 OnInit 事件的触发顺序是不确定的。这是由于终端处理异步事件的特性所致。换句话说,可能会出现不太合理的情况,即新副本在旧副本完全卸载之前就被加载并初始化。如果指标在 OnInit 中进行了一些图表调整(例如,创建图形对象),那么在不采取特殊措施的情况下,已卸载的副本可能会立即“清理”图表(删除对象,认为它是自己创建的)。对于图形对象的特定情况,有一个特定的解决方案:可以给对象命名,使其包含交易品种和时间框架前缀(以及输入变量值的校验和),但在一般情况下,这种方法并不适用。为了通用地解决这个问题,应该实现某种同步机制,例如使用全局变量或资源。
在测试器中测试指标时,MetaTrader 5 开发者决定不生成 OnDeinit 事件。他们的想法是,指标可能会创建一些图形对象,通常会在 OnDeinit 处理函数中删除这些对象,但用户希望在测试完成后看到它们。实际上,如果愿意,MQL 程序的作者可以通过对 MQLInfoInteger(MQL_TESTER) 模式进行正向检查来提供类似的行为。这很奇怪,因为在智能交易系统测试后会调用 OnDeinit 处理函数,并且智能交易系统可以在 OnDeinit 中以同样的方式删除对象。现在,仅对于指标而言,测试器中无法保证 OnDeinit 处理函数的常规行为。此外,也不会执行其他最终处理操作,例如,不会调用全局对象的析构函数。
因此,如果需要在测试运行后执行统计计算、保存文件或其他原本打算在指标的 OnDeinit 中执行的操作,就必须将指标算法转移到智能交易系统中。
脚本和服务程序的主函数:OnStart
实用型程序(脚本和服务程序)在终端中通过调用它们唯一的事件处理函数 OnStart 来执行。
void OnStart()
这个函数没有参数,也不返回任何值。它仅仅作为从终端端进入应用程序的一个入口点。
一般来说,脚本是用于在图表上执行一次性操作的(稍后我们将学习图表 API 所提供的所有功能)。例如,一个脚本可以用来设置订单网格,或者相反,关闭所有盈利的未平仓头寸,自动使用图形对象进行标注,或者临时隐藏所有对象。
在脚本中,你可以使用包含在无限循环中的持续操作,如前面所提到的,在这种情况下,你应该始终检查停止标志(_StopFlag)并定期释放处理器(使用 Sleep 函数)。这里应该记住,当你关闭并重新打开终端时,脚本将不得不再次运行。
因此,对于这种持续的活动,如果它与图表的操作没有直接关系,最好使用服务程序。实现服务程序的标准技术就是一个“无限”循环。
在本书前面的部分中,几乎所有的示例都是作为脚本来实现的。一个服务程序的示例是“使用全局变量同步程序”部分中的 GlobalsWithCondition.mq5 程序。在下一部分关于使用 ExpertRemove 函数停止智能交易系统和脚本的内容中,我们还将看到另一个示例。
以编程方式移除智能交易系统和脚本:ExpertRemove
如果有必要,开发者可以组织停止并卸载两种类型的 MQL 程序:智能交易系统(EA)和脚本。这是通过使用 ExpertRemove 函数来完成的。
void ExpertRemove()
这个函数没有参数,也不返回任何值。它向 MQL 程序执行环境发送一个请求,以删除当前程序。实际上,这会导致设置 _StopFlag 标志,并停止接收(和处理)所有后续事件。在此之后,程序会有 3 秒钟的时间来妥善完成其工作:释放资源、中断算法中的循环等等。如果程序没有这样做,它将被强制卸载,同时会丢失中间数据。
这个函数在指标和服务程序中不起作用(程序会继续运行)。
对于每次函数调用,日志中都会包含“ExpertRemove() 函数被调用”的记录。
这个函数主要用于那些无法以其他方式中断的智能交易系统。对于脚本而言,通常使用 break 语句来中断循环(如果存在循环的话)会更容易。但是,如果循环是嵌套的,或者算法中存在许多相互调用的函数,那么在继续计算的条件中,在不同级别考虑停止标志会更容易,并且在出现错误情况时,使用 ExpertRemove 来设置这个标志。如果不使用这个内置标志,无论如何你都必须引入一个具有相同用途的全局变量。
脚本 ScriptRemove.mq5 提供了 ExpertRemove 的使用示例。
算法运行中可能导致需要卸载脚本的潜在问题由 ProblemSource 类来模拟。在其构造函数中会随机调用 ExpertRemove。
cpp
class ProblemSource
{
public:
ProblemSource()
{
// 模拟对象创建过程中的问题,例如,
// 捕获某些资源,如文件等
if(rand() > 20000)
{
ExpertRemove(); // 将 _StopFlag 设置为 true
}
}
};
接下来,在全局级别和辅助函数内部创建这个类的对象。
cpp
ProblemSource global; // 对象可能会抛出错误
void SubFunction()
{
ProblemSource local; //对象可能会抛出错误
// 模拟一些工作(我们需要检查对象的完整性!)
Sleep(1000);
}
现在,我们在 OnStart 操作中,在带有 IsStopped 条件的循环内部使用 SubFunction。
cpp
void OnStart()
{
int count = 0;
// 循环直到被用户或程序自身停止
while(!IsStopped())
{
SubFunction();
Print(++count);
}
}
以下是一个日志示例(由于随机性,每次运行结果都会不同):
1
2
3
ExpertRemove() function called
4
请注意,如果在创建全局对象时发生错误,循环将永远不会执行。
因为智能交易系统可以在测试器中运行,所以 ExpertRemove 函数也可以在测试器中使用。它的效果取决于函数调用的位置。如果在 OnInit 处理函数内部调用,该函数将取消测试,也就是说,测试器针对当前智能交易系统参数集的一次运行将被取消。这种终止被视为初始化错误。当在算法的任何其他位置调用 ExpertRemove 时,智能交易系统的测试将提前中断,但会以常规方式进行处理,会调用 OnDeinit 和 OnTester。在这种情况下,会获得累积的交易统计数据和优化标准的值,要考虑到模拟的服务器时间 TimeCurrent 没有达到测试器设置中的结束日期。