跨平台智能交易程序: 概论·进阶篇
🔀

跨平台智能交易程序: 概论·进阶篇

(2/3)· 双写 EA 浪费的不只是时间,更埋下两版逻辑漂移的隐患

进阶 第 2/3 篇
很多开发者习惯先写通 MQL5 再重抄一遍 MQL4,以为只是换语法。实际两版并行维护时,微小差异会悄悄演变成回测与实盘行为不一致,排查成本极高。

「用条件编译抹平 MT4 与 MT5 的字符串拼接差异」

MQL4 和 MQL5 共享不少语法,但 StringConcatenate 的行为直接分裂。MT4 里它返回拼接后的字符串,MT5 里同名函数却返回拼接文本的长度整数——同一段调用在 MT4 终端打印 "Hello World!",在 MT5 上只打印 "12"。 若想一套代码双平台跑出相同结果,不必重写主体逻辑,直接用预处理指令做条件编译即可。编译时按平台选取不同分支,会带来极轻微的编译开销,但执行期零额外负担。 MT4 编译器只认 #ifdef __MQL4__ 里的字符串返回版本,MT5 编译器则走 #else 分支用长度返回版本。你可以在头文件里重载 Greeting(),传入两个 string 参数,靠条件编译决定内部拼接方式,终端输出就能保持一致。 外汇与贵金属交易终端的高频重编译场景里,这种预处理差异不会影响实盘 tick 处理延迟,但跨平台移植 EA 时务必自测打印结果。

MQL5 / C++
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 前都需先在策略测试器回测,杠杆品种价格波动可能触发意外平仓。

MQL5 / C++
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)直接调用会编译失败,移植前先列一张「单边存在函数」清单,能省掉后期大量重写的坑。

MQL5 / C++
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 多次就可能触发重复定义或初始化顺序坑。真要复用,基类放声明、派生类放实现,比全堆一起干净。

MQL5 / C++
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 触发时机偏移的高风险。

MQL5 / C++
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 只留一行包含,编译通过即说明版本隔离生效;外汇与贵金属交易自带高杠杆高风险,任何代码仅作技术验证,不代表实盘收益。

MQL5 / C++
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">//+------------------------------------------------------------------+
把双端编译差异交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到跨平台 EA 的编译状态与版本偏差提示,你只需专注策略逻辑本身。

常见问题

编译器按扩展名决定产物,MQ4 出 EX4、MQ5 出 EX5,语法与内置函数集不同,无法用同一份源跨平台产出。
可以,小布盯盘内置的 AIGC 能标记 MT4 与 MT5 版本间的实现偏差,减少人工比对头文件与源文件的时间。
通过 #ifdef __MQL5__ 等宏隔离两端特有 API,共享逻辑放公共段,平台相关调用各自包裹,降低重复代码。
拆分实现是按功能隔离平台相关代码,拆分目录则是把各端源、头、资源分路径管理,前者重逻辑后者重工程结构。