内容目录
- 概论
- EA 例程 Hello World
- 源和头文件
- 条件编译
- 拆分实现
- 拆分目录和文件
- 文件包含
- 继承
- 限制
- 结论
概论
在 MetaTrader 里创建跨平台 EA 的原因有以下几点:
- 您十分喜欢与他人分享 EA, 无论他们使用什么版本的交易平台。
- 您想了解 MQL4 和 MQL5 之间的差异。
- 您希望节省编码时间。
- 如果 MetaTrader 4 突然成为过时软件, 当您的交易机器人迁移至 MetaTrader 5 时会少一些麻烦。
- 您已经是一位 MetaTrader 5 用户, 但出于一些原因, 您打算在 MetaTrader 4 上测试您的 EA。
- 您仍然是一位 MetaTrader 4 的用户, 但您想使用 MQL5 云服务测试和优化您的交易机器人。
当开 EA 时, 甚或指标和脚本, 开发者通常推行以下行动方针:
- 使用一种语言 (MQL4 或 MQL5) 开发软件
- 彻底测试开发的软件
- 用其它语言重新实现相同的软件
这有若干缺点:
- 软件的各方各面都需要重新实现, 包括两个版本之中共享的部份或功能
- 调试和维护将会很困难
- 降低生产力
独立, 并行现实的功能需要近乎倍增编写代码的时间: 一次用 MQL4, 另一次用 MQL5。调试和维护更具挑战性。如果一个版本需要更新, 同样的更新可能也需要引入到其它版本。并且由于 MQL4 和 MQL5 之间的差异, 相同软件的两个版本不可避免地在一些地方存在偏差。这会带来更多潜在的问题, 因为代码中的偏差往往出于单独, 并行实现造成的不明确。
EA 例程 Hello World
让我们从利用 MQL5 编写一段简单的 EA 开始: 一段 hello world 智能交易程序。在所述的 MQL 版本里, 我们的典型源代码如下所示:
(HelloWorld.mq5)
#include <Object.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorld : public CObject { public: CHelloWorld(void); ~CHelloWorld(void); virtual void Greeting(void); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::Greeting(void) { Print("Hello World!"); } CHelloWorld hello; //+------------------------------------------------------------------+ //| EA 初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA 逆初函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| EA 分时处理函数 | //+------------------------------------------------------------------+ void OnTick() { //--- hello.Greeting(); ExpertRemove(); }
利用 MQL4, 我们按照相同方式编写的应用:
HelloWorld.mq4
#include <Object.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorld : public CObject { public: CHelloWorld(void); ~CHelloWorld(void); virtual void Greeting(void); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(void) { } CHelloWorld hello; //+------------------------------------------------------------------+ //| EA 初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA 逆初函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| EA 分时处理函数 | //+------------------------------------------------------------------+ void OnTick() { //--- hello.Greeting(); ExpertRemove(); }
源和头文件
请注意, 前面显示的两个源文件是相同的。它不可能有单一源文件可以跨平台兼容。这在于源文件是如何被编译的:
- 编译一个 MQ4 源文件的结果将生成一个 EX4 文件
- 编译一个 MQ5 源文件的结果将生成一个 EX5 文件。
不太可能有单一源文件可以在两个平台上工作。不过, 两个源文件引用单一的头文件是有可能的, 如下图描绘的那样:
理想情况下, 我们希望在头文件中拥有一切, 两份源文件里只有一行源代码: 链接头文件的声明。然后, 我们可以如下重写 Hello World 的 EA 头文件:
HelloWorld_SingleHeader.mqh
#include <Object.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorld : public CObject { public: CHelloWorld(void); ~CHelloWorld(void); virtual void Greeting(void); virtual void Greeting(const string str1,const string str2); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::Greeting(void) { Print("Hello World!"); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::Greeting(const string str1,const string str2) { string str=NULL; Print(StringConcatenate(str,str1,str2)); } CHelloWorld hello; //+------------------------------------------------------------------+ //| EA 初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA 逆初函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| EA 分时处理函数 | //+------------------------------------------------------------------+ void OnTick() { //--- hello.Greeting("你好 ","世界!"); ExpertRemove(); } //+------------------------------------------------------------------+
在 MQL4 和 MQL5 源文件里分别包含一行代码, 即一条 #include 指令引用上述的头文件:
HelloWorld_SingleHeader.mq4 和 HelloWorld_SingleHeader.mq5
#include <HelloWorld_SingleHeader.mqh>
使用这种方法有若干优点。首先, 我们可以潜在地减少分别为两个平台编程的代码量, 最高可达 50% (至少在本例中)。第二个优点是, 这种设定可令我们关注单一的实现, 而非两个单独的。由于只需在单一的源代码上工作, 在 MQL4 版本里所做的更改也将应用到 MQL5 版本, 反之亦然。
使用正常的方法, 如果一个人修改了一份源文件, 他也要将更改添加到其它平台的其它源文件。智能交易程序很少这样编写示例代码。它们会更复杂。而且因为智能交易程序会变得越来越复杂, 维护两个独立版本也将变得越来越困难。
条件编译
MQL4 和 MQL5 有许多东西是共享的, 但它们彼此在许多方面也有所不同。这其中的差别是 StringConcatenate 函数的实现。在 MQL4 中, 函数定义如下:
string StringConcatenate( void argument1, // 任何简单类型的第一个参数 void argument2, // 任何简单类型的第二个参数 ... // 任何简单类型的下一个参数 );
在 MQL5 中, 函数的实现略有不同:
int StringConcatenate( string& string_var, // 源自字符串 void argument1 // 任何简单类型的第一个参数 void argument2 // 任何简单类型的第一个参数 ... // 任何简单类型的下一个参数 );
我们可以在 Hello World 应用里通过重载我们的类方法 Greeting() 来使用这个函数。新方法将接受两个字符串参数, 并且其连接后的结果将打印在终端上。我们如下更新头文件:
(HelloWorld_SingleHeader.mqh)
#include <Object.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorld : public CObject { public: CHelloWorld(void); ~CHelloWorld(void); virtual void Greeting(void); virtual void Greeting(const string str1,const string str2); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::Greeting(void) { Print("Hello World!"); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::Greeting(const string str1,const string str2) { string str=NULL; Print(StringConcatenate(str,str1,str2)); } CHelloWorld hello; //+------------------------------------------------------------------+ //| EA 初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA 逆初函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| EA 分时处理函数 | //+------------------------------------------------------------------+ void OnTick() { //--- hello.Greeting("你好 ","世界!"); ExpertRemove(); } //+------------------------------------------------------------------+
使用此更新版本, 在 MetaTrader 4平台上我们看到终端上将打印以下结果:
Hello World!
在 MetaTrader 5 上, 我们将会看到与最初预想不同的结果:
12
采用 MQL4 时, 函数返回串联文本的字符串。另一面, 采用 MQL5 时, 返回值则用串联文本的长度整数值替代。。为了让应用程序在两个平台上表现相同的行为, 而不必重写大部分的代码, 我们可以简单地使用 条件编译, 如下所示代码:
CHelloWorld::Greeting(const string str1,const string str2) { #ifdef __MQL5__ string str=NULL; StringConcatenate(str,str1,str2); Print(str); #else Print(StringConcatenate(str1,str2)); #endif }
请注意, 这是一个预处理指令。这有可能在编译时产生额外开销, 但不会出现在执行时间。在 MQL4 中, 编译器如下解释代码:
CHelloWorld::Greeting(const string str1,const string str2) { Print(StringConcatenate(str1,str2)); }
另一面, MQL5 的编译器如下看待代码:
CHelloWorld::Greeting(const string str1,const string str2) { string str=NULL; StringConcatenate(str,str1,str2); Print(str); }
拆分实现
此刻, 我们已能理解为创建跨平台兼容的智能程序要有哪些类型的代码存在:
- 兼容
- 共享函数
- 计算
- 不兼容
- 函数行为不同
- 函数在一方存在, 而另一方没有
- 执行模式不同
在 MQL4 和 MQL5 之间, 存在一套行为相同的函数。函数 Print() 是其中一个例子。无论哪个平台版本的智能交易程序使用, 它的行为都相同。纯计算形成的源代码也能看出兼容。算式 1+1 的结果在 MQL4 和 MQL5 里均相同, 如同现实世界里的任何编程语言。在这两种情况下, 极少需要拆分实现。
若一段源代码的特定部分不可兼容, 或在某个平台上的执行结果不同, 这种情况下就需要拆分实现。函数 StringConcatenate 就是第一种不兼容情况代码的例子。尽管有相同的名字, 但它们的行为在 MQL4 和 MQL5 中不同。还有一些函数在其它语言中没有直接对应的函数。函数 OrderCalcMargin 即是其中一例, 至少在本文截稿之时, 在 MQL4 里没有相应的。第三种情况对于跨平台开发可能最难以处理, 因为在这些地方每一位开发者的实现均有不同。在这种情况下, 也许需要找到两个平台间的共性以便缩减代码长度, 然后依照必要拆分实现。
现在, 仅依靠条件编译可能是一个坏主意。由于代码变长, 很多这些语句可令调试或代码维护非常困难。在面向对象的程序设计中, 我们可能需要将实现分成三部分: (1) 基本实现, (2) MQL4-特殊实现, (3) MQL5-特殊实现。
基类实现将包含可由两个版本共享的代码。当出现不兼容的情况时, 可以从基本实现分出一个分支, 甚至可以将基本实现清空, 并用所有语言分别实现。
对于 Hello World 智能程序, 我们声明一个基类, 将其命名为 CHelloWorldBase, 然后在其内包含 MQL4 和 MQL5 之间共享的代码。包含我们在本文开头定义的初始 Greeting() 方法:
HelloWorld_SingleHeader.mqh
#include <Object.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorldBase : public CObject { public: CHelloWorldBase(void); ~CHelloWorldBase(void); virtual void Greeting(void); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorldBase::CHelloWorldBase(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorldBase::~CHelloWorldBase(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorldBase::Greeting(void) { Print("Hello World!"); } //+------------------------------------------------------------------+
然后我们通过从基类继承来创建平台或特定语言的类对象, 并引入不同实现以达到所需的相同结果:
HelloWorld_SingleHeader_MQL4.mqh
#include "HelloWorld_SingleHeader.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorld : public CHelloWorldBase { public: CHelloWorld(void); ~CHelloWorld(void); virtual void Greeting(const string str1,const string str2); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::Greeting(const string str1,const string str2) { Print(StringConcatenate(str1,str2)); } //+------------------------------------------------------------------+
HelloWorld_SingleHeader_MQL5.mqh
#include "HelloWorld_SingleHeader.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorld : public CHelloWorldBase { public: CHelloWorld(void); ~CHelloWorld(void); virtual void Greeting(const string str1,const string str2); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::Greeting(const string str1,const string str2) { string str=NULL; StringConcatenate(str,str1,str2); Print(str); } //+------------------------------------------------------------------+
然后, 我们将事件函数移回主要源代码文件中的正常之所在:
HelloWorld_SingleHeader.mq5
#include <HelloWorld_SingleHeader_MQL5.mqh> CHelloWorld hello; //+------------------------------------------------------------------+ //| EA 初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA 逆初函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| EA 分时处理函数 | //+------------------------------------------------------------------+ void OnTick() { //--- hello.Greeting("你好 ","世界!"); ExpertRemove(); } //+------------------------------------------------------------------+
HelloWorld_SingleHeader.mq4
#include <HelloWorld_SingleHeader_MQL4.mqh> CHelloWorld hello; //+------------------------------------------------------------------+ //| EA 初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA 逆初函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| EA 分时处理函数 | //+------------------------------------------------------------------+ void OnTick() { //--- hello.Greeting("你好 ","世界!"); ExpertRemove(); } //+------------------------------------------------------------------+
在这个特殊的例子中, 更实际一点是使用单个头文件, 含有基类, 以及两个带有条件编译指令的子类。然而, 在大多数情况下, 将类移动到单独文件是必要的, 尤其是所涉及的源代码很长之时。
文件包含
对于开发者, 自然喜欢简单地在程序中引用包含实际类定义的头文件。例如, 采用 MQL5 实现的 HelloWorld 智能交易程序, 我们能看出两个版本 (HelloWorld_SingleHeader.mq4 and HelloWorld_SingleHeader.mq5) 几乎相同, 除了它们所包含的特别头文件。
#include <HelloWorld_SingleHeader_MQL4.mqh>
#include <HelloWorld_SingleHeader_MQL5.mqh>
另一种方法, 是引用包含基本实现的头文件。然后, 在头文件的结尾, 我们可以使用一个条件编译指令来引用包含适当子类的头文件, 依据编译器的类型进行选择:#include <Object.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorldBase : public CObject { public: CHelloWorldBase(void); ~CHelloWorldBase(void); virtual void Greeting(void); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorldBase::CHelloWorldBase(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorldBase::~CHelloWorldBase(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorldBase::Greeting(void) { Print("Hello World!"); } //+------------------------------------------------------------------+ #ifdef __MQL5__ #include "HelloWorld_SingleHeader_MQL5.mqh" #else #include "HelloWorld_SingleHeader_MQL4.mqh" #endif //+------------------------------------------------------------------+
然后, 我们在主要源文件中引用这个头文件, 而不是特定语言的头文件:
#include <HelloWorld_SingleHeader.mqh> CHelloWorld hello; //+------------------------------------------------------------------+ //| EA 初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA 逆初函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| EA 分时处理函数 | //+------------------------------------------------------------------+ void OnTick() { //--- hello.Greeting("你好 ","世界!"); ExpertRemove(); } //+------------------------------------------------------------------+
此后, 我们从包含特定语言实现的头文件里删除 #include 指令 (将文本显示中带删除线的代码删除):
#include "HelloWorld_SingleHeader.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorld : public CHelloWorldBase { public: CHelloWorld(void); ~CHelloWorld(void); virtual void Greeting(const string str1,const string str2); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::Greeting(const string str1,const string str2) { Print(StringConcatenate(str1,str2)); } //+------------------------------------------------------------------+ #include "HelloWorld_SingleHeader.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorld : public CHelloWorldBase { public: CHelloWorld(void); ~CHelloWorld(void); virtual void Greeting(const string str1,const string str2); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorld::Greeting(const string str1,const string str2) { string str=NULL; StringConcatenate(str,str1,str2); Print(str); } //+------------------------------------------------------------------+
这种方法值得推荐并具有几个优点。首先, 它可保持在 MQL4 和 MQL5 两个主要源文件里的包含指令相同。它还可以节省一些从包含指令中思考选择哪个特定头文件和路径 (譬如 MQL4/ 或 MQL5/) 用来包含的精神开销。第三个优点是在基础头文件上保持基本包容。如果其一在特定语言头文件上使用包含指令, 它将只能作为版本 (MQL4 或 MQL5) 的专用。
拆分目录和文件
当采用 OOP 开发智能交易程序时, 对任何人来讲, 在一个单独的类定义里编写所有东西都确实不容易。这方面的一个证明就是 MQL5 标准库的交易策略类。随着代码行数增加, 更实际的做法是将代码划分为各种头文件。本文建议以下目录格式:
|-Include
|-Base
|-MQL4
|-MQL5
右侧的三个目录可以放置在数据文件夹的 Include 目录中, 或者在所述文件夹内的一个子目录中。
对于我们的例程代码, 我们将采取以下的目录结构:
|-Include
|-MQLx-Intro
|-Base
HelloWorldBase.mqh
|-MQL4
HelloWorld.mqh
|-MQL5
HelloWorld.mqh
使用诸如此类的目录结构, 能令我们更好的组织代码。它也能消除我们早前避免的文件命名冲突问题。
由于我们的头文件目录位置变化, 我们需要在类的主要头文件里更新其两个子代的新位置:
#include <Object.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CHelloWorldBase : public CObject { public: CHelloWorldBase(void); ~CHelloWorldBase(void); virtual void Greeting(void); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorldBase::CHelloWorldBase(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorldBase::~CHelloWorldBase(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CHelloWorldBase::Greeting(void) { Print("Hello World!"); } //+------------------------------------------------------------------+ #ifdef __MQL5__ #include "..\MQL5\HelloWorld.mqh" #else #include "..\MQL4\HelloWorld.mqh" #endif //+------------------------------------------------------------------+
我们还在主要的源文件里更新我们基类的位置。对于全部两个版本, 源文件在这个阶段一致:
HelloWorld_Sample.mq4 and HelloWorld_Sample.mq5
#include <MQLx-Intro\Base\HelloWorldBase.mqh> CHelloWorld hello; //+------------------------------------------------------------------+ //| EA 初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA 逆初函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| EA 分时处理函数 | //+------------------------------------------------------------------+ void OnTick() { //--- hello.Greeting("你好 ","世界!"); ExpertRemove(); } //+------------------------------------------------------------------+
继承
假设我们要扩展前面定义的 HelloWorld 类, 就像名为 CGoodByeWorld 的类。这个类使用 CHelloWorld 的 Greeting() 方法以便创建 "再见世界!" 的消息。为达此目的的一种方式 (推荐) 就是引用祖辈的基类, 即 CHelloWorldBase。之后, 类似于 CHelloWorldBase, 在文件末尾包含预处理条件编译指令, 引用正确的子代。继承层次就如以下模样
然而, 包含头文件的方式略有不同:
类的图解如下图例所示。初始的 Greeting 函数在 CHelloWorldBase 类中, 此方法用于 (继承) 贯穿所有其它的子类。对于 CGoodByeWorld 类也一样, 也有一个新方法名为 GoodBye。同样可以扩展 CHelloWorldBase 的方法, 以致问候语将会说 "再见" 而非 "你好"。
我们只包括基类的头文件。在只涉及单一类层次的情况下, 我们只包括具有最大抽象化的基类 (GoodByeWorldBase.mqh), 因为引用这个文件会自动包含其它所需的头文件。请注意, 我们未使用 #include 来引用特定平台的头文件, 因为基类的头文件将会负责包含它们。
我们的目录结构也将被更新, 完成后, 其内已经包含新的头文件:
|-Include
|-MQLx-Intro
|-Base
HelloWorldBase.mqh
GoodByeWorldBase.mqh
|-MQL4
HelloWorld.mqh
GoodByeWorld.mqh
|-MQL5
HelloWorld.mqh
GoodByeWorld.mqh
以下是 CGoodByeWorldBase 类的实现:
#include "HelloWorldBase.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CGoodByeWorldBase : public CHelloWorld { public: CGoodByeWorldBase(void); ~CGoodByeWorldBase(void); virtual void GoodBye(void); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CGoodByeWorldBase::CGoodByeWorldBase(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CGoodByeWorldBase::~CGoodByeWorldBase(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CGoodByeWorldBase::GoodBye(void) { Greeting("再见 ","世界!"); } //+------------------------------------------------------------------+ #ifdef __MQL5__ #include "..\MQL5\GoodByeWorld.mqh" #else #include "..\MQL4\GoodByeWorld.mqh" #endif //+------------------------------------------------------------------+
要注意的是尽管包含了文件 "HelloWorldBase.mqh", 但 CGoodByeWorldBase 类是继承自 CHelloWorld, 而非 CHelloWorldBase。CHelloWorld 所使用的版本最终取决于 MQL 编译器所用版本。扩展 CHelloWorldBase 也能在其它种情况下工作。不过, 在此例中, 由于 Goodbye() 方法使用 Greeting() 方法, CGoodByeWorldBase 将需要直接继承自 CHelloWorld 的特定平台实现。
由于 GoodBye() 方法可在两个版本之间共享, 那么在基类中保留它是最理想的。并且由于在此类对象里没有其它额外方法, 子代将缺乏任何新的类方法。然后, 我们可以按照以下方式实现子代:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CGoodByeWorld : public CGoodByeWorldBase { }; //+------------------------------------------------------------------+
主要的源文件也需要更新, 这次, 基于 CGoodByeWorld 实例化一个对象, 并在 OnTick 事件处理器中调用 GoodBye() 方法。
HelloWorld_Sample.mq4 and HelloWorld_Sample.mq5
#include <MQLx-Intro\Base\GoodByeWorldBase.mqh> CGoodByeWorld hello; //+------------------------------------------------------------------+ //| EA 初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA 逆初函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| EA 分时处理函数 | //+------------------------------------------------------------------+ void OnTick() { //--- hello.Greeting("你好 ","世界!"); hello.GoodBye(); ExpertRemove(); } //+------------------------------------------------------------------+
运行智能交易程序, 在终端上将会打印如下结果:
你好世界!
再见世界!
ExpertRemove() 函数已被调用
限制
在大多数情况下, 这种方法允许程序员更快、更高效地开发跨平台智能交易程序。不过, 提醒读者要注意某些局限性, 即对于一些人来说, 应用本文中展示的这种方法也许很困难, 甚至不可能:
1. 在 MetaTrader 4 里的限制
2. 两个平台之间的约定或执行有很大差异。
MetaTrader 4, 是较老的交易平台, 缺乏某些在 MetaTrader 5 里拥有的功能。当智能交易程序所需功能在某一平台缺失的情况下, 有必要为此平台开发专用的定制方案。MetaTrader 5 原生智能交易程序的主要问题就是需要在 MetaTrader 4 里有对口功能。MetaTrader 4 的用户在这方面的担心较少, 因为 MetaTrader 4 的大部分功能, 尽管不是全部, 在 MetaTrader 5 均有一个对口, 或者至少有一个简单的变通方案。
两个平台在某些操作中有很大程度的不同。对于交易操作来说尤其如此。在此情况下, 开发者将不得不选择采用哪个约定。例如, 他可能使用 MetaTrader 4 约定并将其翻译成 MetaTrader 5 约定, 以便达成最终行为的一致。或者情况正好相反, 他不得不将 MetaTrader 5 中进行交易的习惯做法应用到 MetaTrader 4 的智能交易程序。
结论
在本文中, 我们已经展示了开发跨平台智能交易程序的可行方法。所述方法提议使用一个基类, 包含两方交易平台共享的实现。在两种语言相互存在差异领域, 将拆分实现放入继承自基类的子类。对于需要进一步定义类层次结构的类, 重复相同的方法。此方法可以证明有益于减少开发跨平台应用的时间, 通过避免单独和并行实现的需求, 令代码维护更容易。