跨平台智能交易程序: 概论·综合运用
(3/3)·从 Hello World 到条件编译与继承,把一套逻辑同时喂给两个平台的最后拼图
多数跨平台 EA 作者仍在维护两套平行代码,改一处逻辑就要两边同步,偏差悄然累积。本篇接前两步铺垫,把目录拆分、文件包含与继承的限制一次性讲透,让单头文件方案真正可落地。
单头文件里塞进 EA 骨架与类实现
把 EA 的 OnDeinit、OnTick 和 CHelloWorld 类的定义、实现全部收进一个 .mqh,再被 mq5 用两行 #include 拉进来,是 MQL5 单头文件封装的常见做法。这样工程只剩一个头文件,编译单元干净,适合做最小可运行样例。 OnTick 里只做了一件事:调用 hello.Greeting("你好 ","世界!") 打印拼接串,随后立刻 ExpertRemove() 把 EA 从图表卸掉。也就是说,这个 EA 加载后第一个 tick 就会输出一次就自毁,不会常驻。 类 CHelloWorld 公有继承 CHelloWorldBase,构造/析构都是空壳,唯一虚函数 Greeting 用 StringConcatenate 拼两串再 Print。注意 Greeting 在基类被声明为 virtual,子类重写时保持签名一致才能走多态。 下面这段是原文里的完整代码,可直接复制到 MT5 的 MQEditor 里建一个 HelloWorld_SingleHeader.mqh 验证:
class=class="str">"cmt">//| EA 逆初函数 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 分时处理函数 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- hello.Greeting("你好 ","世界!"); ExpertRemove(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "HelloWorld_SingleHeader.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class CHelloWorld : class="kw">public CHelloWorldBase { class="kw">public: CHelloWorld(class="type">void); ~CHelloWorld(class="type">void); class="kw">virtual class="type">void Greeting(class="kw">const class="type">class="kw">string str1,class="kw">const class="type">class="kw">string str2); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CHelloWorld::Greeting(class="kw">const class="type">class="kw">string str1,class="kw">const class="type">class="kw">string str2) { Print(StringConcatenate(str1,str2)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "HelloWorld_SingleHeader.mqh"
「从基类派生一个可打印问候的子类」
在 MT5 的 MQL5 工程里,自定义类通常继承自框架提供的基类,这里用 CHelloWorldBase 做父类,派生了 CHelloWorld。派生类公开声明了构造函数、析构函数,以及一个虚函数 Greeting,接收两个字符串参数。 构造与析构函数体暂时为空,意味着对象创建和销毁阶段不额外做事,真正的逻辑落在 Greeting 里。 Greeting 内部先定义空字符串 str,调用 StringConcatenate 把 str1 与 str2 拼接到 str,再用 Print 输出到专家日志。你在 MT5 里挂上这段类,调用 Greeting("Hello ","World") 就能在终端看到 HelloWorld 一行,外汇与贵金属 EA 开发属高风险,这类基础封装仅作结构验证用。
class CHelloWorld : class="kw">public CHelloWorldBase { class="kw">public: CHelloWorld(class="type">void); ~CHelloWorld(class="type">void); class="kw">virtual class="type">void Greeting(class="kw">const class="type">class="kw">string str1,class="kw">const class="type">class="kw">string str2); }; CHelloWorld::CHelloWorld(class="type">void) { } CHelloWorld::~CHelloWorld(class="type">void) { } CHelloWorld::Greeting(class="kw">const class="type">class="kw">string str1,class="kw">const class="type">class="kw">string str2) { class="type">class="kw">string str=NULL; StringConcatenate(str,str1,str2); Print(str); }
◍ 把类拆进分层目录才扛得住扩张
用 OOP 写 EA 时,把所有逻辑塞进一个类文件很快会失控。MQL5 标准库里的策略类就是例子——代码行数一多,就必须拆成多个头文件分目录放。 推荐在 Include 下建 Base、MQL4、MQL5 三层。例程采用 Include/MQLx-Intro/Base 放基类,MQL4 与 MQL5 各放一版 HelloWorld.mqh。这种结构能顺手消掉早期那种文件重名冲突。 目录挪了位,类主头文件和源文件里的 include 路径就得跟着改。下面这段是改完后的基类与版本分支引入示例,MQL4/MQL5 源文件到此阶段内容一致。 让小布替你跑这套 开 MT5 建上述目录,把代码贴进对应 mqh,编译 HelloWorld_Sample 看 Print 是否出 "Hello World!",路径写错会直接报 cannot open include file。
class="macro">#include <Object.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class CHelloWorldBase : class="kw">public CObject { class="kw">public: CHelloWorldBase(class="type">void); ~CHelloWorldBase(class="type">void); class="kw">virtual class="type">void Greeting(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CHelloWorldBase::CHelloWorldBase(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CHelloWorldBase::~CHelloWorldBase(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CHelloWorldBase::Greeting(class="type">void) { Print("Hello World!"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#ifdef __MQL5__ class="macro">#include "..\MQL5\HelloWorld.mqh" class="macro">#else class="macro">#include "..\MQL4\HelloWorld.mqh" class="macro">#endif class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <MQLx-Intro\Base\HelloWorldBase.mqh> CHelloWorld hello; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 逆初函数 | class=class="str">"cmt">//+------------------------------------------------------------------+
EA 生命周期里的空壳与自毁
这段 MQL5 片段把 EA 的两个系统回调摊开了:OnDeinit 只留了空花括号,说明卸载时没做任何资源回收或日志动作;OnTick 里先调 hello.Greeting 打印「你好 世界!」,紧接着丢出 ExpertRemove() 让 EA 跑完这一 tick 就自我注销。 从验证角度看,把这段代码塞进 MT5 的 EA 里编译挂载,日志只会出现一次问候语,随后 EA 从图表消失——这是个极简的「跑一次就退」骨架,适合做新类库调用的冒烟测试。 外汇与贵金属品种上跑这类自毁脚本无直接交易风险,但若在实盘图表误挂带下单逻辑的同类骨架,可能因未设退出条件而重复触发,仍属高风险操作环境,需先在策略测试器确认。
class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 分时处理函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- hello.Greeting("你好 ","世界!"); ExpertRemove(); } class=class="str">"cmt">//+------------------------------------------------------------------+
「让告别语复用问候的继承写法」
想在 MT5 里扩出「再见世界!」而不是重写一套输出逻辑,最干净的做法是让新基类 CGoodByeWorldBase 直接继承 CHelloWorld,而非最顶层的 CHelloWorldBase。因为 GoodBye() 内部要调 Greeting(),只有绑死具体平台的 CHelloWorld 实现,编译器才能正确解析问候函数。 目录结构要同步分层:Base 下放 HelloWorldBase.mqh 与 GoodByeWorldBase.mqh,MQL4 / MQL5 子目录各放对应平台的 HelloWorld.mqh 与 GoodByeWorld.mqh。只 include 最抽象的基类头文件即可,它会自动拉进其余依赖,不用手动引平台头。 实际跑一遍 HelloWorld_Sample,终端会打印:「你好世界! 再见世界! ExpertRemove() 函数已被调用」。外汇与贵金属 EA 开发属高风险活动,继承结构写错可能导致编译通过但逻辑偏移,建议开 MT5 逐项验证。
class="macro">#include "HelloWorldBase.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGoodByeWorldBase : class="kw">public CHelloWorld { class="kw">public: CGoodByeWorldBase(class="type">void); ~CGoodByeWorldBase(class="type">void); class="kw">virtual class="type">void GoodBye(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CGoodByeWorldBase::CGoodByeWorldBase(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CGoodByeWorldBase::~CGoodByeWorldBase(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CGoodByeWorldBase::GoodBye(class="type">void) { Greeting("再见 ","世界!"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#ifdef __MQL5__ class="macro">#include "..\MQL5\GoodByeWorld.mqh" class="macro">#else class="macro">#include "..\MQL4\GoodByeWorld.mqh" class="macro">#endif class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| |
◍ 把类拆到外部头文件再挂到 EA 上
MT5 里想让 EA 干净一点,可以把业务类的定义挪到独立 mqh,主文件只留生命周期钩子。下面这段就是典型写法:先建一个继承自基类的空壳类,再 include 基类头文件,最后在全局实例化一个对象供 OnTick 调用。
注意 include 路径用了相对目录 MQLx-Intro\Base\GoodByeWorldBase.mqh,这意味着头文件必须放在 MQL5/Include 对应子目录下,否则编译会报 cannot open file。
OnInit 和 OnDeinit 目前是空实现,只返回 INIT_SUCCEEDED;真正的动作全在 OnTick:每次报价触发先打问候、再打告别,然后立刻 ExpertRemove() 把 EA 自己卸掉。也就是说这个 EA 挂载后最多只跑一个 tick 就会退出,适合拿来做最小可运行验证而不是实盘。
外汇与贵金属品种波动大、滑点和点差风险高,这种自卸式脚本绝不能直接用于真实账户,仅在策略测试器里确认类调用链路通不通。
class=class="str">"cmt">//+------------------------------------------------------------------+ class CGoodByeWorld : class="kw">public CGoodByeWorldBase { }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <MQLx-Intro\Base\GoodByeWorldBase.mqh> CGoodByeWorld hello; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 逆初函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 分时处理函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- hello.Greeting("你好 ","世界!"); hello.GoodBye(); ExpertRemove(); } class=class="str">"cmt">//+------------------------------------------------------------------+
跨平台移植绕不开的两道坎
用同一套逻辑写跨平台 EA,多数时候能省下不少重复劳动,但有两个硬限制必须先看清,否则写到一半会卡死。 第一道坎在 MetaTrader 4 本身。MT4 是更老的平台,缺了 MT5 原生就带的一些功能。EA 里一旦用到 MT5 专属接口,在 MT4 端就得单独写定制方案补位;反过来 MT4 的功能大多在 MT5 有对应或简易变通,所以 MT4 用户顾虑相对少。 第二道坎是两个平台约定和执行差异大,交易操作上尤其明显。开发者得主动选一种约定做基准,比如拿 MT4 的下单逻辑翻译到 MT5 以保持行为一致,或者反过来把 MT5 的挂单习惯硬塞进 MT4 程序。外汇与贵金属本身高风险,这种翻译层若漏掉滑点或成交规则差异,回测和实盘可能偏离。 开 MT5 随便建个 EA,故意调用 MT5 的 CTrade 类,再试着在 MT4 编译——报错的那一刻你就知道边界在哪了。
「基类拆子类的落地边界」
把共享逻辑塞进一个基类、差异部分丢给子类,这套写法在跨平台 EA 上确实能少写一遍并行代码,维护时只动一处。但社区里有人点过:光靠 #ifdef __MQL5__ 和继承分层还不够,真要稳得在交易 API 和数据访问层就做 OO 替换,否则遇到订单管理、停止位演化还是会返工。 附件 MQLx-Intro.zip 约 69.5 KB,里面是文中提到的基类与子类示例工程。开 MT5 把 zip 解到 MQL5/Experts 下编译一遍,重点看 CExpertAdvisor 基类和对应 MQL4 子类的字段映射,比读文字直观。 外汇与贵金属杠杆高、滑点随机,跨平台工程省的是开发时间,不替你扛实盘风险。跑通编译后先上策略测试器用历史数据验差异分支,再谈接真实账户。