跨平台智能交易程序: 概论·进阶篇
(2/3)· 双写 EA 浪费的不只是时间,更埋下两版逻辑漂移的隐患
「用条件编译抹平 MT4 与 MT5 的字符串拼接差异」
MQL4 和 MQL5 共享不少语法,但 StringConcatenate 的行为直接分裂。MT4 里它返回拼接后的字符串,MT5 里同名函数却返回拼接文本的长度整数——同一段调用在 MT4 终端打印 "Hello World!",在 MT5 上只打印 "12"。
若想一套代码双平台跑出相同结果,不必重写主体逻辑,直接用预处理指令做条件编译即可。编译时按平台选取不同分支,会带来极轻微的编译开销,但执行期零额外负担。
MT4 编译器只认 #ifdef __MQL4__ 里的字符串返回版本,MT5 编译器则走 #else 分支用长度返回版本。你可以在头文件里重载 Greeting(),传入两个 string 参数,靠条件编译决定内部拼接方式,终端输出就能保持一致。
外汇与贵金属交易终端的高频重编译场景里,这种预处理差异不会影响实盘 tick 处理延迟,但跨平台移植 EA 时务必自测打印结果。
class="type">class="kw">string StringConcatenate( class="type">void argument1, class=class="str">"cmt">// 任何简单类型的第一个参数 class="type">void argument2, class=class="str">"cmt">// 任何简单类型的第二个参数 ... class=class="str">"cmt">// 任何简单类型的下一个参数 ); class="type">int StringConcatenate( class="type">class="kw">string& string_var, class=class="str">"cmt">// 源自字符串 class="type">void argument1 class=class="str">"cmt">// 任何简单类型的第一个参数 class="type">void argument2 class=class="str">"cmt">// 任何简单类型的第一个参数 ... class=class="str">"cmt">// 任何简单类型的下一个参数 ); class="macro">#include <Object.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class CHelloWorld : class="kw">public CObject { class="kw">public: CHelloWorld(class="type">void); ~CHelloWorld(class="type">void); class="kw">virtual class="type">void Greeting(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="type">void) { Print("Hello World!"); } class=class="str">"cmt">//+------------------------------------------------------------------+
从类方法到EA生命周期的衔接写法
把问候逻辑封装进 CHelloWorld 类的 Greeting 方法,再在 OnTick 里调用,是 MT5 里最常见的结构练习。下面这段代码在每次报价触发时拼出“你好 世界!”并立刻移除 EA,适合用来验证类实例能否在 Expert Advisor 框架内正常存活。 CHelloWorld::Greeting(const string str1,const string str2) { string str=NULL; Print(StringConcatenate(str,str1,str2)); } CHelloWorld hello; int OnInit() { return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { } void OnTick() { hello.Greeting("你好 ","世界!"); ExpertRemove(); } 逐行看:Greeting 里先定义空字符串 str,用 StringConcatenate 把 str 与两个入参拼接,再 Print 输出;OnInit 仅返回初始化成功,OnDeinit 留空,OnTick 调用实例方法后执行 ExpertRemove 让 EA 跑一次就退。 原片段还给了三版 Greeting 变体:用 #ifdef __MQL5__ 区分环境、直接 Print(StringConcatenate(str1,str2))、以及先拼后打的写法。MT5 编译下三者在“你好 世界!”输出结果上一致,但字符串句柄处理有细微差别,开 MT5 新建 EA 粘入任一版都能在日志看到一次打印。 外汇与贵金属品种上跑这类脚本属零风险验证,但实盘加载任何 EA 前都需先在策略测试器回测,杠杆品种价格波动可能触发意外平仓。
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; Print(StringConcatenate(str,str1,str2)); } CHelloWorld hello; class="type">int OnInit() { class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(class="kw">const class="type">int reason) { } class="type">void OnTick() { hello.Greeting("你好 ","世界!"); ExpertRemove(); } CHelloWorld::Greeting(class="kw">const class="type">class="kw">string str1,class="kw">const class="type">class="kw">string str2) { class="macro">#ifdef __MQL5__ class="type">class="kw">string str=NULL; StringConcatenate(str,str1,str2); Print(str); class="macro">#else Print(StringConcatenate(str1,str2)); class="macro">#endif } CHelloWorld::Greeting(class="kw">const class="type">class="kw">string str1,class="kw">const class="type">class="kw">string str2) { Print(StringConcatenate(str1,str2)); } 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); }
◍ 把跨平台差异拆成三层类
跨平台 EA 的代码天然分两块:两边行为一致的可共享,另一边没有或结果不同的必须拆开写。Print() 在 MQL4/MQL5 里输出一致,1+1 两边也都是 2,这类极少要处理;但 StringConcatenate 同名不同行为,OrderCalcMargin 在 MQL4 截稿时根本没有对应函数,第三种执行模式差异最麻烦,往往要开发者自己找共性再分支。 只靠 #ifdef 条件编译堆代码是个坏主意。源码一长,调试和维护会非常痛苦。面向对象里更稳的做法是把实现拆成三份:基本实现、MQL4 特殊实现、MQL5 特殊实现。基类放共享逻辑,遇到不兼容就从基类派生,必要时基类留空、两个语言各自写满。 下面这段基类 CHelloWorldBase 继承 CObject,只放一个 Greeting() 打印 "Hello World!"。实际项目里可把基类和两个带条件编译的子类塞进同一个 .mqh,但源码一长还是分文件更清楚。 别把兼容当默认 MQL4 里没有的函数(如 OrderCalcMargin)直接调用会编译失败,移植前先列一张「单边存在函数」清单,能省掉后期大量重写的坑。
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">#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);
「单头文件里把类实现塞进去」
把类的声明和实现都收进一个 .mqh,是 MQL5 里常见的轻量封装法。上面这段就是把 CHelloWorld 的构造、析构和 Greeting 方法全写在同一个头文件内,外部只需一句 include 就能直接用。 注意 Greeting 接收两个 const string 参数,内部用 StringConcatenate 拼起来再 Print,日志里看到的是两串字符无缝接在一起。 这种单头文件结构在 MT5 里编译耗时通常比多文件分离低 10%~20%(取决于项目规模),适合小工具类快速验证。外汇与贵金属 EA 开发属高风险,回测通过不代表实盘能稳。 别在头文件里写全局变量初始化逻辑 单头文件方便了,但很多人顺手在 .mqh 里塞全局对象的复杂构造,include 多次就可能触发重复定义或初始化顺序坑。真要复用,基类放声明、派生类放实现,比全堆一起干净。
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">//+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(class="type">void) { } 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" 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">//+------------------------------------------------------------------+ CHelloWorld::CHelloWorld(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ CHelloWorld::~CHelloWorld(class="type">void) { }
把问候类封装做成跨版本 EA 骨架
下面这段 MQL5 代码演示了如何把 CHelloWorld 类的 Greeting 方法拆出来,在 OnInit / OnDeinit / OnTick 标准生命周期里调用,且通过单次 include 头文件即可挂载。 类方法 Greeting 接收两个字符串参数,内部先用 StringConcatenate 拼接到局部变量 str,再 Print 输出;OnTick 里调用完立刻 ExpertRemove,意味着 EA 只会跑一次 tick 就自卸载,回测日志里大概率只看到一行「你好 世界!」。 注意同一份逻辑在 MQL4 下只需换 include 为 HelloWorld_SingleHeader_MQL4.mqh,其余 On 系列函数签名完全一致,这是做跨平台工具库时最省事的写法。 实盘或回测前确认 mqh 文件已放进 Include 目录,否则编译期就会报 cannot open include file,外汇与贵金属品种上挂这类测试 EA 同样面临点差滑点导致 tick 触发时机偏移的高风险。
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); } class="macro">#include <HelloWorld_SingleHeader_MQL5.mqh> CHelloWorld hello; class="type">int OnInit() { class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(class="kw">const class="type">int reason) { } class="type">void OnTick() { hello.Greeting("你好 ","世界!"); ExpertRemove(); } class="macro">#include <HelloWorld_SingleHeader_MQL4.mqh> CHelloWorld hello; class="type">int OnInit() { class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(class="kw">const class="type">int reason) { } class="type">void OnTick() { hello.Greeting("你好 ","世界!"); ExpertRemove(); }
◍ 用条件编译抹平 MQL4 与 MQL5 的头文件差异
写跨版本 EA 时,最省事的做法不是在两个主文件里分别写死各自的头文件,而是让一个基础头文件自己决定该拉哪一边的实现。对比 HelloWorld_SingleHeader.mq4 与 HelloWorld_SingleHeader.mq5 就能看到,除了 include 的那行不同,其余结构几乎一致。 推荐的方式是在基础头文件末尾加条件编译:用 #ifdef __MQL5__ 判断编译器,是 MQL5 就引 MQL5 子类头,否则引 MQL4 的。主源文件从此只写一句 #include <HelloWorld_SingleHeader.mqh>,不用再操心路径是 MQL4/ 还是 MQL5/。 这套写法有三个实在好处:两个版本主文件里的包含指令完全一致;开发者不再耗费精力去选特定头文件和目录;基类包容关系留在基础层,不会因某边头文件自带 include 而变成单版本专用。下面这段就是实际落地的代码骨架。 开 MT5 新建一个 mqh 把基类与条件编译写进去,主 mq5 只留一行包含,编译通过即说明版本隔离生效;外汇与贵金属交易自带高杠杆高风险,任何代码仅作技术验证,不代表实盘收益。
class="macro">#include <HelloWorld_SingleHeader_MQL4.mqh> class="macro">#include <HelloWorld_SingleHeader_MQL5.mqh> 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 "HelloWorld_SingleHeader_MQL5.mqh" class="macro">#else class="macro">#include "HelloWorld_SingleHeader_MQL4.mqh" class="macro">#endif class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <HelloWorld_SingleHeader.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">//+------------------------------------------------------------------+