调试 MQL5 程序·进阶篇
🐞

调试 MQL5 程序·进阶篇

(2/3)· 从编译器警告到调试程序逐步执行,多数自学者卡在忽视黄色提示的那一步

偏理论进阶 第 2/3 篇
把 = 写成 == 的疏忽,编译器只会给黄色警告而非红色错误,程序照常跑却逻辑全歪。不少写过几百行 MQL5 的人从没认真看过 Errors 选项卡以外的警告行。先学会让编译器替你抓粗心,比盲目加 Print 更有用。

调试时怎么把变量看个明白

写 EA 最怕黑盒运行。Print 和 Comment 是 MT5 里随手就能用的两个调试口子:前者把参数当文本丢进日志和 Experts 选项卡,左侧自动带时间和程序名;后者直接把字糊在图表左上角,不用切窗口。老从 MQL4 转过来的人基本都认得这俩。 光看值不够,有时你得知道是谁在调、按什么签名调。__FUNCTION__ 返回函数名,__FUNCSIG__ 连参数列表一起返。重载函数名字一样参数不同,用 __FUNCSIG__ 才能分得清。下面这段代码演示了基础用法: 高频循环里别每条都打印,不然日志炸了也看不出名堂。给 Print 套个条件,比如跑满 1013 次迭代再输出一次,就能只盯关键帧。Comment 同理,不过它每次调用会覆盖上一条——算缺点也算优点,看你想不想留痕。 想留全量痕迹,就声明个全局 string 变量,每轮把新串压到开头、旧值接末尾,再喂给 Comment。这样图表上能滚着看历史。真要查调用链或存大量数据,Print/Comment 都可能跟不上:Print 有时来不及显变化,Comment 本身慢还不可重读。落到文件最稳,但写文件放 OnDeinit 里做一次性落盘,而拼字符串变量是每次迭代都干。 日志太胖会拖死编辑器。经验上每攒 100–200 万条非频繁条目,就把内容搬进 string 数组单元格、把 com 归零开下一段。这样既能躲开变量溢出丢数据,也能分多个文件看而不是开一个巨无霸。别在每次迭代都走「打开-写-关」整圈,我实测这么搞硬盘几个月就废;程序若崩在零除上,还可能留个没关的文件卡系统。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example of displaying data for debugging                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void myfunc(class="type">int a)
  {
   Print(__FUNCSIG__); class=class="str">"cmt">// display data for debugging 
class=class="str">"cmt">//--- here is some code of the function itself
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example of data output for debugging                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void myfunc(class="type">int a)
  {
class=class="str">"cmt">//--- declare the class="kw">static counter
   class="kw">static class="type">int cnt=class="num">0;
class=class="str">"cmt">//--- condition for the function call
   if(cnt==class="num">1013)
      Print(__FUNCSIG__," a=",a); class=class="str">"cmt">// data output for debugging
class=class="str">"cmt">//--- increment the counter
   cnt++;
class=class="str">"cmt">//--- here is some code of the function itself
  }
class="type">class="kw">string com=""; class=class="str">"cmt">// declare the global variable for storing debugging data
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example of data output for debugging                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void myfunc(class="type">int a)
  {
class=class="str">"cmt">//--- declare the class="kw">static counter
   class="kw">static class="type">int cnt=class="num">0;
class=class="str">"cmt">//--- storing debugging data in the global variable
   com=(__FUNCSIG__+" cnt="+(class="type">class="kw">string)cnt+"\n")+com;
   Comment(com); class=class="str">"cmt">// вывод информации для отладки
class=class="str">"cmt">//--- increase the counter
   cnt++;
class=class="str">"cmt">//--- here is some code of the function itself
  }
class="type">class="kw">string com=""; class=class="str">"cmt">// declare the global variable for storing debugging data
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Program shutdown                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- saving data to the file when closing the program
   WriteFile();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example of data output for debugging                             |

◍ 用静态计数器和文件落盘抓函数调用轨迹

在 MT5 的 EA 或脚本里排错,最怕函数被反复调用却看不见次数。下面这段把 static 局部变量和文件写出结合,能直接落地验证。 myfunc 里声明了 static int cnt=0,静态变量只初始化一次,后续每次进函数 cnt 自增,因此它能真实反映调用频次。同时把 __FUNCSIG__(函数签名宏)和当前 cnt 拼进全局字符串 com,相当于打了带次数的调用日志。 WriteFile 负责把 com 落盘:用 FileOpen 开同名 txt,失败就 Print 错误码。注意原文示例里文件打开后连续写了 4 次 com 又关句柄,这是刻意演示重复写——实际调试里重复写会污染日志,你验证时改成一写一关更干净。 外汇与贵金属品种波动大、点差跳变频繁,这类调试输出别挂在实时订单逻辑里跑,先用模拟盘或策略测试器验证,避免日志 IO 拖慢 tick 处理。

MQL5 / C++
class="type">void myfunc(class="type">int a)
  {
class=class="str">"cmt">//--- declare the class="kw">static counter
   class="kw">static class="type">int cnt=class="num">0;
class=class="str">"cmt">//--- storing debugging data in the global variable
   com+=__FUNCSIG__+" cnt="+(class="type">class="kw">string)cnt+"\n";
class=class="str">"cmt">//--- increment the counter
   cnt++;
class=class="str">"cmt">//--- here is some code of the function itself
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save data to file                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void WriteFile(class="type">class="kw">string name="Отладка")
  {
class=class="str">"cmt">//--- open the file
   ResetLastError();
   class="type">int han=FileOpen(name+".txt",FILE_WRITE|FILE_TXT|FILE_ANSI," ");
class=class="str">"cmt">//--- check if the file has been opened
   if(han!=INVALID_HANDLE)
     {
       FileWrite(han,com); class=class="str">"cmt">// печать данных
       FileClose(han);     class=class="str">"cmt">// закрытие файла
     }
   else
     Print("File open failed "+name+".txt, error",GetLastError());
  }
class=class="str">"cmt">//--- open the file
class="type">int han=FileOpen("Debugging.txt",FILE_WRITE|FILE_TXT|FILE_ANSI," ");
class=class="str">"cmt">//--- print data
if(han!=INVALID_HANDLE) FileWrite(han,com);
if(han!=INVALID_HANDLE) FileWrite(han,com);
if(han!=INVALID_HANDLE) FileWrite(han,com);
if(han!=INVALID_HANDLE) FileWrite(han,com);
class=class="str">"cmt">//--- close the file
if(han!=INVALID_HANDLE) FileClose(han);

「用策略测试器逼出稀缺触发条件」

调试 EA 时最磨人的不是逻辑写错,而是某些交易条件在实盘里极少出现。实时模式跑 EA,等几个月才可能撞上一次目标环境,这成本显然不划算。 策略测试器(Strategy Tester)能直接解决这个问题:调试手段还是 Print 和 Comment,Comment 负责在图表上即时反馈当前评估状态,Print 把更细的过程写进测试代理各自的日志目录,方便事后翻查。 做法是把时间本地化后,在测试器里手动框定含目标行情的日期区间,开可视化模式逐 tick 跑。这样能在几分钟内复现实盘中几个月才出现一次的触发场景。 这套思路最早从 MT4 沿用下来——那时它几乎是运行期排查逻辑的唯一办法,MT5 测试器只是把代理日志和可视化做得更顺手。外汇与贵金属市场高杠杆、跳空频繁,回测通过不代表实盘概率一致,验证时务必用多段样本期交叉跑。

用类型名和指针号揪出 OOP 调用链

MQL5 引入面向对象后,调试的痛点从‘找函数’变成了‘找是谁在调’。尤其当类做成纵向继承,光看函数名不够,你得知道当前执行的是基类还是某个衍生类的实例。 模板函数 GetTypeName 能把任意指针类型转成字符串,这给了我们一个低成本的埋点手段。基类 CFirst 在构造函数里把 m_typename 填成自身类型,衍生类 CSecond 覆盖同一个变量——调用时顺手把 m_typename 带出来,就能同时拿到‘函数名 + 对象类型’两条信息。 光有类型还不够区分同类的多个实例。把指针转成字符串打印,等于给每个对象发了一个编号。类内部用 (string)this,类外部用 (string)GetPointer(pointer),谁在跑一目了然。 如果给构造函数传一个对象名参数,把 m_typename 的填法照搬到名字变量上,调试输出就从‘2号未知对象’变成‘2号欧元仓位管理器’。外汇和贵金属 EA 里对象一多,这种命名法能省掉大量对着编号猜身份的時間,但 OOP 调试本身不改交易逻辑,实盘前仍需在 MT5 策略测试器里跑通继承链。

MQL5 / C++
class="kw">template<class="kw">typename T> class="type">class="kw">string GetTypeName(const T &t) { class="kw">return(class="kw">typename(T)); }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Base class contains the variable for storing the type              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CFirst
  {
class="kw">public:
   class="type">class="kw">string            m_typename; class=class="str">"cmt">// variable for storing the type
   class=class="str">"cmt">//--- filling the variable by the custom type in the constructor
                    CFirst(class="type">void) { m_typename=GetTypeName(this); }
                   ~CFirst(class="type">void) { }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Derived class changes the value of the base class variable         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSecond : class="kw">public CFirst
  {
class="kw">public:
   class=class="str">"cmt">//--- filling the variable by the custom type in the constructor
                    CSecond(class="type">void) { m_typename=GetTypeName(this); }
                   ~CSecond(class="type">void) {  }
  };
Print((class="type">class="kw">string)this); class=class="str">"cmt">// print pointer number inside the class
Print((class="type">class="kw">string)GetPointer(pointer)); class=class="str">"cmt">// print pointer number outside the class

◍ 用文件落地看清函数调用链

调试 EA 时,打印日志看顺序只是浅层手段;真遇到逻辑死结,得把整个调用结构扒出来。追踪(tracing)不流行,是因为搭起来麻烦,但它能让你在 MetaEditor 里直接看到谁调了谁、依什么顺序。 最省事的起点是俩宏:zx 在函数头插入,xz 在函数尾或中途 return 前插入。下例里 zx 展开成 Print(__FUNCSIG__+"{"),xz 展开成 Print("}");,依条件退出时也要补 xz,否则追踪括号会错位。

MQL5 / C++
class=class="str">"cmt">//--- opening substitution  
class="macro">#define zx Print(__FUNCSIG__+"{");
class=class="str">"cmt">//--- closing substitution
class="macro">#define xz Print("}");
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example of function tracing                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void myfunc(class="type">int a,class="type">int b)
  {
   zx
class=class="str">"cmt">//--- here is some code of the function itself
   if(a!=b) { xz class="kw">return; } class=class="str">"cmt">// exit in the middle of the function
class=class="str">"cmt">//--- here is some code of the function itself
   xz class="kw">return;
  }
上面这套只打到终端,嵌套一多就糊。更狠的做法是把调用拼进一个全局 string,程序退出时写进 .mqh 文件。文件带 .mqh 后缀,能在 MetaEditor 里用 Ctrl+, 风格化,嵌套结构立刻展开成可折叠的代码树。
MQL5 / C++
class="type">class="kw">string com=""; class=class="str">"cmt">// declare global variable for storing debugging data
class=class="str">"cmt">//--- opening substitution
class="macro">#define zx com+="if("+__FUNCSIG__+"){\n";
class=class="str">"cmt">//--- closing substitution
class="macro">#define xz com+="};\n"; 
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Program shutdown                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- //--- saving data to the file when closing the program
   WriteFile();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example of the function tracing                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void myfunc(class="type">int a,class="type">int b)
  {
   zx
class=class="str">"cmt">//--- here is some code of the function itself
   if(a!=b) { xz class="kw">return; } class=class="str">"cmt">// exit in the middle of the function
class=class="str">"cmt">//--- here is some code of the function itself
   xz class="kw">return;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save data to file                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void WriteFile(class="type">class="kw">string name="Tracing")
  {
class=class="str">"cmt">//--- open the file
   ResetLastError();
   class="type">int han=FileOpen(name+".mqh",FILE_WRITE|FILE_TXT|FILE_ANSI," ");
class=class="str">"cmt">//--- check if the file has opened
   if(han!=INVALID_HANDLE)
     {
      FileWrite(han,com); class=class="str">"cmt">// print data
      FileClose(han);     class=class="str">"cmt">// close the file
     }
   else
想只追某段事件,把宏里加个 bool trace 判断即可:trace=true 才拼字符串,false 就空跑。暂时没空清理源码时,把宏定义直接改成空值,全部追踪瞬间禁用,不留痕迹。 标准 EA 挂上这套后,图表跑起来,Files 目录会生成 tracing.mqh;刚生成时嵌套不明显,按一次 Ctrl+, 就显出整体骨架。外汇与贵金属 EA 调试涉及实盘逻辑,高杠杆下误判调用顺序可能引发行情错单,追踪至少能把‘程序到底走了哪条路’这件事坐实。

MQL5 / C++
class=class="str">"cmt">//--- opening substitution  
class="macro">#define zx Print(__FUNCSIG__+"{");
class=class="str">"cmt">//--- closing substitution
class="macro">#define xz Print("}");
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example of function tracing                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void myfunc(class="type">int a,class="type">int b)
  {
   zx
class=class="str">"cmt">//--- here is some code of the function itself
   if(a!=b) { xz class="kw">return; } class=class="str">"cmt">// exit in the middle of the function
class=class="str">"cmt">//--- here is some code of the function itself
   xz class="kw">return;
  }
if() {...}
class="type">class="kw">string com=""; class=class="str">"cmt">// declare global variable for storing debugging data
class=class="str">"cmt">//--- opening substitution
class="macro">#define zx com+="if("+__FUNCSIG__+"){\n";
class=class="str">"cmt">//--- closing substitution
class="macro">#define xz com+="};\n"; 
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Program shutdown                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- //--- saving data to the file when closing the program
   WriteFile();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example of the function tracing                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void myfunc(class="type">int a,class="type">int b)
  {
   zx
class=class="str">"cmt">//--- here is some code of the function itself
   if(a!=b) { xz class="kw">return; } class=class="str">"cmt">// exit in the middle of the function
class=class="str">"cmt">//--- here is some code of the function itself
   xz class="kw">return;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save data to file                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void WriteFile(class="type">class="kw">string name="Tracing")
  {
class=class="str">"cmt">//--- open the file
   ResetLastError();
   class="type">int han=FileOpen(name+".mqh",FILE_WRITE|FILE_TXT|FILE_ANSI," ");
class=class="str">"cmt">//--- check if the file has opened
   if(han!=INVALID_HANDLE)
     {
      FileWrite(han,com); class=class="str">"cmt">// print data
      FileClose(han);     class=class="str">"cmt">// close the file
     }
   else
把编译警告交给小布盯盘看
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到常见 MQL5 逻辑隐患提示,你专注策略本身而非低级拼写。

常见问题

语法错误以红色显示,不修正就无法生成可执行文件;警告以黄色显示,代码仍能编译,但运行行为可能偏离预期,例如赋值号误用为比较号。
可以,小布盯盘内置的 AIGC 诊断会标注常见警告模式,如误用 = 与 ==,不过最终仍以 MetaEditor 编译结果为准,外汇贵金属交易本身高风险。
若编译阶段有未处理的警告,调试程序的逐步执行会基于有逻辑偏差的字节码运行,定位到的现象未必是真实策略错误。
类与对象的封装会让调用栈变深,断点需落在成员函数内部,倾向结合交互输出才能看清状态流转,概率上比普通函数更易漏看构造期错误。