源代码的跟踪、调试和结构分析·进阶篇
🌲

源代码的跟踪、调试和结构分析·进阶篇

(2/3)· 当EA逻辑嵌套三层函数与循环,靠Print已看不见真实执行顺序,本文给出树形栈方案

偏理论进阶 第 2/3 篇
多数人调试MQL5只在关键处塞Print,函数一嵌套就分不清谁先谁后。一旦OnTick里套了自定义类方法再调指标,执行路径在日志里变成一团乱麻。用调用栈树替代散点日志,才能看清真实进入离开顺序。

用 CTraceCtrl 搭一个调用链跟踪器

MQL5 里想看清 EA 或指标的函数调用层级,可以自己挂一个轻量 tracer。核心类是 CTraceCtrl,它内部用四棵 CTreeCtrl 分别存调用栈(m_stack)、信息(m_info)、按文件分组(m_file)、按类分组(m_class),外加一个 m_cur 指针指向当前节点。 构造时直接 CTraceCtrl() { Create(); Reset(); } 就会建好四棵树并把 m_cur 指到根;析构里 delete 掉四棵树的指针,避免 MT5 策略测试器里反复加载造成内存残留。 实际打点用 CIn 类最省事:进入函数时 CIn.In(__FILE__,__LINE__,func_name),它先判 m_trace / m_tree / m_root 非空,再调 m_trace.In(afile,aline,afunc,-1);离开作用域时 ~CIn() 自动 m_trace.Out(-1) 回退一层。这样在 MT5 里跑一遍,就能在 m_stack 中看到完整的进出栈序列,验证某段订单逻辑是否按预期路径执行。 如果只要特定节点暂停,可调用 Break() 或 BreakIt(id,state,root),传入节点 id 和状态;StepBack() 则把 m_cur 上移一层到父节点,方便单步回溯。外汇与贵金属品种波动剧烈,这类跟踪代码仅用于本地调试,上线前务必清掉或加开关,否则高频日志会拖慢实盘 tick 处理。

MQL5 / C++
class="type">bool      BreakIt(class="type">int id,class="type">bool state,CNode *root=NULL); class=class="str">"cmt">// change the m_break state, search is started from a specified node
   class="type">void      BreakBy(class="type">int mode,CNode *node,class="type">bool state,CNode *root=NULL); class=class="str">"cmt">// set only for a selected one 
   class=class="str">"cmt">//--- operations with nodes
class="kw">public:
   class="type">void      SortBy(class="type">int mode,class="type">bool ascend,CNode *root=NULL); class=class="str">"cmt">// sorting by a class="kw">property
   class="type">void      GroupBy(class="type">int mode,CTreeCtrl *atree,CNode *node=NULL); class=class="str">"cmt">// grouping by a class="kw">property
   };
class CTraceView; class=class="str">"cmt">// provisional declaration
class=class="str">"cmt">//------------------------------------------------------------------    class CTraceCtrl
class CTraceCtrl
  {
class="kw">public:
  CTreeCtrl   *m_stack; class=class="str">"cmt">// object of graph
  CTreeCtrl   *m_info; class=class="str">"cmt">// object of graph
  CTreeCtrl   *m_file; class=class="str">"cmt">// grouping by files
  CTreeCtrl   *m_class; class=class="str">"cmt">// grouping by classes
  CTraceView  *m_traceview; class=class="str">"cmt">// pointer to displaying of class
  CNode   *m_cur; class=class="str">"cmt">// pointer to the current node
           CTraceCtrl() { Create(); Reset(); } class=class="str">"cmt">// tracer created
           ~CTraceCtrl() { class="kw">delete m_stack; class="kw">delete m_info; class="kw">delete m_file; class="kw">delete m_class; } class=class="str">"cmt">// tracer deleted
  class="type">void    Create(); class=class="str">"cmt">// tracer created
  class="type">void    In(class="type">class="kw">string afile,class="type">int aline,class="type">class="kw">string aname,class="type">int aid); class=class="str">"cmt">// entering a specified node 
  class="type">void    Out(class="type">int aid); class=class="str">"cmt">// exit from a specified node
  class="type">bool    StepBack(); class=class="str">"cmt">// exit from a node one step higher(going to the parent)
  class="type">void    Reset() { m_cur=m_stack.m_root; m_stack.Reset(); m_file.Reset(); m_class.Reset(); } class=class="str">"cmt">// resetting all nodes
  class="type">void    Clear() { m_cur=m_stack.m_root; m_stack.Clear(); m_file.Clear(); m_class.Clear(); } class=class="str">"cmt">// resetting all nodes
class="kw">public:
  class="type">void    AddWatch(class="type">class="kw">string name,class="type">class="kw">string val); class=class="str">"cmt">// checking the debug mode for a node
  class="type">void    Break(); class=class="str">"cmt">// pause for a node
  };
class=class="str">"cmt">//------------------------------------------------------------------    CIn
class CIn
  {
class="kw">public:
  class="type">void In(class="type">class="kw">string afile,class="type">int aline,class="type">class="kw">string afunc)
    {
     if(NIL(m_trace)) class="kw">return; class=class="str">"cmt">// exit if there is no graph
     if(NIL(m_trace.m_tree)) class="kw">return;
     if(NIL(m_trace.m_tree.m_root)) class="kw">return;
     if(NIL(m_trace.m_cur)) m_trace.m_cur=m_trace.m_tree.m_root;
     m_trace.In(afile,aline,afunc,-class="num">1); class=class="str">"cmt">// entering the next one
    }
  class="type">void ~CIn() { if(!NIL(m_trace)) m_trace.Out(-class="num">1); } class=class="str">"cmt">// exiting higher
  };

「CIn 怎么用进块离块搭出调用树」

CIn 类干的事很单一:在栈上建实例,借构造和析构自动把调用关系挂成一棵栈树。图形不是一次画完,而是靠 CTraceCtrl 的两个函数分两阶段逐步长出来,连续节奏就是 进入-离开-进入-离开-进入-进入-离开-离开 这样的嵌套调用。 进块发生在大括号 '{' 之后。此时新建一个 CIn 实例,它接手当前 CTraceCtrl,并在构造里调 CTraceCtrl::In,于当前节点 m_cur 之下建新节点,把文件名、行号、类名、函数名、当前时间写进去。遇到 '}' 离块时 MQL 自动触发 CIn 析构,析构里调 CTraceCtrl::Out,把 m_cur 指针在树里上抬一级,节点本身留在树上不销毁。 想看真实调用深度,开 MT5 把下面两个声明塞进自己的 CTraceCtrl 包装类,在 EA 里随意嵌几层函数调用,跑一遍就能在树里数出节点层数。外汇与贵金属品种波动大、滑点频繁,这类栈追踪仅用于排查逻辑路径,不预示任何收益。

MQL5 / C++
class="type">void In(class="type">class="kw">string afile, class="type">int aline, class="type">class="kw">string aname, class="type">int aid); class=class="str">"cmt">// entering a specified node
class="type">void Out(class="type">int aid);  class=class="str">"cmt">// exit before a specified node

◍ 用宏把节点追踪代码压成一行

在 MQL5 里反复手写 CIn 对象创建并进入节点的长串代码,既拖慢书写也更容易拼错文件名和行号。直接上调用宏,把 _in.In(__FILE__, __LINE__, __FUNCTION__) 这类追踪逻辑收进 #define,代码清爽不少。 MQL 编译器有个脾气:局部变量名和全局变量名撞车时必弹警告。所以别只定义一个 _IN,多备 _IN1/_IN2/_IN3 用不同变量名,下钻到子块时依次切换,能避开这类编译噪声。

build 411 之后宏支持 `#define` 传参,像 CTraceCtrl 里就用 `NIL(p)` 宏把 `CheckPointer(p)==POINTER_INVALID` 包了一层。原本要写两遍 `CheckPointer(m_tree)` 和 `CheckPointer(m_cur)` 的判空,现在 `if (NIL(m_tree)NIL(m_cur)) return;` 一行搞定,指针有效性检查短了一大截。

下面这段代码就是实际落法:宏展开后等价于手动建对象并打点,复制进 MT5 随便开个 EA 骨架就能编译验证。

MQL5 / C++
class="macro">#define _IN    CIn _in; _in.In(__FILE__, __LINE__, __FUNCTION__)
class="macro">#define _IN1    CIn _in1; _in1.In(__FILE__, __LINE__, __FUNCTION__)
class="macro">#define _IN2    CIn _in2; _in2.In(__FILE__, __LINE__, __FUNCTION__)
class="macro">#define _IN3    CIn _in3; _in3.In(__FILE__, __LINE__, __FUNCTION__)
class="type">bool CSampleExpert::InitCheckParameters(class="type">int digits_adjust)
  { _IN;
class=class="str">"cmt">//--- initial data checks
   if(InpTakeProfit*digits_adjust<m_symbol.StopsLevel())
     { _IN1;
       printf("Take Profit must be greater than %d",m_symbol.StopsLevel());
class="macro">#define NIL(p)    (CheckPointer(p)==POINTER_INVALID)
if (CheckPointer(m_tree))==POINTER_INVALID || CheckPointer(m_cur))==POINTER_INVALID) class="kw">return;  
if (NIL(m_tree) || NIL(m_cur)) class="kw">return;

把调用栈跟踪器接进EA文件

想在 MT5 里实时抓调用栈,先得把跟踪依赖挂进工程。标准库整套以 CObject 为基类,如果你的自定义类也继承自它,只需在 Object.mqh 里 include 一次 Trace.mqh,全工程就都有了栈记录能力。 接着用 _IN 宏标记你想观测的代码块,拿编辑器的搜索/替换批量塞进去最省事。宏会记下进入块时的上下文,例子里 InitCheckParameters 的函数头和止损校验分支各放了一个 _IN / _IN1,出错时能直接定位到具体判断。 真正让跟踪器跑起来,要在 OnInit、OnTimer、OnDeinit 三个生命周期钩子里分别 new、轮询、delete 全局 CTraceCtrl 与 CTraceView 对象,并互相绑定指针。下面这段可直接粘进主文件,注意 OnTimer 里若图表窗口被手滑关掉,代码会重建视图而非崩溃。 外汇与贵金属品种点差跳变频繁,挂跟踪器会多吃一点主线程时间,回测前先在 EURUSD 的 M1 上跑空 OnInit 确认无报错再上真品种。

MQL5 / C++
class="macro">#include <Trace.mqh>
class="type">bool CSampleExpert::InitCheckParameters(class="type">int digits_adjust)
  { _IN;
class=class="str">"cmt">//--- initial data checks
  if(InpTakeProfit*digits_adjust<m_symbol.StopsLevel())
    { _IN1;
      printf("Take Profit must be greater than %d",m_symbol.StopsLevel());
class=class="str">"cmt">//------------------------------------------------------------------    OnInit
class="type">int OnInit()
  {
class=class="str">"cmt">//****************
  m_traceview= new CTraceView; class=class="str">"cmt">// created displaying of the graph
  m_trace= new CTraceCtrl; class=class="str">"cmt">// created the graph
   m_traceview.m_trace=m_trace; class=class="str">"cmt">// attached the graph
   m_trace.m_traceview=m_traceview; class=class="str">"cmt">// attached displaying of the graph
  m_traceview.Create(ChartID()); class=class="str">"cmt">// created chart
class=class="str">"cmt">//****************
  class=class="str">"cmt">// remaining part of your code…
  class="kw">return(class="num">0);
  }
class=class="str">"cmt">//------------------------------------------------------------------    OnDeinit
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//****************
  class="kw">delete m_traceview;
  class="kw">delete m_trace;
class=class="str">"cmt">//****************
  class=class="str">"cmt">// remaining part of your code…
  }
class=class="str">"cmt">//------------------------------------------------------------------    OnTimer
class="type">void OnTimer()
  {
class=class="str">"cmt">//****************
  if (m_traceview.IsOpenView(m_traceview.m_chart)) m_traceview.OnTimer();
  else { m_traceview.Deinit(); m_traceview.Create(ChartID()); } class=class="str">"cmt">// if the window is accidentally closed
class=class="str">"cmt">//****************
  class=class="str">"cmt">// remaining part of your code…
  }
class=class="str">"cmt">//------------------------------------------------------------------    OnChartEvent
class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam)
  {
class=class="str">"cmt">//****************
  m_traceview.OnChartEvent(id, lparam, dparam, sparam);
class=class="str">"cmt">//****************
  class=class="str">"cmt">// remaining part of your code…
  }
把栈可视化交给小布
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到调用结构的异常分支提示,你只需在疑似死循环处做标记。

常见问题

CNodeBase内嵌指向节点的指针数组,分支数量无上限,而标准库节点受固定结构限制,不适合动态调用栈。
每进一个块就建CIn实例并调CTraceCtrl::In挂新节点,离开时闭合,连续进出对逐步长成有向无环图。
目前小布内置的是结构异常洞察,若你把CTraceCtrl输出按约定格式导出,可在品种页叠加观看,不必自己盯日志。
它用CNode扩展了增删改与多实例容纳能力,且支持以前所有栈的保持,功能覆盖且更灵活。
借助宏包裹进入离开点即可,原逻辑不动,后续篇会讲具体宏与文件标注法。