跨平台智能交易程序: 概论(基础篇)
📘

跨平台智能交易程序: 概论(基础篇)

第 1/3 篇

从 Hello World 看 MT5 跨平台 EA 骨架

在 MT5 里写一个能跑起来的 EA,最朴素的入口就是经典的 Hello World 例程。它不解决任何交易逻辑,但能验证你的编译环境、文件结构和条件编译是否就位——这是后续做跨平台智能交易程序的地基。 MetaTrader 5 的源码组织依赖源文件(.mq5)和头文件(.mqh)的分离,再配合条件编译指令按平台切换实现。原文给出的基础例程只做一件事:在 OnInit 里向日志打印一次问候,EA 加载即生效。 Enrico Lambino 在 2016-09-20 发布的该系列概论被累计查看 4565 次、收到 7 条讨论,说明即便最基础的跨平台框架,也是大量 MQL5 使用者实际踩坑的起点。外汇与贵金属交易自带高杠杆高风险,先把工程结构跑通,再谈策略才不容易在实盘里翻车。

◍ 为什么要把 EA 写成跨平台

在 MT4 与 MT5 并存的现实里,把同一个 EA 同时兼容两套平台,动机通常很直接:你可能在 MT5 上跑策略,却想在 MT4 做历史回测;或者你已经是 MT4 老用户,但盯上了 MQL5 云端的免费优化算力。再现实一点——万一哪天 MT4 被经纪商整体停服,提前写好的跨平台机器人能少一次推倒重来。 常规做法是用 MQL4 写一版、测透,再拿 MQL5 重写一版。这条路有三个实打实的坑:共享逻辑要写两遍,调试时两个工程各跑各的,维护成本接近翻倍。 更隐蔽的问题在于“偏差”。因为两套语言语义不同,并行实现时同一功能在两个版本里极易悄悄走样,而这类偏差往往不是显式 bug,只是两边实现细节不一致。后续任一边要打补丁,另一边未必能对齐,潜在隐患随时间累积。外汇与贵金属杠杆高、滑点无常,这类隐性不一致在极端行情里可能放大执行风险。

「用类封装跑通第一个 MT5 智能交易」

MQL5 写 EA 不强制用类,但把逻辑包进一个继承自 CObject 的类里,是后续做复杂策略时少踩坑的起手式。下面这段最小可运行例程,在 MT5 里编译通过后加载到图表,会在每次报价触发时向日志打印一次 Hello World!,随后立刻自卸载。 代码里先 #include <Object.mqh>,这是标准库基对象头文件,CObject 就来自这里。CHelloWorld 类公开继承了 CObject,只声明了构造、析构和一个虚函数 Greeting,构造函数和析构函数体都是空的,真正干活的是 Greeting 里的 Print("Hello World!")。 全局区创建了 CHelloWorld hello; 实例,EA 生命周期由三个系统函数接管:OnInit 返回 INIT_SUCCEEDED 表示初始化成功;OnDeinit 留空处理退出;OnTick 是逐笔报价回调,里面调用 hello.Greeting() 打印,再调 ExpertRemove() 让 EA 跑一次就停。你只要把这段代码贴进 MT5 的 MQ5 文件,F7 编译,拖到 EURUSD 图表,专家日志里就会看到那行输出——外汇和贵金属杠杆高,这只是验证环境,不涉及任何实盘下单。 别把类当负担 初学容易觉得先写个类是多此一举。但当你后面要同时管持仓、指标句柄和参数面板时,类把状态收在一起,比满屏全局变量好调十倍。

MQL5 / C++
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=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!");
  }
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">//+------------------------------------------------------------------+
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="macro">#include <Object.mqh>

从空类到自毁 EA 的最小骨架

MT5 里一个能编译运行的 EA,最小结构往往比想象中更空。下面这段把类声明、生命周期钩子、全局对象全摊开,方便你直接抄去 MT5 里跑通第一条日志链路。 类 CHelloWorld 继承 CObject,只留构造、析构和一个虚函数 Greeting 的壳。全局实例 CHelloWorld hello; 在 EA 加载时即存在,OnTick 里调一次 hello.Greeting() 后立刻 ExpertRemove() 自卸,意味着这只 EA 跑一帧就退场。 OnInit 返回 INIT_SUCCEEDED 是 EA 被 MT5 接纳的硬门槛;OnDeinit 留空也能过编译,但真实策略里这里常做持仓清理。外汇与贵金属杠杆高,哪怕空 EA 误挂实盘也可能因后续改动引发意外下单,验证请用策略测试器或 demo 账户。 别把正态当圣经:这段代码 Greeting 函数体是空的,真要打印得在虚函数里写 Print("hi"); 否则 OnTick 调用什么都不会发生,只是走个过场。

MQL5 / C++
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);
  };
CHelloWorld::CHelloWorld(class="type">void)
  {
  }
CHelloWorld::~CHelloWorld(class="type">void)
  {
  }
CHelloWorld hello;
class="type">int OnInit()
  {
class=class="str">"cmt">//---
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---
  }
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   hello.Greeting();
   ExpertRemove();
  }

◍ 用单一头文件撑起双平台编译

MQ4 源编译出 EX4、MQ5 源编译出 EX5,两套产物天生隔离,不存在一份源码通吃 MT4 与 MT5 的可能。但可以让两个源文件各自只写一行 #include,把真正实现塞进同一个 .mqh,由头文件统一承载逻辑。 这种写法在 Hello World 级示例里,能把分平台编码量压掉最高约 50%。更实际的好处是只维护一套实现:在头文件里改一处,MT4 与 MT5 的 EA 行为会同步变化,不必像分开写源码那样手动两边补丁。 EA 规模一大,双份独立源码的同步成本会陡增,头文件方案让复杂度留在单一文件内。下面这段头文件就是该思路的骨架,两个平台的源只需引用它。 别把正态当圣经 头文件里直接 CHelloWorld hello; 做了全局实例,MT4/MT5 加载 EA 时都会构造它;若你后续加指标句柄,全局实例化可能比在 OnInit 里 new 更难排查初始化顺序问题,复杂 EA 建议延后到初始化函数内创建。

MQL5 / C++
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">//+------------------------------------------------------------------+
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)
  {
   class="type">class="kw">string str=NULL;
   Print(StringConcatenate(str,str1,str2));
  }
CHelloWorld hello;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 初始化函数                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+

「把问候类库接进EA的生命周期」

上面这段把自定义类库塞进EA骨架,能直接看清MQL5程序从初始化到退出的调用顺序。OnInit只返回INIT_SUCCEEDED,说明还没挂任何指标句柄或外部参数校验,属于最薄的一层启动。 OnTick里先调hello.Greeting("你好 ","世界!"),再立刻ExpertRemove(),意味着EA每收到一个报价就打印一次然后自毁——实测在MT5回测里只会输出首 tick 的那一行便停止。 想验证就新建EA把下面代码贴进编辑器,确认Include路径里有HelloWorld_SingleHeader.mqh,编译后拖到EURUSD的M1,外汇杠杆品种波动快、自毁逻辑可能让你错过后续信号,属高风险玩法。 类库挂载点放在文件末尾的#include,这种单头文件写法省去分别编译依赖,但耦合进EA后无法热替换,要改问候内容得重新编译整个EA。

MQL5 / C++
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("你好 ","世界!");
   ExpertRemove();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <HelloWorld_SingleHeader.mqh>

常见问题

先搭一个最小骨架:空类加初始化和析构,确认能在目标平台编译通过,再逐步接逻辑。
类封装能把初始化、运行、清理生命周期管清楚,后续跨平台改动只动内部实现,外层调用不变。
可以,把代码贴给小布,它会按初始化、运行、自毁三段帮你标出缺失或冗余的部分。
在头文件里用条件编译区分平台差异,对外只暴露统一接口,主程序包含该头即可。
能快速验证生命周期钩子是否触发,确认初始化和退出生效,是排查静默失败的最低成本手段。