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