设计各种类型的 MQL 程序

程序类型是 MQL5 中的基本特性。在 C++ 等通用编程语言中,任何程序都可以以任意方向进行开发,例如添加图形界面或者通过网络从服务器上传数据,但 MQL 程序与此不同,而是依据其用途被划分为特定类别。例如,通过指标实现带可视化功能的技术性时间序列分析,但指标本身无法用于交易。反过来,EA 交易可以使用交易 API 函数,但它们缺少指标缓冲区(用于绘制线条的数组)。

因此,在解决某个具体应用问题时,开发者应将问题分解为多个部分,并且每一部分的功能都应与特定类型的专长相符。当然,简单情况下,单个 MQL 程序即可满足需求,但有时最优技术方案并不显而易见。例如,你会怎样实现砖形图的绘制:是作为 指标,还是作为服务生成的 自定义交易品种 亦或是直接在 EA 交易作为特定计算?所有选项都可行。

MQL 程序的类型由多个因素决定。

首先,每种类型的程序在 MQL5 工作目录中都有独立的文件夹。我们在 第一章 的引言中已经提到过这一点,并列出了相关文件夹。因此,对于指标、EA 交易、脚本和服务,指定文件夹分别为 IndicatorsExpertsScriptsServicesLibraries子文件夹为 MQL5 文件夹中的库保留。在这些文件夹中,你可以按任意配置方式组织嵌套文件夹树形结构。

编译 mq5 文件生成的二进制文件(即扩展名为ex5的成品程序)会与源 mq5 文件存放在同一目录下。然而,我们还应提及 MetaEditor 中的项目(扩展名为 mqproj的文件),这部分内容我们将在 项目一章进行分析。当开发一个项目时,成品会在项目旁边的目录中创建。当在 MetaEditor 中通过 MQL5 向导创建程序时(通过File -> New),源文件默认会放置在与程序类型相对应的文件夹中。如果不小心将程序复制到了错误的目录,也不会造成严重问题:例如,EA 交易不会变成指标,指标也不会变成 EA 交易。可以直接在编辑器中、在Navigator窗口内或者在外部文件管理器中将其移动到所需位置。在 Navigator中,每种程序类型都会显示一个特殊图标。

程序在 MQL5 目录中特定类型子文件夹内的存放位置,并不能决定该特定 MQL 程序的类型。程序类型是根据可执行文件的内容确定的,而可执行文件反过来又是由编译器根据源代码中的特性指令和语句生成的。
 
按程序类型划分的文件夹层级结构是为了便于使用。建议遵循这种层级结构,除非是处理一组相关项目(包含不同类型的程序),这种情况下将它们存储在单独的目录中更符合逻辑。

第二,每种类型的程序都有如下特征:支持一组有限的、特定的系统事件,这些事件会激活该程序。我们将在单独章节中了解 事件处理函数概述 。要在程序中接收特定类型的事件,需要描述一个具有预定义原型(名称、参数列表、返回值)处理程序函数。

例如,我们已经看到,在脚本和服务中,工作在OnStart函数中开始,由于它是其中唯一的函数,因此可以将其称为主要的“入口点”,终端通过此入口点将控制权移交给应用程序代码。在其他类型的程序中,情况会稍微复杂一些。总的来说,我们注意到,程序类型的特征取决于一组特定的处理程序,其中一些处理程序可能是必需的,而另一些是可选的(但同时,对于其他类型的程序来说是不可接受的)。具体来说,指标需要OnCalculate函数(如果没有它,指标将无法编译,编译器会生成错误)。然而 EA 交易中并不使用此函数。

第三,某些类型的程序需要特殊的#property指令。在 程序通用特性一章中,我们已经了解了可以在所有类型的程序中使用的指令。然而,还有其他一些专门的指令。例如,在我们提到的与服务相关的任务中,我们遇到了#property service指令,该指令使程序成为一个服务。如果没有这个指令,即使将程序放在 Services文件夹中,它也无法在后台运行。

同样,#property library指令在创建库时起着决定性作用。所有这些指令特性将在相应类型程序的章节中详细讨论。

在按以下顺序(从上到下,直到找到第一个匹配项)确定 MQL 程序类型时,将考虑指令和事件处理程序的组合:

  • 指标:存在 OnCalculate处理程序
  • 库: #property library
  • 脚本:存在 OnStart处理程序且不存在 #property service
  • 服务:存在 OnStart处理程序且 #property service
  • EA 交易:存在任何其他处理程序

关于这些特性对编译器的影响示例,将在 事件处理函数概述章节中给出。

对于以上提到的所有要点,还需额外考虑一点:程序类型由主编译模块决定,即扩展名为 mq5 的文件,在主编译模块中,可使用#include指令来包含其他目录中的源文件。通过这种方式引入的所有函数,其级别与直接编写在主 .mq5 文件中的函数完全一致。
 
另一方面,#property指令仅在编译的主 mq5 文件中生效。如果这些指令出现在通过 #include引入的程序文件中,则将被忽略。

主 mq5 文件并不需要实际包含事件处理程序函数。将部分或全部算法放置在 mqh 头文件中,然后将这些头文件包含在一个或多个程序中,这样做是完全可以接受的。例如,我们可以在 mqh 文件中实现一个包含一组有用操作的 OnStart处理程序,然后通过 #include 在两个独立程序(脚本和服务)中使用该处理程序。

同时要注意,存在通用事件处理程序并不是将通用算法片段分离到头文件中的唯一原因。例如,你可以在指标和 EA 交易中使用相同的计算公式,而将它们的事件处理程序留在主程序模块中。

虽然习惯上将包含文件称为头文件,并给它们加上mqh扩展名,但从技术层面来讲这并不是必需的。在技术上允许(但不推荐)在一个 mq5 文件中包含另一个 mq5 文件或 txt 文件。这些文件可能包含一些遗留代码,或者可以说包含以常量对数组进行初始化的操作。被包含的 mq5 文件并不会因此成为主文件。
 
应该确保程序中仅包含特定程序类型的事件处理函数,且避免函数重复(如你所知,函数通过名称和参数列表的组合来识别: 函数重载 仅在参数集不同的情况下才被允许)这通常通过各种预处理器指令来实现。例如,在某些程序中包含第三方 mq5 脚本文件前,通过定义 #define OnStart OnStartPrevious宏,我们实际上会将其中描述的 OnStart 函数转变为 OnStartPrevious,并且我们可以从我们自己的事件处理程序中正常调用它。
 
然而,只有在特殊情况下由于某些原因无法修改所包含的 mq5 文件的源代码时,这种方法才有意义,特别是,当无法通过将相关算法选择到单独的头文件中的函数或类来对其进行结构化处理时。

根据与用户交互的原则,MQL 程序可以分为交互式程序和实用程序。

交互式程序(指标和 EA 交易)可以处理 事件(即在软件环境中因响应用户按下键盘上的按钮、移动鼠标、更改窗口大小等操作而触发的事件),以及与接收报价数据或计时器操作相关的许多其他事件。

而实用程序(服务和脚本)仅依据启动时设置的输入变量来运行,并且不对系统中的事件做出响应。

除了所有这些类型的程序之外,还有 。库始终作为其他类型的 MQL 程序(四种主要类型之一)的一部分来执行,因此不具备任何独特的特征或行为。特别是,库不能直接从终端接收事件,也没有自己的线程(请参阅 下一节)。同一个库可以连接到许多程序,并且这种连接是在每个父程序启动时动态发生的。在库章节中,我们将学习如何描述库的导出 API 并将其导入到父程序中。