MQL5 中的策略可视化:在标准图表中展示优化结果·综合运用
📊

MQL5 中的策略可视化:在标准图表中展示优化结果·综合运用

(3/3)·从类封装到 EA 接入,五标签页把夏普、净利、盈利因子一次看透

新手友好 第 3/3 篇
不少交易者跑完优化只盯着最后一行最优参数,却没看过每次通过的余额曲线怎么演化。把结果导出表格再手绘图表,既慢又容易漏掉回撤拐点。可视化窗口若没接好 EA,重放和切换标签就是摆设。
本章目录
  1. 表格控件里逐行塞单元格与改色那点事
  2. 把 RGB 拼回 color 并往画布上写字
  3. 把优化结果画成可视图层
  4. 点表头自动回放优化帧的实现片段
  5. 进度条类的画布指针与成员布局
  6. 进度条对象的边界与配色封装
  7. 进度条与统计图的底层绘制逻辑
  8. 统计图表类的成员与画布绑定
  9. 画布上图表区与进度条的边界绑定
  10. 控件边界与按钮指针的取法
  11. 回放按钮的生成与结果按钮的层级控制
  12. 回放按钮着色与结果按钮的惰性创建
  13. 结果按钮组的初始化与配色
  14. 净值曲线绘制类的接口与事件分发
  15. 统计图表类的刷新与序列追加逻辑
  16. 序列极值与坐标翻转的绘制细节
  17. 把序列值映射成画布像素的实战拆法
  18. 把每轮优化结果装进一个类
  19. 把回测帧指标直接打到日志里
  20. 按两位小数给回测指标排座次
  21. 回测帧数据的比较与查看器骨架
  22. 多标签优化面板里的对象与索引映射
  23. 按标签索引取回优化结果表与图
  24. 框架对象入列与绘制接口的防呆处理
  25. 把优化结果画到图表上的几个关键接口
  26. 批量改线色与回测事件的接口设计
  27. 回测前把图表先藏起来再挂标签控件
  28. 把回测指标铺进五个分页的画法
  29. 优化面板多标签的绘制与按钮兜底
  30. 回测收尾时把标签刷成可重放状态
  31. 在 OnTester 里抓取资金曲线与四项指标
  32. 回测帧里捞夏普与回撤
  33. 图表事件里的自适应重绘逻辑
  34. 优化回放与分页绘制的分支逻辑
  35. 回放锁与多标签切换的事件处理
  36. 标签页与结果按钮的事件分发逻辑
  37. 切换标签时重绘优化结果视图
  38. 切换统计标签时的按钮重排逻辑
  39. 回放开局先把按钮层级理清
  40. 把优化前三名画到图表上
  41. 按优化指标挑出前三轮回测
  42. 回测结果里捞最优帧的底层取法
  43. 把输入参数刷进优化结果表
  44. 回测面板里逐行填回测指标
  45. 把优化结果画进自定义标签页
  46. 优化结果面板的表头与网格绘制
  47. 参数表与优化图的画布落点
  48. 优化结果图表的边界与表头绘制逻辑
  49. 把帧查看器类挂进现成 EA
  50. 在 OnInit 里把信号与跟踪挂到专家对象
  51. EA初始化失败的逐级拦截
  52. 把优化结果画进图表的那几行钩子
  53. 把优化结果变成可回放画面
  54. 把这条线请下神坛

「表格控件里逐行塞单元格与改色那点事」

在 MT5 自定义表格控件里,给每一行批量挂载单元格的典型做法是先拿 RowsTotal() 取总行数,再跑一个从 0 到 total-1 的 for 循环。循环内用 GetRow(j) 取行对象,空指针就 continue 跳过,避免崩在半路。 新建单元格用 new CTableCell(row.Row(),i),i 一般是列索引。若 new 出来是 NULL,用 PrintFormat 打一行带 __FUNCTION__ 和索引的报错然后 continue;若 row.AddCell(cell) 失败则 delete cell 回收,这两道防御能让控件在内存紧张时仍可能稳住不闪退。 cell.SetXY(col_x,row.Y()) 和 cell.SetSize(col_w,row_h) 把坐标和尺寸锚到列宽与行高上,循环结束调 canvas.Update(false) 只刷画布不重绘整图,CPU 占用倾向更低。 配色上 NewColor() 接收 base_color 加三个通道偏移,内部用 ColorToRGB 拆成 0~255 的浮点,每个分量做越界钳制(小于 0 取 0、大于 255 取 255),再 RGBToColor 合回去。这意味着你传 shift_red=300 也不会溢出,出来的仍是合法色值。 外汇与贵金属图表叠加这类自绘控件时波动刷新的风险偏高,建议先在回测器外的 demo 账户验证内存与重绘表现。

MQL5 / C++
class="type">int total=this.RowsTotal();
for(class="type">int j=class="num">0;j<total;j++)
  {
   CTableRow *row=this.GetRow(j);
   if(row==NULL)
      class="kw">continue;
   CTableCell *cell=new CTableCell(row.Row(),i);
   if(cell==NULL)
      {
       ::PrintFormat("%s: Failed to create table cell object at index %lu",(class="type">class="kw">string)__FUNCTION__,i);
       class="kw">continue;
      }
   if(!row.AddCell(cell))
      {
       class="kw">delete cell;
       class="kw">continue;
      }
   cell.SetXY(col_x,row.Y());
   cell.SetSize(col_w, row_h);
   }
canvas.Update(false);

class="type">color CTableDataControl::NewColor(class="type">color base_color, class="type">int shift_red, class="type">int shift_green, class="type">int shift_blue)
  {
   class="type">class="kw">double clR=class="num">0, clG=class="num">0, clB=class="num">0;
   this.ColorToRGB(base_color,clR,clG,clB);
   class="type">class="kw">double clRn=(clR+shift_red  < class="num">0 ? class="num">0 : clR+shift_red  > class="num">255 ? class="num">255 : clR+shift_red);
   class="type">class="kw">double clGn=(clG+shift_green< class="num">0 ? class="num">0 : clG+shift_green> class="num">255 ? class="num">255 : clG+shift_green);
   class="type">class="kw">double clBn=(clB+shift_blue < class="num">0 ? class="num">0 : clB+shift_blue > class="num">255 ? class="num">255 : clB+shift_blue);
   class="kw">return this.RGBToColor(clRn,clGn,clBn);
  }

class="type">color CTableDataControl::RGBToColor(const class="type">class="kw">double r,const class="type">class="kw">double g,const class="type">class="kw">double b) const
  {
   class="type">int int_r=(class="type">int)::round(r);
   class="type">int int_g=(class="type">int)::round(g);

把 RGB 拼回 color 并往画布上写字

在自定义表格控件里,颜色经常要以 R/G/B 三个分量分别存、再合成 MT5 的 color 类型。下面这段先把蓝色分量左移 8 位、或上绿色,再左移 8 位、或上红色,最终强转回 color 返回,是一个典型的 0xBBGGRR 打包过程。 int int_b=(int)::round(b); int clr=0; clr=int_b; clr<<=8; clr|=int_g; clr<<=8; clr|=int_r; return (color)clr; ColorToRGB 则是反向操作,把传入的 color 拆回 r/g/b 三个 double 引用,内部直接调 GetR/GetG/GetB,方便外部按需改分量。 DrawText 负责把一段文字绘到 CCanvas 指定坐标。若传入 width 和 height 都是 0,整块画布会被 0x00FFFFFF(全透明)擦掉;否则按文字实际尺寸或调用方给的宽高,先用透明色 FillRectangle 清掉旧内容,再 TextOut 写新字,最后 Update(false) 只刷缓冲区不重绘屏幕。 在 MT5 里开个脚本挂 CCanvas 验证:传 width=0,height=0 看是否整屏变透明,再传具体坐标和 WRONG_VALUE 看文字尺寸自适应,能直接确认这套擦除逻辑有没有坑。外汇与贵金属图表叠加自绘控件波动剧烈,误清屏可能遮住价格,实盘前务必在模拟环境跑通。

MQL5 / C++
class="type">int int_b=(class="type">int)::round(b);
class="type">int clr=class="num">0;
clr=int_b;
clr<<=class="num">8;
clr|=int_g;
clr<<=class="num">8;
clr|=int_r;
class=class="str">"cmt">//---
class="kw">return (class="type">color)clr;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Retrieving RGB component values                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableDataControl::ColorToRGB(const class="type">color clr,class="type">class="kw">double &r,class="type">class="kw">double &g,class="type">class="kw">double &b)
  {
  r=GetR(clr);
  g=GetG(clr);
  b=GetB(clr);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Prints text message to specified coordinates                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableDataControl::DrawText(CCanvas *canvas,const class="type">class="kw">string text,const class="type">int x,const class="type">int y,const class="type">color clr=clrNONE,const class="type">uint align=class="num">0,const class="type">int width=WRONG_VALUE,const class="type">int height=WRONG_VALUE)
  {
class=class="str">"cmt">//--- Declare variables to record width and height of text in them
  class="type">int w=width;
  class="type">int h=height;
class=class="str">"cmt">//--- If width and height of text passed to method have zero values,
class=class="str">"cmt">//--- entire canvas space is completely erased with transparent class="type">color
  if(width==class="num">0 && height==class="num">0)
     canvas.Erase(0x00FFFFFF);
class=class="str">"cmt">//--- Otherwise
  else
   {
   class=class="str">"cmt">//--- If passed width and height have class="kw">default values(-class="num">1), get its width and height from text
   if(width==WRONG_VALUE && height==WRONG_VALUE)
      canvas.TextSize(text,w,h);
   class=class="str">"cmt">//--- otherwise,
   else
     {
     class=class="str">"cmt">//--- if width passed to method has class="kw">default value(-class="num">1), get width from text, or
     class=class="str">"cmt">//--- if width passed to method has value greater than zero, use width passed to method, or
     class=class="str">"cmt">//--- if width passed to method has zero value, use value class="num">1 for width
     w=(width ==WRONG_VALUE ? canvas.TextWidth(text)  : width>class="num">0 ? width  : class="num">1);
     class=class="str">"cmt">//--- if height passed to method has class="kw">default value(-class="num">1), get height from text, or
     class=class="str">"cmt">//--- if height passed to method has value greater than zero, use height passed to method, or
     class=class="str">"cmt">//--- if height passed to method has zero value, use value class="num">1 for height
     h=(height==WRONG_VALUE ? canvas.TextHeight(text) : height>class="num">0 ? height : class="num">1);
     }
   class=class="str">"cmt">//--- Fill space according to specified coordinates and by resulting width and height with transparent class="type">color (erase previous entry)
   canvas.FillRectangle(x,y,x+w,y+h,0x00FFFFFF);
   }
class=class="str">"cmt">//--- Print text in place cleared of previous text and update workspace without redrawing screen
  canvas.TextOut(x,y,text,::ColorToARGB(clr==clrNONE ? this.m_fore_color : clr),align);
  canvas.Update(false);
  }

◍ 把优化结果画成可视图层

在 MT5 策略测试器里跑多参数寻优时,默认只给一张枯燥的表格。通过 CCanvas 封装一个填充矩形函数,可以把关键区域直接刷到图表画布上,坐标与透明度都由调用方控制。 下面这段 DrawRectangleFill 是底层绘制入口:传入画布指针、左上角 x/y、宽高、颜色与 alpha 通道,内部用 ColorToARGB 合成后 FillRectangle 再 Update,刷屏延迟取决于画布分辨率,一般 1920×1080 下肉眼无感。

MQL5 / C++
class="type">void CTableDataControl::DrawRectangleFill(CCanvas *canvas,const class="type">int x,const class="type">int y,const class="type">int width,const class="type">int height,const class="type">color clr,const class="type">uchar alpha)
  {
   canvas.FillRectangle(x,y,x+width,y+height,::ColorToARGB(clr,alpha));
   canvas.Update();
  }
真正的优化挂钩在 OnTester 系列事件里。OnTesterInit(3) 预先开 3 条余额曲线通道;每跑完一组参数 OnTesterPass 抓帧;结束时 OnTesterDeinit 收尾。优化判据这里写成夏普比绝对值乘净利润,净利为负则取反,倾向让测试器把“亏得少”也排前面。
MQL5 / C++
class="macro">#include <FrameGenerator.mqh>
CFrameGenerator fg;
class="type">class="kw">double OnTester()
  {
   class="type">class="kw">double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT));
   TesterCritetia=TesterStatistics(STAT_PROFIT)>class="num">0?TesterCritetia:(-TesterCritetia);
   fg.OnTester(TesterCritetia);
   class="kw">return(TesterCritetia);
  }
class="type">void OnTesterInit()
  {
   fg.OnTesterInit(class="num">3);
  }
class="type">void OnTesterPass()
  {
   fg.OnTesterPass();
  }
class="type">void OnTesterDeinit()
  {
   fg.OnTesterDeinit();
  }
外汇与贵金属品种点差跳空频繁,这套图形化框架只解决“看得清”,不解决“算得准”;实盘前务必用历史数据多周期回测验证判据稳定性。

MQL5 / C++
class="type">void CTableDataControl::DrawRectangleFill(CCanvas *canvas,const class="type">int x,const class="type">int y,const class="type">int width,const class="type">int height,const class="type">color clr,const class="type">uchar alpha)
  {
   canvas.FillRectangle(x,y,x+width,y+height,::ColorToARGB(clr,alpha));
   canvas.Update();
  }
class="macro">#include <FrameGenerator.mqh>
CFrameGenerator fg;
class="type">class="kw">double OnTester()
  {
   class="type">class="kw">double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT));
   TesterCritetia=TesterStatistics(STAT_PROFIT)>class="num">0?TesterCritetia:(-TesterCritetia);
   fg.OnTester(TesterCritetia);
   class="kw">return(TesterCritetia);
  }
class="type">void OnTesterInit()
  {
   fg.OnTesterInit(class="num">3);
  }
class="type">void OnTesterPass()
  {
   fg.OnTesterPass();
  }
class="type">void OnTesterDeinit()
  {
   fg.OnTesterDeinit();
  }

「点表头自动回放优化帧的实现片段」

在 MT5 优化器里,若想在点击结果表头后自动顺序回放各参数组合的测试帧,可在图表事件回调末尾手动触发一次帧播放事件。下面这段调用把播放间隔锁在 100 毫秒,实测在普通 i7 笔记本上回放 200 帧约耗时 20 秒,卡顿概率随帧数上升而增加。 fg.OnChartEvent(id,lparam,dparam,sparam,100); // 100 即两帧之间的停顿毫秒数,调小会更快但可能丢帧 配套的 FrameViewer 头文件里用宏写死了表格与控件的尺寸:单元格宽 128、高 19,结果选择按钮宽为 CELL_W+30,单次展示的数据列数 DATA_COUNT 定为 8。这些硬常数直接决定面板布局,改之前最好先量一下自己显示器的 DPI。 需要留意,外汇与贵金属品种的高频优化回放属于计算密集型操作,实盘环境误触可能拖慢终端;建议仅在策略测试器离线模式下验证该类代码。

MQL5 / C++
  class=class="str">"cmt">//--- starts playback of frames upon completion of optimization when clicking on header
  fg.OnChartEvent(id,lparam,dparam,sparam,class="num">100); class=class="str">"cmt">// class="num">100 - this is pause in ms between frames
  }

进度条类的画布指针与成员布局

在 MT5 自定义指标里用 CCanvas 做离屏绘图时,把背景层和前景层拆成两个指针是常见做法。下面这段类声明给出了一个彩色进度条控件的骨架:m_background 负责底层绘制,m_foreground 叠在上方画框线和动态元素,二者都必须在构造后通过 SetCanvas 注入,否则直接 Print 报错返回。 成员里 m_bound 用 CRect 存工作区坐标和尺寸,m_good_color / m_bad_color 分开存盈利与亏损序列的着色,m_back_color / m_fore_color 管背景和边框。m_passes 是布尔数组记录已处理的 pass 数,m_last_index 存上一次 pass 的索引——这几个字段共同决定了重绘时从哪里断点续画。 SetCanvas 的防御写法值得抄:先判 background 和 foreground 是否为 NULL,任一为空就打印带 __FUNCTION__ 的错误并 return,不往下赋值。这种双空指针检查能避免后期 Canvas 操作直接崩 EA,开 MT5 新建一个 CCanvas 派生类时建议原样保留该判断逻辑。

MQL5 / C++
  CCanvas                *m_background;                     class=class="str">"cmt">// Pointer to CCanvas class object for drawing on background
  CCanvas                *m_foreground;                     class=class="str">"cmt">// Pointer to CCanvas class object for drawing on foreground
  CRect                  m_bound;                           class=class="str">"cmt">// Coordinates and dimensions of workspace
  class="type">color                  m_good_color, m_bad_color;         class=class="str">"cmt">// Colors of profitable and loss series
  class="type">color                  m_back_color, m_fore_color;        class=class="str">"cmt">// Background and frame colors
  class="type">bool                   m_passes[];                        class=class="str">"cmt">// Number of processed passes
  class="type">int                    m_last_index;                      class=class="str">"cmt">// Last pass index
class="kw">public:
class=class="str">"cmt">//--- Constructor/destructor
                       CColorProgressBar(class="type">void);
                      ~CColorProgressBar(class="type">void){};

class=class="str">"cmt">//--- Sets pointer to canvas
  class="type">void                  SetCanvas(CCanvas *background, CCanvas *foreground)
                       {
                         if(background==NULL)
                          {
                           ::Print(__FUNCTION__, ": Error. Background is NULL");
                           class="kw">return;
                          }
                         if(foreground==NULL)
                          {
                           ::Print(__FUNCTION__, ": Error. Foreground is NULL");
                           class="kw">return;
                          }

                         this.m_background=background;
                         this.m_foreground=foreground;
                       }
class=class="str">"cmt">//--- Sets coordinates and dimensions of workspace on canvas

◍ 进度条对象的边界与配色封装

在 MT5 自定义指标里画进度条,第一步是把矩形区域的四角坐标和颜色状态锁进类成员。下面这段接口把边界存进 m_bound,用 left/top/right/bottom 四个整型字段表达像素盒,调用方只需传 x1,y1,x2,y2 即可重设。 void SetBound(const int x1, const int y1, const int x2, const int y2) { this.m_bound.SetBound(x1, y1, x2, y2); } int X1(void) const { return this.m_bound.left; } int Y1(void) const { return this.m_bound.top; } int X2(void) const { return this.m_bound.right; } int Y2(void) const { return this.m_bound.bottom; } 配色走两套独立 setter:SetBackColor 写 m_back_color,SetForeColor 写 m_fore_color,之后用 BackColor() / ForeColor() 原样读出。实盘面板若要根据品种波动切换边框色,直接挂这两个方法就行。 进度状态靠 Reset() 把 m_last_index 清零,AddResult() 按 good 布尔值追加绘制段,Update() 负责把进度刷到图表。外汇与贵金属行情跳空频繁,这类重绘若放在 OnCalculate 每 tick 调,可能拖慢 MT5 响应,建议按秒级定时器触发 Update。

MQL5 / C++
class="type">void SetBound(const class="type">int x1, const class="type">int y1, const class="type">int x2, const class="type">int y2)
  { this.m_bound.SetBound(x1, y1, x2, y2); }
class=class="str">"cmt">//--- Return of coordinates of bounds of rectangular area
class="type">int X1(class="type">void) const { class="kw">return this.m_bound.left;   }
class="type">int Y1(class="type">void) const { class="kw">return this.m_bound.top;    }
class="type">int X2(class="type">void) const { class="kw">return this.m_bound.right;  }
class="type">int Y2(class="type">void) const { class="kw">return this.m_bound.bottom; }

class=class="str">"cmt">//--- Setting background class="type">color and frame
class="type">void SetBackColor(const class="type">color clr)  { this.m_back_color=clr;    }
class="type">void SetForeColor(const class="type">color clr)  { this.m_fore_color=clr;    }
class=class="str">"cmt">//--- Returning background class="type">color and frame
class="type">color BackColor(class="type">void) const { class="kw">return this.m_back_color;  }
class="type">color ForeColor(class="type">void) const { class="kw">return this.m_fore_color;  }
class=class="str">"cmt">//--- Resets counter to zero
class="type">void Reset(class="type">void)  { this.m_last_index=class="num">0;      }
class=class="str">"cmt">//--- Adds result for drawing strip in progress bar
class="type">void AddResult(class="type">bool good, const class="type">bool chart_redraw);
class=class="str">"cmt">//--- Updates progress bar on chart
class="type">void Update(const class="type">bool chart_redraw);
};

「进度条与统计图的底层绘制逻辑」

在 MT5 的 EA 可视化组件里,CColorProgressBar 用了一个长度 5000、预留增量 1000 的 pass 数组来记录每笔优化结果的成败。构造函数里直接 ArrayResize(m_passes,5000,1000) 并清零,意味着最多缓存约 5000 根竖线,超出后靠环形索引回卷,不会频繁重分配内存。 AddResult 每次只做三件事:把 bool 写进 m_passes,用 LineVertical 在前景层画一条宽 1 像素的竖线(成功用 clrSeaGreen、失败用 clrLightPink 的 ARGB 值),然后 m_foreground.Update 决定是否立刻重绘。m_last_index 到达边界宽度减 1 时归零,所以进度条视觉上会循环覆盖。 CStatChart 类则负责把优化结果和参数做成统计图表。私有成员里 m_seria[] 是 CArrayDouble 数组,用来按序列存数值;m_lines 控制图表行数,m_line_width 以像素定线宽,背景与边框色由 m_back_color、m_fore_color 分开管。想验证的话,开 MT5 把这段塞进自定义指标,跑一组 3000 次以上的回测,就能看到粉绿竖条在边框内滚动。外汇与贵金属品种波动大,这类可视化仅辅助判断,不预示任何收益。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
CColorProgressBar::CColorProgressBar() : m_last_index(class="num">0), m_good_color(clrSeaGreen), m_bad_color(clrLightPink)
  {
class=class="str">"cmt">//--- Set size of pass array with reserve
   ::ArrayResize(this.m_passes, class="num">5000, class="num">1000);
   ::ArrayInitialize(this.m_passes, class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Adding result                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CColorProgressBar::AddResult(class="type">bool good, const class="type">bool chart_redraw)
  {
   this.m_passes[this.m_last_index]=good;
class=class="str">"cmt">//--- Add another vertical line of desired class="type">color to progress bar
   this.m_foreground.LineVertical(this.X1()+class="num">1+this.m_last_index, this.Y1()+class="num">1, this.Y2()-class="num">1, ::ColorToARGB(good ? this.m_good_color : this.m_bad_color));
class=class="str">"cmt">//--- Update on chart
   this.m_foreground.Update(chart_redraw);
class=class="str">"cmt">//--- Updating index
   this.m_last_index++;
   if(this.m_last_index>=this.m_bound.Width()-class="num">1)
      this.m_last_index=class="num">0;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Updating progress bar on chart                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CColorProgressBar::Update(const class="type">bool chart_redraw)
  {
class=class="str">"cmt">//--- Fill background with background class="type">color
   this.m_background.FillRectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_back_color));
class=class="str">"cmt">//--- Draw border
   this.m_background.Rectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_fore_color));
class=class="str">"cmt">//--- Update chart
   this.m_background.Update(chart_redraw);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class for rendering statistics charts and tables                 |
class=class="str">"cmt">//| of optimization results and EA’s settings parameters             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CStatChart: class="kw">public CObject
  {
class="kw">private:
   class="type">color            m_back_color;      class=class="str">"cmt">// Background class="type">color
   class="type">color            m_fore_color;      class=class="str">"cmt">// Border class="type">color
   class="type">int              m_line_width;      class=class="str">"cmt">// Line width in pixels
   class="type">int              m_lines;           class=class="str">"cmt">// Number of lines on chart
   CArrayDouble     m_seria[];         class=class="str">"cmt">// Arrays for storing chart values

统计图表类的成员与画布绑定

在自绘回测统计面板时,CStatChart 类把盈亏序列、画布指针和交互控件全收进成员变量。m_profitseria[] 用布尔数组标记每一条序列是否盈利,m_lastseria_index 记录图表上最新一根线的索引,初始构造里被置为 0。 颜色变量直接决定视觉反馈:盈利序列默认 clrForestGreen,亏损用 clrOrangeRed,被选中的最优序列用 clrDodgerBlue。这些硬编码默认值在构造函数初始化列表里一次性写完,后面想换配色直接改这里就行。 protected 段里挂了两个 CCanvas 指针,m_background 负责底层绘制、m_foreground 负责上层覆盖,外加 CRect 界定图表与表头区域、CColorProgressBar 和两类按钮对象。注意析构只把指针置 NULL,并不释放 CCanvas 实例本身——外层管理别漏了。 SetCanvas 是唯一入口,传入背景和前景画布指针。任一为 NULL 就打印带 __FUNCTION__ 的错误并 return,不抛异常。开 MT5 写自己的面板时,先确认这两个指针非空再调绘制,否则界面会静默空白。

MQL5 / C++
  class="type">bool                m_profitseria[];       class=class="str">"cmt">// Profitable series or not
  class="type">int                 m_lastseria_index;     class=class="str">"cmt">// Index of fresh line on chart
  class="type">color               m_profit_color;        class=class="str">"cmt">// Color of profitable series
  class="type">color               m_loss_color;          class=class="str">"cmt">// Color of loss series
  class="type">color               m_selected_color;      class=class="str">"cmt">// Color of selected best series

class="kw">protected:
  CCanvas             *m_background;         class=class="str">"cmt">// Pointer to object of CCanvas class for drawing on background
  CCanvas             *m_foreground;         class=class="str">"cmt">// Pointer to CCanvas class object for drawing on foreground
  CRect               m_bound_chart;         class=class="str">"cmt">// Workspace of chart
  CRect               m_bound_head;          class=class="str">"cmt">// Chart header workspace
  CColorProgressBar   m_progress_bar;        class=class="str">"cmt">// Progress bar
  CButton             m_button_replay;       class=class="str">"cmt">// Replay button
  CButtonSwitch       m_button_res;          class=class="str">"cmt">// Button for selecting one of top three results
  class="type">int                 m_tab_id;              class=class="str">"cmt">// Tab ID

class="kw">public:
class=class="str">"cmt">//--- Constructor/destructor
                    CStatChart() : m_lastseria_index(class="num">0), m_profit_color(clrForestGreen), m_loss_color(clrOrangeRed), m_selected_color(clrDodgerBlue), m_tab_id(class="num">0) {};
                   ~CStatChart() { this.m_background=NULL; this.m_foreground=NULL; }

class=class="str">"cmt">//--- Sets pointer to canvas
  class="type">void                SetCanvas(CCanvas *background, CCanvas *foreground)
                     {
                       if(background==NULL)
                         {
                          ::Print(__FUNCTION__, ": Error. Background is NULL");
                          class="kw">return;
                         }
                       if(foreground==NULL)
                         {
                          ::Print(__FUNCTION__, ": Error. Foreground is NULL");
                          class="kw">return;
                         }

◍ 画布上图表区与进度条的边界绑定

在自定义统计图表类里,坐标和尺寸必须显式绑定到画布,否则进度条和表头会画到错误区域。SetChartBounds 接收四个整数参数 (x1,y1,x2,y2),分别代表图表工作区的左上角和右下角像素坐标。 注意表头并不占用工作区内部空间:代码里用 y1-CELL_H 把表头放在工作区上方一格,进度条则贴着工作区下沿 y2-CELL_H 到 y2。CELL_H 是单元格高度常量,若你改了行高,这里也要同步改,否则头部和进度条会重叠。 GetProgressBar 直接返回内部 m_progress_bar 的指针,意味着外部可以绕过封装直接调进度条方法;SetCanvas 里同时把背景前景色传给进度条,说明配色是集中管理的。开 MT5 把这段塞进你的 CStatChart 类,改 CELL_H 看表头间距变化就能验证。

MQL5 / C++
      this.m_background=background;
      this.m_foreground=foreground;
      this.m_progress_bar.SetCanvas(background, foreground);
     }
class=class="str">"cmt">//--- Sets coordinates and dimensions of chart workspace and progress bar on canvas
   class="type">void            SetChartBounds(const class="type">int x1, const class="type">int y1, const class="type">int x2, const class="type">int y2)
     {
      this.m_bound_chart.SetBound(x1, y1, x2, y2);
      this.SetBoundHeader(x1, y1-CELL_H, x2, y1);
      this.m_progress_bar.SetBound(x1, y2-CELL_H, x2, y2);
     }
class=class="str">"cmt">//--- Sets coordinates and dimensions of chart header on canvas
   class="type">void            SetBoundHeader(const class="type">int x1, const class="type">int y1, const class="type">int x2, const class="type">int y2)
     {
      this.m_bound_head.SetBound(x1, y1, x2, y2);
     }
class=class="str">"cmt">//--- Returns pointer to(class="num">1) itself, (class="num">2) progress bar
   CStatChart      *Get(class="type">void)                   { class="kw">return &this;                }
   CColorProgressBar*GetProgressBar(class="type">void)       { class="kw">return(&this.m_progress_bar); }
class=class="str">"cmt">//--- Setting/returning tab ID
   class="type">void            SetTabID(const class="type">int id)       { this.m_tab_id=id;            }
   class="type">int             TabID(class="type">void)           const  { class="kw">return this.m_tab_id;        }

class=class="str">"cmt">//--- Returning coordinates of bounds of chart’s rectangular area
   class="type">int             X1(class="type">void)                    const { class="kw">return this.m_bound_chart.left;  }

「控件边界与按钮指针的取法」

在自定义面板类里,坐标边界和按钮句柄都通过一组轻量 getter 暴露出来,方便外部直接读像素位置做碰撞判断或重绘。 图表主区四个角由 m_bound_chart 的成员给出:Y1 取上边、X2 取右边、Y2 取下边,对应 top / right / bottom。头部矩形则用 m_bound_head 的 left、top、right、bottom 返回,注释里写清这是标题栏区域。 进度条区域不直接存坐标,而是转发给 m_progress_bar 对象的 X1/Y1/X2/Y2,说明进度条自身也是带边界子对象。 按钮部分返回的是指针:重播按钮是 CButton*,结果切换是 CButtonSwitch*,而最差结果按钮通过 m_button_res.GetButton(0) 拿到第 0 个子按钮(CButtonTriggered*)。这种分层取法在 MT5 里能让你在 EA 中直接操纵面板控件,而不必重写坐标逻辑。 外汇与贵金属图表加载这类自定义面板时波动剧烈,控件重绘可能滞后于价格跳动,属高风险操作环境,实盘前应在模拟盘验证坐标刷新频率。

MQL5 / C++
class="type">int Y1(class="type">void) const { class="kw">return this.m_bound_chart.top; }
class="type">int X2(class="type">void) const { class="kw">return this.m_bound_chart.right; }
class="type">int Y2(class="type">void) const { class="kw">return this.m_bound_chart.bottom; }
class=class="str">"cmt">//--- Return of coordinates of bounds of rectangular header area
class="type">int HeaderX1(class="type">void) const { class="kw">return this.m_bound_head.left; }
class="type">int HeaderY1(class="type">void) const { class="kw">return this.m_bound_head.top; }
class="type">int HeaderX2(class="type">void) const { class="kw">return this.m_bound_head.right; }
class="type">int HeaderY2(class="type">void) const { class="kw">return this.m_bound_head.bottom; }
class=class="str">"cmt">//--- Return of coordinates of bounds of rectangular area of progress bar
class="type">int ProgressBarX1(class="type">void) const { class="kw">return this.m_progress_bar.X1(); }
class="type">int ProgressBarY1(class="type">void) const { class="kw">return this.m_progress_bar.Y1(); }
class="type">int ProgressBarX2(class="type">void) const { class="kw">return this.m_progress_bar.X2(); }
class="type">int ProgressBarY2(class="type">void) const { class="kw">return this.m_progress_bar.Y2(); }

class=class="str">"cmt">//--- Returns pointer to button of: (class="num">1) replay, (class="num">2) result selection(class="num">3) worst, (class="num">4) average, (class="num">5) best result
CButton *ButtonReplay(class="type">void) { class="kw">return(&this.m_button_replay); }
CButtonSwitch *ButtonResult(class="type">void) { class="kw">return(&this.m_button_res); }
CButtonTriggered *ButtonResultMin(class="type">void) { class="kw">return(this.m_button_res.GetButton(class="num">0)); }

回放按钮的生成与结果按钮的层级控制

在自定义优化报告面板里,结果选择按钮通常有三个实例:索引 0、1、2 分别对应最小、中间、最大结果。通过 GetButton(1) 和 GetButton(2) 可以直接拿到中间与最大结果的触发按钮指针,方便在外部逻辑里读取点击状态。 结果按钮组的显隐与置顶由三个短函数包办:Hide() 隐藏整组,Show() 恢复显示,BringToTop() 把按钮提到前景层,避免被背景矩形遮挡。实盘或回测可视化时,若面板叠在 K 线上,忘记 BringToTop() 会导致按钮点不到。 回放按钮的创建有前置依赖:必须先调用 SetCanvas() 给 m_background 赋值,否则函数直接打印“Фон не задан”并返回 false。按钮文本固定为 "Optimization Completed: Click to Replay",宽度按 TextWidth(text) 实测,再依据表头中心点水平居中、紧贴表头下沿偏移 2 像素定位。 按钮高度用 CELL_H-1 像素,ID 按 Tab 编号拼接成 "Tab%d_ButtonReplay"。Create() 失败同样返回 false,因此调用方应检查返回值再决定是否启用回放交互。外汇与贵金属品种回测中这类面板仅作辅助,杠杆风险高,按钮逻辑错误不会直接导致下单,但可能误导参数选择。

MQL5 / C++
CButtonTriggered *ButtonResultMid(class="type">void) { class="kw">return(this.m_button_res.GetButton(class="num">1)); }
CButtonTriggered *ButtonResultMax(class="type">void) { class="kw">return(this.m_button_res.GetButton(class="num">2)); }

class=class="str">"cmt">//--- (class="num">1) Hides, (class="num">2) shows, (class="num">3) brings results selection button to foreground
class="type">bool ButtonsResultHide(class="type">void) { class="kw">return(this.m_button_res.Hide()); }
class="type">bool ButtonsResultShow(class="type">void) { class="kw">return(this.m_button_res.Show()); }
class="type">bool ButtonsResultBringToTop(class="type">void) { class="kw">return(this.m_button_res.BringToTop()); }

class=class="str">"cmt">//--- Creates replay button
class="type">bool CreateButtonReplay(class="type">void)
  {
   if(this.m_background==NULL)
     {
      ::PrintFormat("%s: Фон не задан (сначала используйте функцию SetCanvas())");
      class="kw">return false;
     }
   class="type">class="kw">string text="Optimization Completed: Click to Replay";
   class="type">int w=this.m_background.TextWidth(text);
   
   class=class="str">"cmt">//--- Upper-left coordinate of button
   CPoint cp=this.m_bound_head.CenterPoint();
   class="type">int x=cp.x-w/class="num">2;
   class="type">int y=this.Y1()+this.m_bound_head.top-class="num">2;
   
   class=class="str">"cmt">//--- Create button and set new colors for it, hide created button
   if(!this.m_button_replay.Create(::StringFormat("Tab%d_ButtonReplay", this.m_tab_id), text, x, y, w, CELL_H-class="num">1))
      class="kw">return false;

◍ 回放按钮着色与结果按钮的惰性创建

在面板初始化收尾阶段,回放按钮需要先定好未激活态的三层颜色。背景、边框统一用相近的浅绿(R144 不变,G 从 238 递减到 218),文字用黑、禁用态转灰,随后 ResetUsedColors(STATE_OFF) 清掉缓存并 Draw(false) 后 Hide(),避免空按钮闪一下。 紧跟着的 CreateButtonResults() 负责建“结果选择”按钮,但它不急着显示。函数先判 m_background 是否为 NULL,若背景画布都没通过 SetCanvas() 准备好就直接 PrintFormat 报错并返回 false,这是典型的依赖前置检查。 按钮坐标取头部左边 +1、进度条下方 CELL_H+2,宽用宏 BUTT_RES_W,高取 CELL_H-1。Create 时名字按 Tab{id}_ButtonRes 拼,画布为空字符串,建完不立刻显示——留到后续状态切换再拉出来用。外汇与贵金属面板开发涉及实时重绘,回测与实盘高频刷新下控件隐藏逻辑没写好容易闪屏,建议开 MT5 把这段挂到 EA 初始化里单步看一次。

MQL5 / C++
this.m_button_replay.SetDefaultColors(COLOR_BACKGROUND, STATE_OFF, C&class="macro">#x27;class="num">144,class="num">238,class="num">144&class="macro">#x27;, C&class="macro">#x27;class="num">144,class="num">228,class="num">144&class="macro">#x27;, C&class="macro">#x27;class="num">144,class="num">218,class="num">144&class="macro">#x27;, clrSilver);
this.m_button_replay.SetDefaultColors(COLOR_BORDER, STATE_OFF, C&class="macro">#x27;class="num">144,class="num">238,class="num">144&class="macro">#x27;, C&class="macro">#x27;class="num">144,class="num">228,class="num">144&class="macro">#x27;, C&class="macro">#x27;class="num">144,class="num">218,class="num">144&class="macro">#x27;, clrSilver);
this.m_button_replay.SetDefaultColors(COLOR_FOREGROUND, STATE_OFF, clrBlack, clrBlack, clrBlack, clrGray);
this.m_button_replay.ResetUsedColors(STATE_OFF);
this.m_button_replay.Draw(false);
this.m_button_replay.Hide();
class="kw">return true;
}

class=class="str">"cmt">//--- Creates results selection button
class="type">bool CreateButtonResults(class="type">void)
  {
   if(this.m_background==NULL)
     {
      ::PrintFormat("%s: Фон не задан (сначала используйте функцию SetCanvas())");
      class="kw">return false;
     }
   class=class="str">"cmt">//--- Upper-left coordinate of button
   class="type">int x=this.m_bound_head.left+class="num">1;
   class="type">int y=this.m_progress_bar.Y1()+CELL_H+class="num">2;
   class="type">int w=BUTT_RES_W;
   
   class=class="str">"cmt">//--- Creatу button and set new colors for it, hidу created button
   if(!this.m_button_res.Create(::StringFormat("Tab%u_ButtonRes",this.m_tab_id), "", x, y, w, CELL_H-class="num">1))
     class="kw">return false;

「结果按钮组的初始化与配色」

在面板里塞一组结果按钮时,先声明包含 3 个字符串的数组:分别对应前三名策略的最差、平均、最佳结果。用 AddNewButton 批量建按钮,若返回 false 说明资源没申请成功,直接中断初始化。 三个按钮的 OFF 状态都刷成浅灰底(C'228,228,228')配银色边框,再调 ResetUsedColors(STATE_OFF) 清掉旧配色缓存,保证界面状态一致。最后 Draw(false) 画出来并 Hide(),按钮建好但先不显示,等触发时再亮。 背景和边框色由独立方法接管:SetBackColor 写进 m_back_color 并同步给进度条,SetForeColor 同理改 m_fore_color。这样外汇或贵金属 EA 面板的高风险提示区,也能跟着主题色走,不至于刺眼。

MQL5 / C++
class="type">class="kw">string text[class="num">3]={"Worst result of the top class="num">3", "Average result of the top class="num">3", "Best result of the top class="num">3"};
if(!this.m_button_res.AddNewButton(text, w))
   class="kw">return false;
this.m_button_res.GetButton(class="num">0).SetDefaultColors(COLOR_BORDER, STATE_OFF, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, clrSilver);
this.m_button_res.GetButton(class="num">0).ResetUsedColors(STATE_OFF);
this.m_button_res.GetButton(class="num">1).SetDefaultColors(COLOR_BORDER, STATE_OFF, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, clrSilver);
this.m_button_res.GetButton(class="num">1).ResetUsedColors(STATE_OFF);
this.m_button_res.GetButton(class="num">2).SetDefaultColors(COLOR_BORDER, STATE_OFF, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, C&class="macro">#x27;class="num">228,class="num">228,class="num">228&class="macro">#x27;, clrSilver);
this.m_button_res.GetButton(class="num">2).ResetUsedColors(STATE_OFF);
this.m_button_res.Draw(false);
this.m_button_res.Hide();
class="kw">return true;
class=class="str">"cmt">//--- Sets background class="type">color
class="type">void SetBackColor(const class="type">color clr)
{
   this.m_back_color=clr;
   this.m_progress_bar.SetBackColor(clr);
}
class=class="str">"cmt">//--- Sets border class="type">color
class="type">void SetForeColor(const class="type">color clr)
{
   this.m_fore_color=clr;
   this.m_progress_bar.SetForeColor(clr);
}

净值曲线绘制类的接口与事件分发

在 MT5 自定义指标或 EA 里做回测可视化时,常需要一个能同时画多组序列(如盈利单与亏损单曲线)的对象类。下面这段接口定义给出了该类对外暴露的核心方法,可直接拷进你的 .mqh 头文件做二次开发。 SetLines 负责设定序列条数并同步扩容两个动态数组:m_seria 存原始数值,m_profitseria 标记该序列是否属盈利组。注意 ArrayResize 用的是全局域操作符 ::,说明它绕开可能的成员遮蔽直接调标准库。 配色接口分三档:SetProfitColorLine 管盈利线、SetLossColorLine 管亏损线、SetSelectedLineColor 管选中态。回测面板里把这三者设成对比色(比如盈利绿、亏损红、选中黄),肉眼区分胜败单会快很多。 绘制相关方法里,Draw 按序列索引上色,Line 则用左→右、下→上的常规坐标画线段;MaxValue / MinValue 提供某序列的极值,便于你自动缩放纵轴。外汇与贵金属波动大,这类自绘曲线只作概率参考,实盘仍属高风险。 事件侧只截了头半段:OnChartEvent 里先判断重播按钮 m_button_replay 与结果选择按钮 m_button_res 是否隐藏,未隐藏才转发事件。这意味着你点面板按钮时,对象类自己先拦一道,避免图表事件被别的脚本吞掉。

MQL5 / C++
class="type">void SetLines(const class="type">int num)
  {
   this.m_lines=num;
   ::ArrayResize(this.m_seria, num);
   ::ArrayResize(this.m_profitseria, num);
  }

class=class="str">"cmt">//--- Setting class="type">color of(class="num">1) profitable, (class="num">2) loss, (class="num">3) selected series
class="type">void SetProfitColorLine(const class="type">color clr)   { this.m_profit_color=clr;   }
class="type">void SetLossColorLine(const class="type">color clr)     { this.m_loss_color=clr;     }
class="type">void SetSelectedLineColor(const class="type">color clr) { this.m_selected_color=clr; }

class=class="str">"cmt">//--- Updating object on screen
class="type">void Update(class="type">color clr, const class="type">int line_width, const class="type">bool chart_redraw);
class=class="str">"cmt">//--- Adding data from array
class="type">void AddSeria(const class="type">class="kw">double &array[], class="type">bool profit);
class=class="str">"cmt">//--- Draws chart 
class="type">void Draw(const class="type">int seria_index, class="type">color clr, const class="type">int line_width, const class="type">bool chart_redraw);
class=class="str">"cmt">//--- Draws line in usual coordinates(from left to right, from bottom to top)
class="type">void Line(class="type">int x1, class="type">int y1, class="type">int x2, class="type">int y2, class="type">uint col, class="type">int size);
class=class="str">"cmt">//--- Getting max. and min. values in the series 
class="type">class="kw">double MaxValue(const class="type">int seria_index);
class="type">class="kw">double MinValue(const class="type">int seria_index);

class=class="str">"cmt">//--- Event handler
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">//--- If replay button is not hidden, call its event handler
   if(!this.m_button_replay.IsHidden())
     this.m_button_replay.OnChartEvent(id, lparam, dparam, sparam);
   class=class="str">"cmt">//--- If result selection button is not hidden, call its event handler
   if(!this.m_button_res.IsHidden())

◍ 统计图表类的刷新与序列追加逻辑

CStatChart::Update() 负责把背景画布、边框、进度条和多条数据序列一次性重绘到图表上。方法开头先判空:若 m_background 或 m_foreground 任一为 NULL 直接 return,避免canvas未初始化就写屏导致运行时报错。 背景填充用 FillRectangle 按控件坐标 X1/Y1/X2/Y2 铺底,边框用 Rectangle 描边,颜色均经 ColorToARGB 转换——这意味着你在MT5里改 m_back_color、m_fore_color 时,必须传的是普通 color 类型,ARGB透明度由系统接手。 序列绘制循环遍历 m_lines 条线:若传入 clr 为 clrNONE,则按 m_profitseria[i] 标志在 m_loss_color 与 m_profit_color 之间自动切换;否则统一用 m_selected_color。实测中这种默认配色逻辑能让盈利/亏损序列在优化结果图上直观区分,但如果你只关心某一条,显式传色更高效。 AddSeria() 用环形索引 m_lastseria_index 追加数据:先 Resize(0) 清空当前槽,再 AddArray 拷入新数组并记 profit 标志,然后自增索引,越界则回卷到0。这意味着当序列数超过 m_lines 时,最早添加的那组会被静默覆盖,调参面板若显示数据『跳变』,优先查这里。 外汇与贵金属品种上跑这类自定义统计图时,注意历史数据缺口可能造成 array 长度不齐,环形覆盖后旧序列痕迹可能残留,建议 m_lines 设值留有余量。

MQL5 / C++
class="type">void CStatChart::Update(class="type">color clr, const class="type">int line_width, const class="type">bool chart_redraw)
  {
class=class="str">"cmt">//--- If canvas for background or foreground is not installed, exit
   if(this.m_background==NULL || this.m_foreground==NULL)
      class="kw">return;
class=class="str">"cmt">//--- StatChart fill in background
   this.m_background.FillRectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_back_color));
class=class="str">"cmt">//--- StatChart draw border
   this.m_background.Rectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_fore_color));

class=class="str">"cmt">//--- ProgressBar fill in background and draw border
   this.m_progress_bar.Update(false);

class=class="str">"cmt">//--- Draw each series for class="num">80% of available chart area vertically and horizontally
   for(class="type">int i=class="num">0; i<this.m_lines; i++)
     {
      class=class="str">"cmt">//--- If class="type">color is set missing, use colors of profitable and loss series
      if(clr==clrNONE)
        {
         clr=this.m_loss_color;
         if(this.m_profitseria[i])
            clr=this.m_profit_color;
        }
      class=class="str">"cmt">//--- otherwise, use class="type">color set for selected line
      else
         clr=this.m_selected_color;

      class=class="str">"cmt">//--- Draw a chart of optimization results
      this.Draw(i, clr, line_width, false);
     }
class=class="str">"cmt">//--- Update both canvases
   this.m_background.Update(false);
   this.m_foreground.Update(chart_redraw);
  }

class="type">void CStatChart::AddSeria(const class="type">class="kw">double &array[], class="type">bool profit)
  {
class=class="str">"cmt">//--- Adding array to series number m_lastseria_index
   this.m_seria[this.m_lastseria_index].Resize(class="num">0);
   this.m_seria[this.m_lastseria_index].AddArray(array);
   this.m_profitseria[this.m_lastseria_index]=profit;
class=class="str">"cmt">//--- Track index of last line(not currently in use)
   this.m_lastseria_index++;
   if(this.m_lastseria_index>=this.m_lines)
      this.m_lastseria_index=class="num">0;
  }

「序列极值与坐标翻转的绘制细节」

CStatChart 类里取极值的方式很直白:MaxValue 和 MinValue 都把序列第 0 个元素预置为初值,再从索引 1 跑到 total-1 做单向比较,时间复杂度 O(n),不依赖任何排序。 注意 Draw 方法里有一处容易看走眼的反写:min 实际接的是 MaxValue(seria_index),max 接的是 MinValue(seria_index)。这不是笔误,而是为后面像素映射腾位置,配合 Line 里 y1_adj = Height()-CELL_H-y1 的 Y 轴倒置,把数据坐标翻成画布坐标。 Line 方法在 m_foreground 为空时直接 return,不做任何绘制;线宽小于 1 会被强制修正为 1,再走 LineThick 画圆头实线。要在 MT5 里验证,把 CStatChart 挂上资源画布后,改 CELL_H 常量观察折线纵向偏移即可。 外汇与贵金属图表叠加这类自绘统计层时,需注意品种跳空可能造成序列断层,极值映射偏差概率会升高,属于高风险场景。

MQL5 / C++
class="type">class="kw">double CStatChart::MaxValue(const class="type">int seria_index)
  {
   class="type">class="kw">double res=this.m_seria[seria_index].At(class="num">0);
   class="type">int total=this.m_seria[seria_index].Total();
class=class="str">"cmt">//--- Iterate through array and compare every two adjacent series
   for(class="type">int i=class="num">1; i<total; i++)
     {
      if(this.m_seria[seria_index].At(i)>res)
        res=this.m_seria[seria_index].At(i);
     }
class=class="str">"cmt">//--- result
   class="kw">return res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Getting minimum value of specified series                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CStatChart::MinValue(const class="type">int seria_index)
  {
   class="type">class="kw">double res=this.m_seria[seria_index].At(class="num">0);;
   class="type">int total=this.m_seria[seria_index].Total();
class=class="str">"cmt">//--- Iterate through array and compare every two adjacent series
   for(class="type">int i=class="num">1; i<total; i++)
     {
      if(this.m_seria[seria_index].At(i)<res)
        res=this.m_seria[seria_index].At(i);
     }
class=class="str">"cmt">//--- result
   class="kw">return res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Overloading the basic drawing function                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStatChart::Line(class="type">int x1, class="type">int y1, class="type">int x2, class="type">int y2, class="type">uint col, class="type">int size)
  {
class=class="str">"cmt">//--- If canvas is not set, exit
   if(this.m_foreground==NULL)
     class="kw">return;
class=class="str">"cmt">//--- Since Y-axis is inverted, invert y1 and y2
   class="type">int y1_adj=this.m_bound_chart.Height()-CELL_H-y1;
   class="type">int y2_adj=this.m_bound_chart.Height()-CELL_H-y2;

class=class="str">"cmt">//--- Draw smoothed line
class=class="str">"cmt">//--- If line thickness is less than class="num">3, draw line using the Wu smoothing algorithm
class=class="str">"cmt">//--- (for thicknesses of class="num">1 and class="num">2, LineWu() method is called in LineThick() method),
class=class="str">"cmt">//--- otherwise, draw smoothed line of given thickness using LineThick
   this.m_foreground.LineThick(x1, y1_adj, x2, y2_adj,::ColorToARGB(col), (size<class="num">1 ? class="num">1 : size), STYLE_SOLID, LINE_END_ROUND);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Drawing balance line on chart                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStatChart::Draw(const class="type">int seria_index, class="type">color clr, const class="type">int line_width, const class="type">bool chart_redraw)
  {
class=class="str">"cmt">//--- If canvas is not set, exit
   if(this.m_foreground==NULL)
     class="kw">return;

class=class="str">"cmt">//--- Preparing coefficients for converting values into pixels
   class="type">class="kw">double min=this.MaxValue(seria_index);
   class="type">class="kw">double max=this.MinValue(seria_index);

把序列值映射成画布像素的实战拆法

在自定义图表框架里,把数据序列画成折线,核心是先算坐标变换系数,再逐点转像素。下面这段逻辑直接决定了折线在 MT5 子窗口里的拉伸比例和留白。 边缘缩进取图表宽高的 5%,即 x_indent = 宽*0.05、y_indent = 高*0.05;纵向系数 k_y 用 (max-min) 除以(图表高 - 2*CELL_H - 2*y_indent),横向系数 k_x 用序列总数 size 除以(图表宽 - 2*x_indent)。这两个系数控制着数据如何被压缩进可视区。 起点 start_x 是左边界加 x_indent,start_y 是底边界减 4*CELL_H 再减 y_indent(注意代码里写了 2*CELL_H*2,实际就是 4 倍格高)。循环从 i=1 开始,把第 i 个值和第 i-1 个值分别转成 (x1,y1) 与 (x2,y2) 像素坐标,调用 Line() 连起来;最后用 m_foreground.Update() 决定是否重绘。 下面给出原文代码,并附逐行拆解要点: double size=this.m_seria[seria_index].Total(); // 取当前序列的总点数 double x_indent=this.m_bound_chart.Width()*0.05; // 横向留白为图表宽 5% double y_indent=this.m_bound_chart.Height()*0.05; // 纵向留白为图表高 5% double k_y=(max-min)/(this.m_bound_chart.Height()-2*CELL_H-2*y_indent); // 纵向像素/数值比 double k_x=(size)/(this.m_bound_chart.Width()-2*x_indent); // 横向像素/点数比 double start_x=this.m_bound_chart.left+x_indent; // 折线起始 X 像素 这套映射若 CELL_H 设得过大,start_y 会被抬太高,折线可能挤出底部坐标区;开 MT5 把 CELL_H 从默认改到 10~20 之间试渲染,能直观看到留白变化。外汇与贵金属自定义指标绘制涉及高风险环境,参数误设只影响显示、不预示行情方向。

MQL5 / C++
  class="type">class="kw">double size=this.m_seria[seria_index].Total();

class=class="str">"cmt">//--- Indentations from chart edge
  class="type">class="kw">double x_indent=this.m_bound_chart.Width()*class="num">0.05;
  class="type">class="kw">double y_indent=this.m_bound_chart.Height()*class="num">0.05;

class=class="str">"cmt">//--- Calculate coefficients
  class="type">class="kw">double k_y=(max-min)/(this.m_bound_chart.Height()-class="num">2*CELL_H-class="num">2*y_indent);
  class="type">class="kw">double k_x=(size)/(this.m_bound_chart.Width()-class="num">2*x_indent);

class=class="str">"cmt">//--- Coefficients
  class="type">class="kw">double start_x=this.m_bound_chart.left+x_indent;
  class="type">class="kw">double start_y=this.m_bound_chart.bottom-class="num">2*CELL_H*class="num">2-y_indent;

class=class="str">"cmt">//--- Now draw polyline passing through all points of series
  for(class="type">int i=class="num">1; i<size; i++)
    {
      class=class="str">"cmt">//--- convert values to pixels
      class="type">int x1=(class="type">int)((i-class="num">0)/k_x+start_x);  class=class="str">"cmt">// set value number horizontally
      class="type">int y1=(class="type">int)(start_y-(m_seria[seria_index].At(i)-min)/k_y);      class=class="str">"cmt">// vertically
      class="type">int x2=(class="type">int)((i-class="num">1-class="num">0)/k_x+start_x);class=class="str">"cmt">// set value number horizontally
      class="type">int y2=(class="type">int)(start_y-(m_seria[seria_index].At(i-class="num">1)-min)/k_y);    class=class="str">"cmt">// vertically
      class=class="str">"cmt">//--- Draw line from previous point to current one
      this.Line(x1, y1, x2, y2, clr, line_width);
    }
class=class="str">"cmt">//--- Updating canvas with chart redrawing(if flag is set)
  this.m_foreground.Update(chart_redraw);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Enumerations                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_FRAME_PROP                                                                  class=class="str">"cmt">// Frame properties
  {
   FRAME_PROP_PASS_NUM,                                                                class=class="str">"cmt">// Pass number
   FRAME_PROP_SHARPE_RATIO,                                                            class=class="str">"cmt">// Sharpe Ratio result
   FRAME_PROP_NET_PROFIT,                                                              class=class="str">"cmt">// Net Profit result
   FRAME_PROP_PROFIT_FACTOR,                                                          class=class="str">"cmt">// Profit Factor result
   FRAME_PROP_RECOVERY_FACTOR,                                                        class=class="str">"cmt">// Recovery Factor result
  };

◍ 把每轮优化结果装进一个类

做参数寻优时,MT5 的每轮 pass 会吐出一堆指标,散着存容易乱。下面这个类把单轮结果固化成一个对象,后面塞进 CArrayObj 就能按帧比优劣。 类里 protected 区挂了五个字段:m_pass 记轮次编号,m_sharpe_ratio 存夏普率,m_net_profit 是净利润,m_profit_factor 为盈利因子,m_recovery_factor 是恢复因子。外汇与贵金属回测中这些量波动大,高风险环境下别只看净值。 public 区成对出现 Set/Get 方法,比如 SetSharpeRatio 写值、SharpeRatio() 读值。注意 Pass() 返回 ulong,其余四个返回 double,类型别在调用时弄混。 [CODE] 里就是原生定义,粘贴进 MT5 的 include 头文件即可编译。实盘前建议先跑 50 轮 pass,看 m_recovery_factor 分布再决定过滤阈值。

MQL5 / C++
class CFrameData : class="kw">public CObject
  {
class="kw">protected:
   class="type">ulong             m_pass;                                          class=class="str">"cmt">// Pass number
   class="type">class="kw">double            m_sharpe_ratio;                                  class=class="str">"cmt">// Sharpe Ratio
   class="type">class="kw">double            m_net_profit;                                    class=class="str">"cmt">// Total profit
   class="type">class="kw">double            m_profit_factor;                                 class=class="str">"cmt">// Profitability
   class="type">class="kw">double            m_recovery_factor;                               class=class="str">"cmt">// Recovery factor
class="kw">public:
class=class="str">"cmt">//--- Setting frame properties(pass results)
   class="type">void              SetPass(const class="type">ulong pass)                        { this.m_pass=pass;            }
   class="type">void              SetSharpeRatio(const class="type">class="kw">double value)                { this.m_sharpe_ratio=value;  }
   class="type">void              SetNetProfit(const class="type">class="kw">double value)                 { this.m_net_profit=value;    }
   class="type">void              SetProfitFactor(const class="type">class="kw">double value)              { this.m_profit_factor=value; }
   class="type">void              SetRecoveryFactor(const class="type">class="kw">double value)            { this.m_recovery_factor=value;}
class=class="str">"cmt">//--- Returning frame properties(pass results)
   class="type">ulong             Pass(class="type">void)                               const { class="kw">return this.m_pass;            }
   class="type">class="kw">double            SharpeRatio(class="type">void)                        const { class="kw">return this.m_sharpe_ratio;  }
   class="type">class="kw">double            NetProfit(class="type">void)                         const { class="kw">return this.m_net_profit;    }
  };

「把回测帧指标直接打到日志里」

做优化器或批量回测时,每一帧(Frame)的优劣不能只藏在成员变量里,得有办法一眼在 MT5 专家日志中读出来。下面这段类方法把 Pass 序号、夏普、净利润、盈利因子、恢复因子全部格式化成字符串并逐行 Print,省去你手动拼 log 的麻烦。 ProfitFactor() 与 RecoveryFactor() 是两个只读取值器,直接返回 m_profit_factor 和 m_recovery_factor 的 double 值;前者衡量总盈利对总亏损的倍数关系,后者反映净值回升相对于最大回撤的效率,外汇与贵金属品种因点差滑点跳空,这两个值常比股票回测更敏感。 Description 系列方法统一用 StringFormat 做定点输出:夏普保留 2 位小数("%.2f"),Pass 用 %I64u 打无符号 64 位整数。Print() 里先打帧标识,再连续 5 次 PrintFormat 把 5 个指标缩进打印,复盘时你只要扫一眼日志就能筛掉 PF<1.2 或恢复因子<1 的帧。 让小布替你跑这套 把这段代码塞进你的 CFrame 类,开 MT5 策略测试器跑 200 代遗传优化,日志里会直接吐出 "Frame Pass: 183 / Sharpe Ratio: 1.37 / Profit Factor: 1.84" 这类行,比导出 XML 再解析快得多。

MQL5 / C++
  class="type">class="kw">double                ProfitFactor(class="type">void)                 const { class="kw">return this.m_profit_factor;  }
  class="type">class="kw">double                RecoveryFactor(class="type">void)                const { class="kw">return this.m_recovery_factor; }
class=class="str">"cmt">//--- Description of properties
  class="type">class="kw">string                PassDescription(class="type">void)               const { class="kw">return ::StringFormat("Pass: %I64u", this.m_pass);                    }
  class="type">class="kw">string                SharpeRatioDescription(class="type">void)         const { class="kw">return ::StringFormat("Sharpe Ratio: %.2f", this.m_sharpe_ratio);       }
  class="type">class="kw">string                NetProfitDescription(class="type">void)           const { class="kw">return ::StringFormat("Net Profit: %.2f", this.m_net_profit);           }
  class="type">class="kw">string                ProfitFactorDescription(class="type">void)        const { class="kw">return ::StringFormat("Profit Factor: %.2f", this.m_profit_factor);     }
  class="type">class="kw">string                RecoveryFactorDescription(class="type">void)      const { class="kw">return ::StringFormat("Recovery Factor: %.2f", this.m_recovery_factor); }
class=class="str">"cmt">//--- Printing frame properties to log
  class="type">void                  Print(class="type">void)
                     {
                     ::PrintFormat("Frame %s:", this.PassDescription());
                     ::PrintFormat(" - %s", this.SharpeRatioDescription());
                     ::PrintFormat(" - %s", this.NetProfitDescription());
                     ::PrintFormat(" - %s", this.ProfitFactorDescription());
                     ::PrintFormat(" - %s", this.RecoveryFactorDescription());
                     }
class=class="str">"cmt">//--- A method for comparing two objects
  class="kw">virtual class="type">int           Compare(const CObject *node,const class="type">int mode=class="num">0) const
                     {

按两位小数给回测指标排座次

在框架节点的比较逻辑里,夏普比率、净利润、盈利因子、恢复因子都不是拿原始浮点值硬比,而是先统一压到两位小数再判定大小。这样做能避开浮点尾差导致的排序抖动,尤其在 MT5 里跑大量历史帧对比时,1.234567 与 1.234568 会被正确归为同一档。 具体实现是把当前对象 this 和传入节点 obj 的对应方法返回值用 NormalizeDouble(...,2) 包一层,然后走 > 返 1、< 返 -1、相等返 0 的三元结构。比如夏普比率比较段:先算 this.SharpeRatio() 两位化,再算 obj.SharpeRatio() 两位化,二者差在第三位小数及以后直接被舍弃。 你开 MT5 随便找个 EA 的回测报告,把夏普从 1.31 改成 1.3099,用这套比较函数仍会判为相等;若改成 1.32 才会翻成大于。外汇与贵金属品种受点差跳空影响,这类两位小数截断可能让相邻帧的优劣边界变模糊,调参时建议先验证截断精度是否匹配你的样本波动。

MQL5 / C++
class=class="str">"cmt">//--- Compare real values as two-digit values
   const CFrameData *obj=node;
   class="kw">switch(mode)
     {
      case FRAME_PROP_SHARPE_RATIO       :  class="kw">return(::NormalizeDouble(this.SharpeRatio(),class="num">2)  > ::NormalizeDouble(obj.SharpeRatio(),class="num">2)     ?  class="num">1 :
                                                                  ::NormalizeDouble(this.SharpeRatio(),class="num">2)  < ::NormalizeDouble(obj.SharpeRatio(),class="num">2)     ? -class="num">1 : class="num">0);
      case FRAME_PROP_NET_PROFIT          :  class="kw">return(::NormalizeDouble(this.NetProfit(),class="num">2)      > ::NormalizeDouble(obj.NetProfit(),class="num">2)         ?  class="num">1 :
                                                                  ::NormalizeDouble(this.NetProfit(),class="num">2)      < ::NormalizeDouble(obj.NetProfit(),class="num">2)         ? -class="num">1 : class="num">0);
      case FRAME_PROP_PROFIT_FACTOR       :  class="kw">return(::NormalizeDouble(this.ProfitFactor(),class="num">2)  > ::NormalizeDouble(obj.ProfitFactor(),class="num">2)   ?  class="num">1 :
                                                                  ::NormalizeDouble(this.ProfitFactor(),class="num">2)  < ::NormalizeDouble(obj.ProfitFactor(),class="num">2)   ? -class="num">1 : class="num">0);
      case FRAME_PROP_RECOVERY_FACTOR     :  class="kw">return(::NormalizeDouble(this.RecoveryFactor(),class="num">2)> ::NormalizeDouble(obj.RecoveryFactor(),class="num">2) ?  class="num">1 :

◍ 回测帧数据的比较与查看器骨架

这段实现里,CFrameData 的重载比较逻辑按属性分派:当排序键是回撤恢复系数时,用 NormalizeDouble 截断到 2 位小数再比大小,避免浮点误差导致同值帧乱序;其余情况(如 FRAME_PROP_PASS_NUM)直接比 Pass 次数,返回 1 / -1 / 0 三态。 构造函数把 pass、sharpe_ratio、net_profit、profit_factor、recovery_factor 一次性落成员,默认构造全置 0,析构为空实现,说明该结构只是纯数据载体,不持有需要释放的资源。 CFrameViewer 紧接着声明了画布宽高(m_w / m_h)、选中序列颜色与线宽、优化完成标记 m_completed,以及一个临时帧对象 m_frame_tmp 用于按属性查找。你在 MT5 里写优化结果浏览器时,可以直接套这个私有成员布局,省去重复设计。 外汇与贵金属品种回测受点差跳空影响大,这类帧比较只解决排序稳定性,不代表某组参数实盘概率更高,上真金前务必多周期验证。

MQL5 / C++
::NormalizeDouble(this.RecoveryFactor(),class="num">2)< ::NormalizeDouble(obj.RecoveryFactor(),class="num">2) ? -class="num">1 : class="num">0);
		class=class="str">"cmt">//---FRAME_PROP_PASS_NUM
		class="kw">default					: class="kw">return(this.Pass()>obj.Pass() ? class="num">1 : this.Pass()<obj.Pass() ? -class="num">1 : class="num">0);
		 }
	 }

class=class="str">"cmt">//--- Constructors/destructor
	 CFrameData(const class="type">ulong pass, const class="type">class="kw">double sharpe_ratio, const class="type">class="kw">double net_profit, const class="type">class="kw">double profit_factor, const class="type">class="kw">double recovery_factor) :
		m_pass(pass), m_sharpe_ratio(sharpe_ratio), m_net_profit(net_profit), m_profit_factor(profit_factor), m_recovery_factor(recovery_factor) {}
	 CFrameData(class="type">void) :
		m_pass(class="num">0), m_sharpe_ratio(class="num">0), m_net_profit(class="num">0), m_profit_factor(class="num">0), m_recovery_factor(class="num">0) {}
	~CFrameData(class="type">void) {}
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ▸Frame viewer class													     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CFrameViewer : class="kw">public CObject
{
class="kw">private:
  class="type">int			 m_w;						 class=class="str">"cmt">// Chart width
  class="type">int			 m_h;						 class=class="str">"cmt">// Chart height
  class="type">color		 m_selected_color;			 class=class="str">"cmt">// Color of selected series from top three
  class="type">uint			 m_line_width;				 class=class="str">"cmt">// Width of line of selected series from top three
  class="type">bool			 m_completed;				 class=class="str">"cmt">// Optimization completion flag

  CFrameData	 m_frame_tmp;				 class=class="str">"cmt">// Frame object for searching by class="kw">property

「多标签优化面板里的对象与索引映射」

在 MT5 自建优化报告界面时,通常用 CTabControl 承载多个评价维度的分页。上面这段声明里,m_list_frames 管所有子框架生命周期,m_tab_control 是分页容器,下面挂了 0~4 共 5 个标签:0 号做基础优化(带进度条 CColorProgressBar),1~4 号分别按夏普比率、净利润、盈利因子、恢复因子展示参数表与统计图。 注意 0 号标签里 m_table_stat_0 被重复声明了两次,编译虽能过但属于冗余,实盘面板代码建议删掉一行以免维护时改错位置。 GetTableInputs(uint tab_id) 用 switch 把标签序号映射到对应的 CTableDataControl 指针,目前只展开了 case 0~2。若你要在 EA 里动态读取某标签的输入参数表,补完 case 3、4 并返回 m_table_inp_3.Get() / m_table_inp_4.Get() 即可,缺了会返回空指针导致面板刷新报错。 外汇与贵金属品种做这类优化回测波动剧烈,参数表里的数值仅代表历史样本表现,换周期或滑点设定后分布可能明显偏移,验证时务必用真实点差重跑。

MQL5 / C++
  CArrayObj        m_list_frames;           class=class="str">"cmt">// List of frames
  CTabControl      m_tab_control;           class=class="str">"cmt">// Tab Control

class=class="str">"cmt">//--- Declare tab objects on Tab Control
class=class="str">"cmt">//--- Tab class="num">0 (Optimization) of Tab Control
  CTableDataControl m_table_inp_0;           class=class="str">"cmt">// Table of optimization parameters on tab class="num">0
  CTableDataControl m_table_stat_0;          class=class="str">"cmt">// Table of optimization results on tab class="num">0
  CTableDataControl m_table_stat_0;          class=class="str">"cmt">// Table of optimization results on tab class="num">0
  CColorProgressBar*m_progress_bar;          class=class="str">"cmt">// Progress bar on optimization chart on tab class="num">0

class=class="str">"cmt">//--- Tab class="num">1 (Sharpe Ratio) of Tab Control
  CTableDataControl m_table_inp_1;           class=class="str">"cmt">// Table of optimization parameters on tab class="num">1
  CTableDataControl m_table_stat_1;          class=class="str">"cmt">// Table of optimization results on tab class="num">1
  CStatChart       m_chart_stat_1;           class=class="str">"cmt">// Table of optimization results on tab class="num">1

class=class="str">"cmt">//--- Tab class="num">2 (Net Profit) of Tab Control
  CTableDataControl m_table_inp_2;           class=class="str">"cmt">// Table of optimization parameters on tab class="num">2
  CTableDataControl m_table_stat_2;          class=class="str">"cmt">// Table of optimization results on tab class="num">2
  CStatChart       m_chart_stat_2;           class=class="str">"cmt">// Chart of optimization results on tab class="num">2

class=class="str">"cmt">//--- Tab class="num">3 (Profit Factor) of Tab Control
  CTableDataControl m_table_inp_3;           class=class="str">"cmt">// Table of optimization parameters on tab class="num">3
  CTableDataControl m_table_stat_3;          class=class="str">"cmt">//  Table of optimization results on tab class="num">3
  CStatChart       m_chart_stat_3;           class=class="str">"cmt">// Chart of optimization results on tab class="num">3

class=class="str">"cmt">//--- Tab class="num">4 (Recovery Factor) of Tab Control
  CTableDataControl m_table_inp_4;           class=class="str">"cmt">// Table of optimization parameters on tab class="num">4
  CTableDataControl m_table_stat_4;          class=class="str">"cmt">// able of optimization results on tab class="num">4
  CStatChart       m_chart_stat_4;           class=class="str">"cmt">// Chart of optimization results on tab class="num">4
class="kw">protected:
class=class="str">"cmt">//--- Returns pointer to table of optimization parameters by tab index
  CTableDataControl*GetTableInputs(const class="type">uint tab_id)
                     {
                       class="kw">switch(tab_id)
                         {
                          case class="num">0 : class="kw">return this.m_table_inp_0.Get();
                          case class="num">1 : class="kw">return this.m_table_inp_1.Get();
                          case class="num">2 : class="kw">return this.m_table_inp_2.Get();

按标签索引取回优化结果表与图

在 MT5 优化面板的多标签结构里,用统一入口按 tab_id 拿数据比直接暴露成员安全。下面三个方法分别返回输入参数表、统计结果表和统计图表指针,索引 0~4 对应五个标签页。 switch 的 default 分支一律返回 NULL,意味着传入越界 id 不会崩,但调用方必须自行判空,否则后续访问会触发无效指针异常。 实际调试时,可在 OnInit 里循环调 GetTableStats(i) 打印非空数量,验证五个标签是否都正确绑定。外汇与贵金属品种优化受点差跳空影响大,回测结果仅代表历史概率,实盘有滑点风险。

MQL5 / C++
case class="num">3 : class="kw">return this.m_table_inp_3.Get();
case class="num">4 : class="kw">return this.m_table_inp_4.Get();
class="kw">default: class="kw">return NULL;
}
}
class=class="str">"cmt">//--- Returns pointer to table of optimization results by tab index
  CTableDataControl*GetTableStats(const class="type">uint tab_id)
    {
     class="kw">switch(tab_id)
      {
       case class="num">0 : class="kw">return this.m_table_stat_0.Get();
       case class="num">1 : class="kw">return this.m_table_stat_1.Get();
       case class="num">2 : class="kw">return this.m_table_stat_2.Get();
       case class="num">3 : class="kw">return this.m_table_stat_3.Get();
       case class="num">4 : class="kw">return this.m_table_stat_4.Get();
       class="kw">default: class="kw">return NULL;
      }
    }
class=class="str">"cmt">//--- Returns pointer to chart of optimization results by tab index
  CStatChart       *GetChartStats(const class="type">uint tab_id)
    {
     class="kw">switch(tab_id)
      {
       case class="num">0 : class="kw">return this.m_chart_stat_0.Get();
       case class="num">1 : class="kw">return this.m_chart_stat_1.Get();
       case class="num">2 : class="kw">return this.m_chart_stat_2.Get();
       case class="num">3 : class="kw">return this.m_chart_stat_3.Get();

◍ 框架对象入列与绘制接口的防呆处理

在优化统计面板里,AddFrame 负责把单个 CFrameData 塞进帧列表,但先做了空指针拦截:传入 NULL 时直接 PrintFormat 报错并返回 false,避免后续排序和查找踩到野指针。 入列前会用 m_frame_tmp.SetPass 暂存帧的 Pass 值,再对 m_list_frames 按 FRAME_PROP_PASS_NUM 排序,随后 Search 查重——若 index 大于 WRONG_VALUE 说明已存在,同样返回 false 不重复添加。这一套让 MT5 跑批量回测时,面板数据不会因重复帧而错位。 绘制侧暴露了四个方法:TableStatDraw 画统计表、TableInpDraw 画输入参数表(带 rows 控制行数)、ChartOptDraw 画优化曲线、DrawDataChart 在指定 tab_id 下统调前三者。x/y/w/h 与 chart_redraw 参数让你能精确控制面板坐标与是否强制重绘,实盘外接显示器布局时很实用。 把下面这段贴进 MT5 的同类面板类里,能直接验证空帧拦截与查重逻辑是否按预期走。

MQL5 / C++
case class="num">4 : class="kw">return this.m_chart_stat_4.Get();
				class="kw">default: class="kw">return NULL;
				}
		 }
class=class="str">"cmt">//--- Adds frame object to list
	 class="type">bool				AddFrame(CFrameData *frame)
				{
					if(frame==NULL)
						{
						 ::PrintFormat("%s: Error: Empty object passed",__FUNCTION__);
						 class="kw">return false;
						}
					this.m_frame_tmp.SetPass(frame.Pass());
					this.m_list_frames.Sort(FRAME_PROP_PASS_NUM);
					class="type">int index=this.m_list_frames.Search(frame);
					if(index>WRONG_VALUE)
						 class="kw">return false;
					class="kw">return this.m_list_frames.Add(frame);
				 }
class=class="str">"cmt">//--- Draws table of optimization statistics on specified tab
	 class="type">void				TableStatDraw(const class="type">uint tab_id, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">bool chart_redraw);
class=class="str">"cmt">//--- Draws table of input optimization parameters on specified tab
	 class="type">void				TableInpDraw(const class="type">uint tab_id, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">uint rows, const class="type">bool chart_redraw);
class=class="str">"cmt">//--- Draws chart of optimization on specified tab
	 class="type">void				ChartOptDraw(const class="type">uint tab_id, const class="type">bool opt_completed, const class="type">bool chart_redraw);
class=class="str">"cmt">//--- Draws data tables and optimization chart
	 class="type">void				DrawDataChart(const class="type">uint tab_id);

「把优化结果画到图表上的几个关键接口」

在 MT5 优化器跑完之后,想把前几名参数组的可视化结果直接落到图表,核心靠一组类内方法。下面这段声明里,DrawBestFrameData 负责按优化准则画出前三名 pass 的曲线,ReplayFrames 则可按 delay_ms 毫秒间隔重放每一帧,方便肉眼核对参数演进路径。 DrawFrameData 与 DrawFrameDataByPass 的区别在于入参:前者吃当前帧文本与 pass 引用,后者直接指定 pass_num 取历史帧。FillArrayBestFrames 会按 tab_id 把前三名的帧索引塞进 array_passes,之后 DrawBestFrameDataAll 一次性把所有标签的前三名都绘出。 公开方法里有两个setter值得当场改:SetSelectedLineWidth 改选中线宽,SetProfitColorLine 遍历 TabsTotal() 拿到的总页数(从1计)逐个设盈利系列颜色。下面这段是后者的实现,注意它跳过了索引0。 别在回测里硬写颜色 循环从 i=1 开始而不是0,说明 tab 0 大概率是总览页或占位页,直接改可能无效甚至触发空指针。先 Print(TabsTotal()) 确认页数再调。

MQL5 / C++
class=class="str">"cmt">//--- Setting class="type">color of profitable series
  class="type">void              SetProfitColorLine(const class="type">color clr)
    {
     class="type">int total=this.m_tab_control.TabsTotal();
     for(class="type">int i=class="num">1; i<total; i++)
       {
        CStatChart *chart=this.GetChartStats(i);
        if(chart!=NULL)
           chart.SetProfitColorLine(clr);
       }
    }

批量改线色与回测事件的接口设计

在自定义统计面板类里,亏损线颜色和选中线颜色都走统一的遍历逻辑。两个方法都先取 Tab 控件的总页数 TabsTotal(),再从索引 1 开始循环(默认 0 号往往是汇总页,不染具体图表线),拿到每页的 CStatChart 指针后判空再下发颜色,避免空指针崩在 OnTester 跑大批参数时。 下面这段是这两个方法的骨架,注意循环起点写死为 1 而不是 0,这是多标签统计面板常见的跳过总览页做法: void SetLossColorLine(const color clr) { int total=this.m_tab_control.TabsTotal(); for(int i=1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetLossColorLine(clr); } } void SetSelectedLineColor(const color clr) { int total=this.m_tab_control.TabsTotal(); for(int i=1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetSelectedLineColor(clr); } } 回测生命周期也暴露了四个钩子:OnTesterInit 在初始化时收线条数、选中线宽和颜色三个参数,OnTesterPass 每过一组参数触发一次,OnTesterDeinit 收尾,OnTester 回传一个 double 值供自定义评价。图表交互则只留了一个 OnChartEvent,把 id、lparam、dparam、sparam 和 delay_ms 全透传出来,由派生类自己分流。 如果你在 MT5 里写自己的统计标签控件,直接复用这套遍历染色的写法,能省掉每个子图单独绑事件的麻烦;外汇与贵金属品种波动大、滑点不可控,回测钩子里输出的任何“优劣”都只是历史概率,实盘前务必用真实点差重跑。

MQL5 / C++
class="type">void SetLossColorLine(const class="type">color clr)
  {
   class="type">int total=this.m_tab_control.TabsTotal();
   for(class="type">int i=class="num">1; i<total; i++)
     {
      CStatChart *chart=this.GetChartStats(i);
      if(chart!=NULL)
         chart.SetLossColorLine(clr);
     }
  }
class=class="str">"cmt">//--- Setting class="type">color of selected series
class="type">void SetSelectedLineColor(const class="type">color clr)
  {
   class="type">int total=this.m_tab_control.TabsTotal();
   for(class="type">int i=class="num">1; i<total; i++)
     {
      CStatChart *chart=this.GetChartStats(i);
      if(chart!=NULL)
         chart.SetSelectedLineColor(clr);
     }
  }

class=class="str">"cmt">//--- Event handlers of strategy tester
class="type">void OnTester(const class="type">class="kw">double OnTesterValue);
class="type">int OnTesterInit(const class="type">int lines, const class="type">int selected_line_width, const class="type">color selected_line_color);
class="type">void OnTesterPass(class="type">void);
class="type">void OnTesterDeinit(class="type">void);

class=class="str">"cmt">//--- Chart event handlers
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,const class="type">int delay_ms);

◍ 回测前把图表先藏起来再挂标签控件

在 EA 的 OnTesterInit 里做可视化回测面板,第一步不是画图,而是把跑帧模式的主图表先摘干净。代码先取 ChartID,随后用 ChartSetInteger 连关 CHART_SHOW 和 CHART_IS_DOCKED,失败就 PrintFormat 报错并返回 INIT_FAILED,这样浮窗才不会和原图叠在一起。 紧接着 ObjectsDeleteAll 清空全部图形对象,再按 ChartGetInteger 拿到的像素宽高 w、h 去建一个覆盖全图的 Tab Control。示例里建完就往里加五个标签页,实际你改个数字就能控面板分区数量。 构造函数里顺手把 m_w、m_h 也用 CHART_WIDTH_IN_PIXELS / CHART_HEIGHT_IN_PIXELS 取了,默认线宽 1、选中色 clrDodgerBlue,析构只清 m_list_frames。外汇与贵金属回测涉及高杠杆高风险,面板只是辅助观察,不构成任何方向暗示。

MQL5 / C++
class="type">void OnTabSwitchEvent(const class="type">int tab_id);
class="type">void OnButtonSwitchEvent(const class="type">int tab_id, const class="type">uint butt_id);

class="kw">public:
class=class="str">"cmt">//--- Constructor/destructor
 CFrameViewer(class="type">void);
 ~CFrameViewer(class="type">void){ this.m_list_frames.Clear(); }
 };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor |
class=class="str">"cmt">//+------------------------------------------------------------------+
CFrameViewer::CFrameViewer(class="type">void) : m_completed(false), m_progress_bar(NULL), m_selected_color(clrDodgerBlue), m_line_width(class="num">1)
 {
class=class="str">"cmt">//--- Chart window size
 this.m_w=(class="type">int)::ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS);
 this.m_h=(class="type">int)::ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS);
class=class="str">"cmt">//--- Get pointer to progress bar from statistics chart object
 this.m_progress_bar=this.m_chart_stat_0.GetProgressBar();
 this.m_list_frames.Clear();
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| It must be called in handler of OnTesterInit() Expert Advisor |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CFrameViewer::OnTesterInit(const class="type">int lines, const class="type">int selected_line_width, const class="type">color selected_line_color)
 {
class=class="str">"cmt">//--- Chart ID with expert running in Frame mode
 class="type">long chart_id=::ChartID();

class=class="str">"cmt">//--- Preparing a floating chart for drawing statistics tables and balance lines
 ::ResetLastError();
 if(!::ChartSetInteger(chart_id, CHART_SHOW, false))
  {
   ::PrintFormat("%s: ChartSetInteger() failed. Error %d",__FUNCTION__, GetLastError());
   class="kw">return INIT_FAILED;
  }
 if(!::ChartSetInteger(chart_id, CHART_IS_DOCKED, false))
  {
   ::PrintFormat("%s: ChartSetInteger() failed. Error %d",__FUNCTION__, GetLastError());
   class="kw">return INIT_FAILED;
  }
class=class="str">"cmt">//--- Clearing chart completely of all graphical objects
 ::ObjectsDeleteAll(chart_id);

class=class="str">"cmt">//--- Based on chart size create Tab Control with five tabs
 class="type">int w=(class="type">int)::ChartGetInteger(chart_id, CHART_WIDTH_IN_PIXELS);
 class="type">int h=(class="type">int)::ChartGetInteger(chart_id, CHART_HEIGHT_IN_PIXELS);
 if(this.m_tab_control.Create("TabControl", "", class="num">0, class="num">0, w, h))
  {
   class=class="str">"cmt">//--- If control is created successfully, add five tabs to it

「把回测指标铺进五个分页的画法」

在 MT5 自定义面板里做优化结果可视化时,常需要把 Sharpe、净利润、盈利因子、恢复因子分开展示。下面这段逻辑先建了 5 个标签页,索引 0 到 4 分别对应 Optimization、Sharpe Ratio、Net Profit、Profit Factor、Recovery Factor,任一页添加失败就直接返回 INIT_FAILED,避免半残界面。 循环里用三元表达式给每个 tab 写名字,res 用按位与累积每次 AddTab 的返回值,只要有一次 false 整段就废。这种写法比挨个 if 判断更紧凑,但调试时得留心具体是第几个 tab 挂了。 标签页建好后,每个页都通过 GetTabBackground / GetTabForeground 拿到独立的 CCanvas 指针,再塞给对应的统计图表对象。注意 m_chart_stat_0 到 4 的 SetTabID 必须和 tab 索引对齐,否则画出来的线会跑到别的页。 最后给图表设背景色和前景色,例如 m_chart_stat_0 用 clrIvory 配 C'200,200,200' 灰线。外汇与贵金属回测高风险,参数敏感,建议在策略测试器里跑完优化后再用这套面板看分布,别拿单组结果当规律。

MQL5 / C++
class="type">bool res=true;
for(class="type">int i=class="num">0; i<class="num">5; i++)
  {
   class="type">class="kw">string tab_text=(i==class="num">1 ? "Sharpe Ratio" : i==class="num">2 ? "Net Profit" : i==class="num">3 ? "Profit Factor" : i==class="num">4 ? "Recovery Factor" : "Optimization");
   res &=this.m_tab_control.AddTab(i, tab_text);
  }
if(!res)
  {
   ::PrintFormat("%s: Errors occurred while adding tabs to the Tab Control",__FUNCTION__);
   class="kw">return INIT_FAILED;
  }
 }
 else
  {
   Print("Tab Control creation failed");
   class="kw">return INIT_FAILED;
  }

class=class="str">"cmt">//--- CCanvas objects in workspace of tab class="num">0 (Optimization) for drawing background images and text
  CCanvas *tab0_background=this.m_tab_control.GetTabBackground(class="num">0);
  CCanvas *tab0_foreground=this.m_tab_control.GetTabForeground(class="num">0);
class=class="str">"cmt">//--- CCanvas objects in workspace of tab class="num">1 (Sharpe Ratio) for drawing background images and text
  CCanvas *tab1_background=this.m_tab_control.GetTabBackground(class="num">1);
  CCanvas *tab1_foreground=this.m_tab_control.GetTabForeground(class="num">1);
class=class="str">"cmt">//--- CCanvas objects in workspace of tab class="num">2 (Net Profit) for drawing background images and text
  CCanvas *tab2_background=this.m_tab_control.GetTabBackground(class="num">2);
  CCanvas *tab2_foreground=this.m_tab_control.GetTabForeground(class="num">2);
class=class="str">"cmt">//--- CCanvas objects in workspace of tab class="num">3 (Profit Factor) for drawing background images and text
  CCanvas *tab3_background=this.m_tab_control.GetTabBackground(class="num">3);
  CCanvas *tab3_foreground=this.m_tab_control.GetTabForeground(class="num">3);
class=class="str">"cmt">//--- CCanvas objects in workspace of tab class="num">4 (Recovery Factor) for drawing background images and text
  CCanvas *tab4_background=this.m_tab_control.GetTabBackground(class="num">4);
  CCanvas *tab4_foreground=this.m_tab_control.GetTabForeground(class="num">4);

class=class="str">"cmt">//--- Set tab identifiers for objects of optimization statistics charts
  this.m_chart_stat_0.SetTabID(class="num">0);
  this.m_chart_stat_1.SetTabID(class="num">1);
  this.m_chart_stat_2.SetTabID(class="num">2);
  this.m_chart_stat_3.SetTabID(class="num">3);
  this.m_chart_stat_4.SetTabID(class="num">4);

class=class="str">"cmt">//--- Indicate for objects of statistics charts that we draw on tab with corresponding index
  this.m_chart_stat_0.SetCanvas(tab0_background, tab0_foreground);
  this.m_chart_stat_1.SetCanvas(tab1_background, tab1_foreground);
  this.m_chart_stat_2.SetCanvas(tab2_background, tab2_foreground);
  this.m_chart_stat_3.SetCanvas(tab3_background, tab3_foreground);
  this.m_chart_stat_4.SetCanvas(tab4_background, tab4_foreground);

class=class="str">"cmt">//--- Set number of series on optimization statistics charts
  this.m_chart_stat_0.SetLines(lines);
  this.m_chart_stat_1.SetLines(lines);
  this.m_chart_stat_2.SetLines(lines);
  this.m_chart_stat_3.SetLines(lines);
  this.m_chart_stat_4.SetLines(lines);

class=class="str">"cmt">//--- Setting background and foreground colors of optimization statistics charts
  this.m_chart_stat_0.SetBackColor(clrIvory);
  this.m_chart_stat_0.SetForeColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;);

优化面板多标签的绘制与按钮兜底

在自定义优化报告面板里,四个统计子视图先统一刷成象牙底配浅灰字:m_chart_stat_1 到 _4 都走 SetBackColor(clrIvory) 与 SetForeColor(C'200,200,200'),视觉上把最优通道高亮区和普通数据区拉开层次。 接着设定被选中最优线的线宽与颜色(selected_line_width / selected_line_color),再对 0 号标签调用 TableStatDraw(0,4,4,CELL_W*2,CELL_H,false) 与 TableInpDraw 把统计表和输入参数表铺出来,ChartOptDraw 负责画进度与优化图。

  • 号标签额外建了回放按钮:CreateButtonReplay 若返回 false 就 Print('Button Replay creation failed') 并 return INIT_FAILED,这步失败会直接让面板初始化废掉。

1、2 号标签结构类似,只是分别绑定 Sharpe 与 Net Profit 排序,并各自建结果选择按钮 CreateButtonResults;任一个返回 false 同样走 INIT_FAILED。外汇与贵金属品种回测优化受点差跳空影响大,面板能跑通不代表实盘概率占优,开 MT5 把这段粘进 EA 的 OnInit 末端即可验证标签是否按预期渲染。

MQL5 / C++
this.m_chart_stat_1.SetBackColor(clrIvory);
this.m_chart_stat_1.SetForeColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;);
this.m_chart_stat_2.SetBackColor(clrIvory);
this.m_chart_stat_2.SetForeColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;);
this.m_chart_stat_3.SetBackColor(clrIvory);
this.m_chart_stat_3.SetForeColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;);
this.m_chart_stat_4.SetBackColor(clrIvory);
this.m_chart_stat_4.SetForeColor(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;);

class=class="str">"cmt">//--- Set thickness and class="type">color of selected line of best pass
this.SetSelectedLineWidth(selected_line_width);
this.SetSelectedLineColor(selected_line_color);

class=class="str">"cmt">//--- Draw two tables with optimization results and input parameters on tab class="num">0 (Optimization),
class=class="str">"cmt">//--- and window with progress bar for printing charts and optimization process
this.TableStatDraw(class="num">0, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false);
this.TableInpDraw(class="num">0, class="num">4, this.m_table_stat_0.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, class="num">0, false);
this.ChartOptDraw(class="num">0, this.m_completed, true);
class=class="str">"cmt">//--- Create optimization replay button on tab class="num">0
if(!this.m_chart_stat_0.CreateButtonReplay())
  {
    Print("Button Replay creation failed");
    class="kw">return INIT_FAILED;
  }

class=class="str">"cmt">//--- Draw two tables with optimization results and input parameters on tab class="num">1 (Sharpe Ratio),
class=class="str">"cmt">//--- and window to print charts of optimization results
this.TableStatDraw(class="num">1, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false);
this.TableInpDraw(class="num">1, class="num">4, this.m_table_stat_1.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, class="num">0, false);
this.ChartOptDraw(class="num">1, this.m_completed, true);
class=class="str">"cmt">//--- Create result selection button on tab class="num">1
if(!this.m_chart_stat_1.CreateButtonResults())
  {
    Print("Tab1: There were errors when creating the result buttons");
    class="kw">return INIT_FAILED;
  }

class=class="str">"cmt">//--- Draw two tables with optimization results and input parameters on tab class="num">2 (Net Profit),
class=class="str">"cmt">//--- and window to print charts of optimization results
this.TableStatDraw(class="num">2, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false);
this.TableInpDraw(class="num">2, class="num">4, this.m_table_stat_1.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, class="num">0, false);
this.ChartOptDraw(class="num">2, this.m_completed, true);
class=class="str">"cmt">//--- Create result selection button on tab class="num">2
if(!this.m_chart_stat_2.CreateButtonResults())
  {
    Print("Tab2: There were errors when creating the result buttons");
    class="kw">return INIT_FAILED;
  }

class=class="str">"cmt">//--- Draw two tables with optimization results and input parameters on tab class="num">3 (Profit Factor),
class=class="str">"cmt">//--- and window to print charts of optimization results

◍ 回测收尾时把标签刷成可重放状态

优化跑完之后,框架会在第 3、4 个标签页分别再画两张表:统计结果表用 TableStatDraw(3,4,4,...) 和 (4,4,4,...),输入参数表紧接在 m_table_stat_1 底部往下偏移 4 像素处用 TableInpDraw 绘制,单元格宽取 CELL_W*2、高取 CELL_H。两张表都带 ChartOptDraw 把优化曲线图渲染出来,随后必须调 CreateButtonResults 生成结果选择按钮,任一返回失败就 Print 报错并 return INIT_FAILED,否则最终 return INIT_SUCCEEDED。 OnTesterDeinit 里先抓标签页 0 的 background 和 foreground 两块画布指针,空指针直接 return。随后把 m_completed 置 true,并从 m_chart_stat_0 取表头坐标算中心点 (x,y) = ((x1+x2)/2, (y1+y2)/2)。 背景块用 clrLightGreen 实色填充,前景块填 0x00FFFFFF 把原表头文字擦掉;再写一行"Optimization Complete: Click to Replay",字体 Calibri、粗体、MidnightBlue 色、居中对齐。background.Update(false) 不触发重绘事件,foreground.Update(true) 立即上屏——这时用户点表头就能重放优化过程。外汇与贵金属回测仅反映历史概率,实盘仍属高风险。

MQL5 / C++
   this.TableStatDraw(class="num">3, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false);
   this.TableInpDraw(class="num">3, class="num">4, this.m_table_stat_1.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, class="num">0, false);
   this.ChartOptDraw(class="num">3, this.m_completed, true);
class=class="str">"cmt">//--- Create result selection button on tab class="num">3
   if(!this.m_chart_stat_3.CreateButtonResults())
     {
       Print("Tab3: There were errors when creating the result buttons");
       class="kw">return INIT_FAILED;
     }
   
class=class="str">"cmt">//--- Draw two tables with optimization results and input parameters on tab class="num">4 (Recovery Factor),
class=class="str">"cmt">//--- and window to print charts of optimization results
   this.TableStatDraw(class="num">4, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false);
   this.TableInpDraw(class="num">4, class="num">4, this.m_table_stat_1.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, class="num">0, false);
   this.ChartOptDraw(class="num">4, this.m_completed, true);
class=class="str">"cmt">//--- Create result selection button on tab class="num">4
   if(!this.m_chart_stat_4.CreateButtonResults())
     {
       Print("Tab4: There were errors when creating the result buttons");
       class="kw">return INIT_FAILED;
     }
   
   class="kw">return INIT_SUCCEEDED;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| It must be called in handler of OnTesterDeinit() Expert Advisor  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameViewer::OnTesterDeinit(class="type">void)
  {
class=class="str">"cmt">//--- Get pointers to canvas to draw background and foreground
   CCanvas *background=this.m_tab_control.GetTabBackground(class="num">0);
   CCanvas *foreground=this.m_tab_control.GetTabForeground(class="num">0);
   
   if(background==NULL || foreground==NULL)
      class="kw">return;
   
class=class="str">"cmt">//--- Set optimization completion flag
   this.m_completed=true;
   
class=class="str">"cmt">//--- Chart header coordinates
   class="type">int x1=this.m_chart_stat_0.HeaderX1();
   class="type">int y1=this.m_chart_stat_0.HeaderY1();
   class="type">int x2=this.m_chart_stat_0.HeaderX2();
   class="type">int y2=this.m_chart_stat_0.HeaderY2();
       
   class="type">int x=(x1+x2)/class="num">2;
   class="type">int y=(y1+y2)/class="num">2;
       
class=class="str">"cmt">//--- Repaint background and erase header text
   background.FillRectangle(x1, y1, x2, y2, ::ColorToARGB(clrLightGreen));
   foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF);
   
class=class="str">"cmt">//--- Change text and class="type">color of header 
   class="type">class="kw">string text="Optimization Complete: Click to Replay";
   foreground.FontSet("Calibri", -class="num">100, FW_BLACK);
   foreground.TextOut(x, y, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);
   background.Update(false);
   foreground.Update(true);
   
class=class="str">"cmt">//--- Get active tab index and call method for controlling print of control objects on optimization charts

「在 OnTester 里抓取资金曲线与四项指标」

EA 做优化时,想实时把每趟 pass 的余额轨迹和关键统计丢给可视化框架,正统落点是 CFrameViewer::OnTester()。它必须在 EA 的 OnTester() 处理器里被调用,否则框架拿不到这一趟的结算数据。 函数先以 STAT_INITIAL_DEPOSIT 初始化 balance_current,再用 HistorySelect(0, TimeCurrent()) 拉全量历史。若返回失败会 PrintFormat 报错并直接 return,这一步在 MT5 策略测试器里常因历史缓存未就绪而漏数据,值得手动加断点验证。 随后遍历 HistoryDealsTotal(),只处理 DEAL_ENTRY_OUT 与 DEAL_ENTRY_INOUT 的平仓成交:把每笔 DEAL_PROFIT 累加到 balance_current,并 ArrayResize 扩容后写入 balance[]。data_count 就是实际平仓笔数,也是后续数组长度的依据。 真正发给帧的数据由 data[] 承载:前面 DATA_COUNT 个槽位塞统计值——data[0] 夏普、data[1] 净利润、data[2] 盈利因子、data[3] 恢复因子,后面才用 ArrayCopy 把 balance 曲线接上。开 MT5 把这段接进自己的 EA,跑一遍 EURUSD 的 1 分钟优化,就能在帧里看到四指标加余额线的原始拼接。外汇与贵金属优化本身高风险,历史表现不预示实盘概率。

MQL5 / C++
class="type">int tab_selected=this.m_tab_control.GetSelectedTabID();
this.ControlObjectsView(tab_selected);

class=class="str">"cmt">//--- On each tab(class="num">1-class="num">4), draw charts of three best optimization passes
this.DrawBestFrameDataAll();
::ChartRedraw();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Prepares array of balance values and sends it in frame           |
class=class="str">"cmt">//| It must be called in Expert Advisor in OnTester() handler        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameViewer::OnTester(const class="type">class="kw">double OnTesterValue)
  {
class=class="str">"cmt">//--- Variables for working with pass results
  class="type">class="kw">double balance[];
  class="type">int    data_count=class="num">0;
  class="type">class="kw">double balance_current=::TesterStatistics(STAT_INITIAL_DEPOSIT);
class=class="str">"cmt">//--- Temporary variables for working with trades
  class="type">ulong  ticket=class="num">0;
  class="type">class="kw">double profit;
  class="type">class="kw">string symbol;
  class="type">long   entry;

class=class="str">"cmt">//--- Request entire trading history
  ::ResetLastError();
  if(!::HistorySelect(class="num">0, ::TimeCurrent()))
     {
      PrintFormat("%s: HistorySelect() failed. Error ",__FUNCTION__, ::GetLastError());
      class="kw">return;
     }

class=class="str">"cmt">//--- Collect data of trades
  class="type">uint deals_total=::HistoryDealsTotal();
  for(class="type">uint i=class="num">0; i<deals_total; i++)
     {
      ticket=::HistoryDealGetTicket(i);
      if(ticket==class="num">0)
         class="kw">continue;
      symbol=::HistoryDealGetString(ticket, DEAL_SYMBOL);
      entry =::HistoryDealGetInteger(ticket, DEAL_ENTRY);
      profit=::HistoryDealGetDouble(ticket, DEAL_PROFIT);
      if(entry!=DEAL_ENTRY_OUT && entry!=DEAL_ENTRY_INOUT)
         class="kw">continue;
      balance_current+=profit;
      data_count++;
      ::ArrayResize(balance, data_count);
      balance[data_count-class="num">1]=balance_current;
     }
class=class="str">"cmt">//--- data[] array to send data to frame
  class="type">class="kw">double data[];
  ::ArrayResize(data, ::ArraySize(balance)+DATA_COUNT);
  ::ArrayCopy(data, balance, DATA_COUNT, class="num">0);

class=class="str">"cmt">//--- Fill in first DATA_COUNT values of array with test results
  data[class="num">0]=::TesterStatistics(STAT_SHARPE_RATIO);          class=class="str">"cmt">// Sharpe Ratio
  data[class="num">1]=::TesterStatistics(STAT_PROFIT);                class=class="str">"cmt">// net profit
  data[class="num">2]=::TesterStatistics(STAT_PROFIT_FACTOR);         class=class="str">"cmt">// profit factor
  data[class="num">3]=::TesterStatistics(STAT_RECOVERY_FACTOR);       class=class="str">"cmt">// recovery factor

回测帧里捞夏普与回撤

在 MT5 优化器里跑 EA,想边跑边看每趟 pass 的关键指标,得靠 FrameAdd 把数据推回终端。上面这段把成交笔数、交易笔数、资金最大回撤百分比和用户自定义优化值塞进 data 数组,若 profit_factor 取到 DBL_MAX 就归零,避免后续画图炸掉。 接收端放在 OnTesterPass 里,用 FrameNext 循环捞帧。每拿到一帧就累加 frame_counter,并用 TimeLocal 算已耗时(分:秒),把夏普、净利、盈利因子、恢复因子从 data[0]~data[3] 拆出来,new 一个 CFrameData 存进链表。 实盘前先在策略测试器开「优化+帧模式」验证:若 frame_counter 增长但图表不刷数据,八成是 FrameAdd 的 FRAME_ID 和接收端对不上,或者 DrawFrameData 返回 false 被静默吞了。外汇与贵金属回测含跳空与滑点偏差,高风险,帧内回撤百分比仅作概率参考。

MQL5 / C++
  data[class="num">4]=::TesterStatistics(STAT_TRADES);                class=class="str">"cmt">// number of trades
  data[class="num">5]=::TesterStatistics(STAT_DEALS);                  class=class="str">"cmt">// number of deals
  data[class="num">6]=::TesterStatistics(STAT_EQUITY_DDREL_PERCENT);   class=class="str">"cmt">// maximum drawdown of funds as percentage
  data[class="num">7]=OnTesterValue;                                   class=class="str">"cmt">// value of user optimization criterion
  if(data[class="num">2]==DBL_MAX)
     data[class="num">2]=class="num">0;
class=class="str">"cmt">//--- Create data frame and send it to terminal
  if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME), FRAME_ID, deals_total, data))
     ::PrintFormat("%s: Frame add error: ",__FUNCTION__, ::GetLastError());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Retrieves data frame during optimization and prints chart         |
class=class="str">"cmt">//| It must be called in Expert Advisor in OnTesterPass() handler     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameViewer::OnTesterPass(class="type">void)
  {
class=class="str">"cmt">//--- Variables to work with frames
  class="type">class="kw">string name;
  class="type">ulong  pass;
  class="type">long   id;
  class="type">class="kw">double value, data[];
  class="type">class="kw">string params[];
  class="type">uint   par_count;

class=class="str">"cmt">//--- Auxiliary variables
  class="kw">static class="type">class="kw">datetime start=::TimeLocal();
  class="kw">static class="type">int      frame_counter=class="num">0;
class=class="str">"cmt">//--- When receive new frame, we receive data from it
  while(!::IsStopped() && ::FrameNext(pass, name, id, value, data))
     {
     frame_counter++;
     class="type">class="kw">string text=::StringFormat("Frames completed(tester passes): %d in %s", frame_counter,::TimeToString(::TimeLocal()-start, TIME_MINUTES|TIME_SECONDS));
     class=class="str">"cmt">//--- Get input parameters of Expert Advisor, for which frame was formed, and send them to tables and on chart
     class=class="str">"cmt">//--- Upon successful retrieval of frame write its data to frame object and locate it in list
     if(this.DrawFrameData(class="num">0, text, clrNONE, class="num">0, pass, params, par_count, data))
       {
       class=class="str">"cmt">//--- Results of tester&class="macro">#x27;s pass
       class="type">class="kw">double sharpe_ratio=data[class="num">0];
       class="type">class="kw">double net_profit=data[class="num">1];
       class="type">class="kw">double profit_factor=data[class="num">2];
       class="type">class="kw">double recovery_factor=data[class="num">3];

       class=class="str">"cmt">//--- Create new frame object and save it in list
       CFrameData *frame=new CFrameData(pass, sharpe_ratio, net_profit, profit_factor, recovery_factor);
       if(frame!=NULL)
         {
         if(!this.AddFrame(frame))

◍ 图表事件里的自适应重绘逻辑

在 MT5 自定义控件里,窗口缩放是最容易被忽略却最致命的渲染断点。上面这段 CFrameViewer::OnChartEvent 只做一件事:拦截 CHARTEVENT_CHART_CHANGE,拿到当前图表像素宽高,和缓存的 m_w / m_h 比对,不一致才触发重排。 注意它先判 w==0 或 h==0 直接 return,这是防止图表初始化瞬间尺寸为 0 导致 Resize 计算出错。实测在 4K 屏从 1280×720 拖到 1920×1080 时,若不拦截零值,Tab 控件坐标会算出负偏移。 重排顺序有讲究:先 m_tab_control.Resize(w,h) 把容器撑满,再用 GetSelectedTabID() 取当前页,调 DrawDataChart 重画数据表与优化图。切换标签时 button_switch 只在 tab_selected>0 才取,标签 0 是优化总览页,没有结果打印按钮,直接给 NULL 避免空指针。 开 MT5 把这段塞进你自己的 CFrameViewer,拖一下窗口边框,若子图没跟着铺满,八成是漏了 ChartRedraw 或 Resize 顺序反了。外汇与贵金属图表插件开发属高风险调试,参数错乱可能让 EA 面板卡死。

MQL5 / C++
class="type">void CFrameViewer::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,
                      const class="type">int delay_ms)
  {
class=class="str">"cmt">//--- Call event handlers for the tab control object and optimization result charts
   this.m_tab_control.OnChartEvent(id, lparam, dparam, sparam);
   this.m_chart_stat_0.OnChartEvent(id, lparam, dparam, sparam);
   this.m_chart_stat_1.OnChartEvent(id, lparam, dparam, sparam);
   this.m_chart_stat_2.OnChartEvent(id, lparam, dparam, sparam);
   this.m_chart_stat_3.OnChartEvent(id, lparam, dparam, sparam);
   this.m_chart_stat_4.OnChartEvent(id, lparam, dparam, sparam);

class=class="str">"cmt">//--- If chart change event has arrived
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      class=class="str">"cmt">//--- get chart size
      class="type">int w=(class="type">int)::ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS);
      class="type">int h=(class="type">int)::ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS);
      if(w!=this.m_w || h!=this.m_h)
        {
         if(w==class="num">0 || h==class="num">0)
            class="kw">return;

         class=class="str">"cmt">//--- Change size of Tab Control
         this.m_tab_control.Resize(w, h);

         class=class="str">"cmt">//--- Get ID of selected tab and draw data tables and optimization chart on tab
         class="type">int tab_selected=this.m_tab_control.GetSelectedTabID();
         this.DrawDataChart(tab_selected);

         class=class="str">"cmt">//--- Get pointer to toggle button and selected button for printing optimization results
         CButtonSwitch *button_switch=(tab_selected>class="num">0 ? this.GetChartStats(tab_selected).ButtonResult() : NULL);
         class="type">uint res_index=(button_switch!=NULL ? button_switch.SelectedButton() : -class="num">1);

         class=class="str">"cmt">//--- Depending on selected tab
         class="kw">switch(tab_selected)
           {
            class=class="str">"cmt">//--- tab class="num">0 (Optimization)
            case class="num">0 :

「优化回放与分页绘制的分支逻辑」

在优化可视化控件的事件处理里,tab 0 与 default(tab 1~4)走了两套绘制分支。tab 0 只画最近一次 pass 的折线加两张空表,default 则先取按钮选中的结果索引,再把选中分页的三次最佳 pass 画出来,同时保留 tab 0 的空表底图。 被注释掉的 ReplayFrames(1) 说明回放本可在 m_completed 为真时自动跑,但当前版本把它交给了 Replay 按钮事件触发,避免回放占用其余交互。若你接手这套代码,想看全部 pass 重播,点一下 Replay 按钮即可,按钮会先 Hide 再走 ChartOptDraw(0, m_completed, true)。 末尾的 m_w / m_h 赋值把当前宽高存住,供后续 OnChartEvent 的尺寸校验使用;若优化未结束(!m_completed)则直接 return,自定义事件(id>CHARTEVENT_CUSTOM)里的回放分支根本进不去。外汇与贵金属回测环境波动剧烈,这类界面逻辑只负责呈现,不代表任何参数组合在未来实盘有正期望。

MQL5 / C++
this.DrawDataChart(class="num">0);
class=class="str">"cmt">//if(this.m_completed)
class=class="str">"cmt">//  this.ReplayFrames(class="num">1);
break;

class="kw">default:
  res_index=button_switch.SelectedButton();
  this.DrawDataChart(tab_selected);
  this.DrawBestFrameData(tab_selected, -class="num">1);
  this.DrawBestFrameData(tab_selected, res_index);
  this.DrawDataChart(class="num">0);
class=class="str">"cmt">//if(this.m_completed)
class=class="str">"cmt">//  this.ReplayFrames(class="num">1);
  break;
}

this.m_w=w;
this.m_h=h;
}

if(!this.m_completed)
  class="kw">return;

if(id>CHARTEVENT_CUSTOM)
{
  if(sparam==this.m_chart_stat_0.ButtonReplay().Name() && this.m_completed)
  {
    this.m_chart_stat_0.ButtonReplay().Hide();
    this.ChartOptDraw(class="num">0, this.m_completed, true);
  }
}

回放锁与多标签切换的事件处理

在 MT5 自定义面板的回放逻辑里,先置 m_completed 为 false 是为了防止同一帧被连续重跑多次,回放结束再解锁为 true。这个布尔锁直接影响 ReplayFrames(delay_ms) 能否安全被反复调用,delay_ms 若设成 1 则几乎实时刷图,设成较大值则肉眼可追帧。 回放完成后必须主动把 Replay 按钮重新 Show() 并调 ChartRedraw(),否则界面会停在最后一帧不刷新。下面是这段控制流的核心代码片段: 面板用 GetTabButton(0)~(4) 一口气抓 5 个标签指针,任一为 NULL 就直接 return,说明标签结构一旦缺一个整个事件分发就废了。随后用 GetSelectedTabID() 拿当前选中页,再用 sparam 比对各 tab 的 Name() 来决定走哪个分支。 切到 tab 0 时只画空结果表并 ChartRedraw(),注释里那段 ReplayFrames(1) 是被刻意注释掉的——意味着默认不在切页时自动重放,想追优化过程得自己手动点按钮。tab 1 的切换则转交 OnTabSwitchEvent(1) 处理,把绘制与数据装载从主事件里解耦。

MQL5 / C++
this.m_completed=false;     class=class="str">"cmt">// block it so as not to run it several times in a row
this.ReplayFrames(delay_ms);  class=class="str">"cmt">// replay procedure
this.m_completed=true;        class=class="str">"cmt">// unlock  

class=class="str">"cmt">//--- After replay is complete, show Replay button and redraw chart
this.m_chart_stat_0.ButtonReplay().Show();
::ChartRedraw();
}

class=class="str">"cmt">//--- Get pointers to tab buttons
CTabButton *tab_btn0=this.m_tab_control.GetTabButton(class="num">0);
CTabButton *tab_btn1=this.m_tab_control.GetTabButton(class="num">1);
CTabButton *tab_btn2=this.m_tab_control.GetTabButton(class="num">2);
CTabButton *tab_btn3=this.m_tab_control.GetTabButton(class="num">3);
CTabButton *tab_btn4=this.m_tab_control.GetTabButton(class="num">4);
if(tab_btn0==NULL || tab_btn1==NULL || tab_btn2==NULL || tab_btn3==NULL || tab_btn4==NULL)
   class="kw">return;

class=class="str">"cmt">//--- Get ID of selected tab
class="type">int tab_selected=this.m_tab_control.GetSelectedTabID();

class=class="str">"cmt">//--- If class="kw">switch event has arrived to tab class="num">0
if(sparam==tab_btn0.Name())
  {
   class=class="str">"cmt">//--- On tab class="num">0, draw chart with last pass line and two tables with empty results
   this.DrawDataChart(class="num">0);
   class=class="str">"cmt">//--- It starts replay of performed optimization
   class=class="str">"cmt">//--- (it can take a class="type">long time - if desired, you can click Replay button to print charts)
   class=class="str">"cmt">//if(this.m_completed)
   class=class="str">"cmt">//   this.ReplayFrames(class="num">1);
   ::ChartRedraw();
   class="kw">return;
   }

class=class="str">"cmt">//--- Get pointer to chart of selected tab
CStatChart *chart_stat=this.GetChartStats(tab_selected);
if(tab_selected==class="num">0 || chart_stat==NULL)
   class="kw">return;
class=class="str">"cmt">//--- Get pointers to chart buttons of selected tab(tab index class="num">1-class="num">4)
CButtonTriggered *button_min=chart_stat.ButtonResultMin();
CButtonTriggered *button_mid=chart_stat.ButtonResultMid();
CButtonTriggered *button_max=chart_stat.ButtonResultMax();
if(button_min==NULL || button_mid==NULL || button_max==NULL)
   class="kw">return;
class=class="str">"cmt">//--- If class="kw">switch event has arrived to tab class="num">1
if(sparam==tab_btn1.Name())
  {
   class=class="str">"cmt">//--- call handler for switching to tab
   this.OnTabSwitchEvent(class="num">1);
   }
class=class="str">"cmt">//--- If class="kw">switch event has arrived to tab class="num">2
if(sparam==tab_btn2.Name())
  {

◍ 标签页与结果按钮的事件分发逻辑

在 MT5 自定义面板里,标签切换和结果档位按钮都走同一个事件入口,靠 sparam 比对控件 Name() 来路由。下面这段分发代码把 4 个标签页和每页的 min/mid/max 三个结果按钮分别映射到 OnTabSwitchEvent 与 OnButtonSwitchEvent,参数就是页号和档位索引(0/1/2)。

MQL5 / C++
片段:标签页与按钮事件分发
class=class="str">"cmt">//--- 若事件来自标签3
if(sparam==tab_btn3.Name())
  this.OnTabSwitchEvent(class="num">3);
class=class="str">"cmt">//--- 若事件来自最小结果按钮
if(sparam==button_min.Name())
  this.OnButtonSwitchEvent(tab_selected, class="num">0);
OnTabSwitchEvent 拿到 tab_id 后,先取该页统计图表指针与切换按钮指针,空指针直接 return 防崩。随后读 SelectedButton() 得到当前档位索引,再顺序调 DrawDataChart 画结果图、ControlObjectsView 控显隐、DrawBestFrameData 画三条最优通路,最后用 butt_index 高亮选中档位。 实盘外接这类面板时,若发现切页后图表不刷新,优先查 tab_selected 变量是否在按钮事件里被正确赋值——很多自制 EA 面板漏了这步,导致 OnButtonSwitchEvent 永远拿到默认页。

MQL5 / C++
   class=class="str">"cmt">//--- If class="kw">switch event has arrived to tab class="num">3
   if(sparam==tab_btn3.Name())
     {
      class=class="str">"cmt">//--- call handler for switching to tab
      this.OnTabSwitchEvent(class="num">3);
     }
   class=class="str">"cmt">//--- If class="kw">switch event has arrived to tab class="num">4
   if(sparam==tab_btn4.Name())
     {
      class=class="str">"cmt">//--- call handler for switching to tab
      this.OnTabSwitchEvent(class="num">4);
     }
   class=class="str">"cmt">//--- If event has arrived click on minimum result button of selected tab
   if(sparam==button_min.Name())
     {
      class=class="str">"cmt">//--- call handler for toggle button switching 
      this.OnButtonSwitchEvent(tab_selected, class="num">0);
     }
   class=class="str">"cmt">//--- If event has arrived click on mid result button of selected tab
   if(sparam==button_mid.Name())
     {
      class=class="str">"cmt">//--- call handler for toggle button switching 
      this.OnButtonSwitchEvent(tab_selected, class="num">1);
     }
   class=class="str">"cmt">//--- If event has arrived click on max result button of selected tab
   if(sparam==button_max.Name())
     {
      class=class="str">"cmt">//--- call handler for toggle button switching 
      this.OnButtonSwitchEvent(tab_selected, class="num">2);
     }
class="type">void CFrameViewer::OnTabSwitchEvent(const class="type">int tab_id)
  {
   class=class="str">"cmt">//--- Get pointer to chart of selected tab
   CStatChart *chart_stat=this.GetChartStats(tab_id);
   if(chart_stat==NULL)
      class="kw">return;
   class=class="str">"cmt">//--- Get pointer to chart toggle button of selected tab
   CButtonSwitch *button_switch=chart_stat.ButtonResult();
   if(button_switch==NULL)
      class="kw">return;
   class=class="str">"cmt">//--- Index of pressed button
   class="type">uint butt_index=button_switch.SelectedButton();
   class=class="str">"cmt">//--- Initialize chart of results on tab_id and
   this.DrawDataChart(tab_id);
   class=class="str">"cmt">//--- call method that controls display of control elements on all tabs
   this.ControlObjectsView(tab_id);
   class=class="str">"cmt">//--- Draw all three best passes
   this.DrawBestFrameData(tab_id, -class="num">1);
   class=class="str">"cmt">//--- Highlight pass selected by button
   this.DrawBestFrameData(tab_id, butt_index);
  }

「切换标签时重绘优化结果视图」

在 MT5 优化结果查看器的内部实现里,用户点开某个结果标签(tab_id)会触发 OnButtonSwitchEvent,此时先重绘该标签的数据图表,再把全部三次最优遍历画出来,最后用 butt_id 指定的按钮高亮其中一条遍历。这种「先全画、再单高亮」的顺序能避免图表层叠错位。 DrawDataChart 负责把统计表、参数表和优化曲线一起铺到界面上:统计表从左上角 (4,4) 起、单元格宽 CELL_W*2、高 CELL_H;参数表紧接统计表下边界加 4 像素偏移;优化图则根据 m_completed 标记决定是否带完成态渲染。 ControlObjectsView 只处理当前激活标签(GetSelectedTabID 拿到索引),用 table_stat.X2()+10 和 tab 右边界减 10 算出图头区间,再按 tab_index 分支摆控件。以 case 0 优化标签为例,Replay 按钮被移到图头水平中线 (x1+x2)/2 处,且仅当 m_completed 为真才 Show 并 BringToTop,其余标签的按钮直接隐藏。外汇与贵金属 EA 回测优化界面改动后,建议直接在 MT5 附带的 FrameViewer 类里断点验证这套坐标逻辑。

MQL5 / C++
class="type">void CFrameViewer::OnButtonSwitchEvent(const class="type">int tab_id, const class="type">uint butt_id)
  {
  class=class="str">"cmt">//--- Initialize chart of results on tab_id
  this.DrawDataChart(tab_id);
  class=class="str">"cmt">//--- Draw all three best passes
  this.DrawBestFrameData(tab_id, -class="num">1);
  class=class="str">"cmt">//--- Highlight pass selected by butt_id button
  this.DrawBestFrameData(tab_id, butt_id);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Draws data tables and optimization chart                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameViewer::DrawDataChart(const class="type">uint tab_id)
  {
class=class="str">"cmt">//--- Draw table of statistics, table of input parameters, and optimization chart
  this.TableStatDraw(tab_id, class="num">4, class="num">4, CELL_W*class="num">2, CELL_H, false);
  this.TableInpDraw(tab_id, class="num">4, this.GetTableStats(tab_id).Y2()+class="num">4, CELL_W*class="num">2, CELL_H, this.GetTableInputs(tab_id).RowsTotal(), false);
  this.ChartOptDraw(tab_id, this.m_completed, true);
  class=class="str">"cmt">//--- call method that controls display of control elements on all tabs
  this.ControlObjectsView(tab_id);
  }
class=class="str">"cmt">//+-------------------------------------------------------------------+
class=class="str">"cmt">//|Controls view of control objects on optimization charts            |
class=class="str">"cmt">//+-------------------------------------------------------------------+
class="type">void CFrameViewer::ControlObjectsView(const class="type">uint tab_id)
  {
class=class="str">"cmt">//--- Get index of active tab
  class="type">int tab_index=this.m_tab_control.GetSelectedTabID();

class=class="str">"cmt">//--- Get pointer to active tab and optimization statistics table
  CTab              *tab=this.m_tab_control.GetTab(tab_index);
  CTableDataControl *table_stat=this.GetTableStats(tab_index);
  if(tab==NULL || table_stat==NULL)
     class="kw">return;

class=class="str">"cmt">//--- Coordinates of left and right boundaries of header of optimization results chart
  class="type">int w=class="num">0, cpx=class="num">0, x=class="num">0, y=class="num">0;
  class="type">int x1=table_stat.X2()+class="num">10;
  class="type">int x2=tab.GetField().Right()-class="num">10;

class=class="str">"cmt">//--- Depending on selected tab index
  class="kw">switch(tab_index)
     {
     class=class="str">"cmt">//--- Optimization
     case class="num">0 :
        class=class="str">"cmt">//--- Shift Replay button to center of header
        w=this.m_chart_stat_0.ButtonReplay().Width();
        cpx=(x1+x2)/class="num">2;
        x=cpx-w/class="num">2;
        this.m_chart_stat_0.ButtonReplay().MoveX(x);

        class=class="str">"cmt">//--- If optimization is completed, show button in foreground
        if(this.m_completed)
           {
           this.m_chart_stat_0.ButtonReplay().Show();
           this.m_chart_stat_0.ButtonReplay().BringToTop();
           }

        class=class="str">"cmt">//--- Hide buttons of all other tabs
        this.m_chart_stat_1.ButtonsResultHide();

切换统计标签时的按钮重排逻辑

在 MT5 面板里切到不同绩效标签(夏普、净利、盈利因子、恢复因子)时,代码用 switch-case 把非当前标签的结果按钮全藏掉,只把对应标签的切换键提到最前。 以 case 1 为例:先隐藏重放按钮,再按进度条下沿加 CELL_H+2 像素算出新 Y 坐标,把标签1的结果键移过去。CELL_H 通常是单元格行高,若你改了面板字号,这个值不跟着调就会错位。 每个 case 都重复「BringToTop 自己、Hide 其他三个」的动作,说明对象数组 m_chart_stat_1~4 与标签索引一一对应。外汇与贵金属图表加载这类面板属高风险操作环境,参数误改可能导致界面重绘异常,建议在策略测试器里先验证。 直接看这段就能复现定位逻辑:

MQL5 / C++
this.m_chart_stat_2.ButtonsResultHide();
this.m_chart_stat_3.ButtonsResultHide();
this.m_chart_stat_4.ButtonsResultHide();
break;

class=class="str">"cmt">//--- Sharpe Ratio
case class="num">1 :
  class=class="str">"cmt">//--- Hide Replay button
  this.m_chart_stat_0.ButtonReplay().Hide();
  
  class=class="str">"cmt">//--- Get Y coordinate and move toggle button to it
  y=this.m_chart_stat_1.ProgressBarY1()+CELL_H+class="num">2;
  this.m_chart_stat_1.ButtonResult().MoveY(y);
  
  class=class="str">"cmt">//--- Move toggle button on tab class="num">1 to foreground,
  class=class="str">"cmt">//--- and hide all other buttons on other tabs
  this.m_chart_stat_1.ButtonsResultBringToTop();
  this.m_chart_stat_2.ButtonsResultHide();
  this.m_chart_stat_3.ButtonsResultHide();
  this.m_chart_stat_4.ButtonsResultHide();
  break;

class=class="str">"cmt">//--- Net Profit
case class="num">2 :
  this.m_chart_stat_0.ButtonReplay().Hide();
  
  class=class="str">"cmt">//--- Get Y coordinate and move toggle button to it
  y=this.m_chart_stat_2.ProgressBarY1()+CELL_H+class="num">2;
  this.m_chart_stat_2.ButtonResult().MoveY(y);
  
  class=class="str">"cmt">//--- Move toggle button on tab class="num">2 to foreground, 
  class=class="str">"cmt">//--- and hide all other buttons on other tabs
  this.m_chart_stat_2.ButtonsResultBringToTop();
  this.m_chart_stat_1.ButtonsResultHide();
  this.m_chart_stat_3.ButtonsResultHide();
  this.m_chart_stat_4.ButtonsResultHide();
  break;

class=class="str">"cmt">//--- Profit Factor
case class="num">3 :
  this.m_chart_stat_0.ButtonReplay().Hide();
  
  class=class="str">"cmt">//--- Get Y coordinate and move toggle button to it
  y=this.m_chart_stat_3.ProgressBarY1()+CELL_H+class="num">2;
  this.m_chart_stat_3.ButtonResult().MoveY(y);
  
  class=class="str">"cmt">//--- Move toggle button on tab class="num">3 to foreground, 
  class=class="str">"cmt">//--- and hide all other buttons on other tabs
  this.m_chart_stat_3.ButtonsResultBringToTop();
  this.m_chart_stat_1.ButtonsResultHide();
  this.m_chart_stat_2.ButtonsResultHide();
  this.m_chart_stat_4.ButtonsResultHide();
  break;

class=class="str">"cmt">//--- Recovery Factor
case class="num">4 :
  this.m_chart_stat_0.ButtonReplay().Hide();

◍ 回放开局先把按钮层级理清

在标签切换的 break 分支里,先把第 4 页结果按钮的 Y 坐标算出来:取进度条底部坐标 m_chart_stat_4.ProgressBarY1(),再叠一个格子高度 CELL_H 加 2 像素偏移,然后 MoveY 落位。这样按钮不会和第 4 页的进度条控件重叠。 随后调用 ButtonsResultBringToTop() 把第 4 页按钮提到最前,并对前三个标签页的按钮统一执行 ButtonsResultHide(),避免多 tab 按钮在图表上互相遮挡。外汇与贵金属图表多控件叠加时易误触,这类显隐管理能降低手动操作风险。 优化跑完后的回放由 ReplayFrames(delay_ms) 负责:先 FrameFirst() 把帧指针归零,用 FrameNext 逐帧取 pass/name/id/value/data,每帧 frame_counter 自增并打印“Playing with pause %d ms: frame %d”。delay_ms 直接喂给 Sleep(),所以设 200 时每帧至少停 200 毫秒,可在 MT5 策略测试器里改这个值控制回放节奏。 DrawFrameDataByPass() 则按指定 pass_num 搜帧:同样 FrameFirst() 起手,循环 FrameNext 直到命中目标 pass,再把该帧数据往图表上画。想验证某次优化_pass 的可视化结果,直接传 pass_num 调它即可。

MQL5 / C++
  class=class="str">"cmt">//--- Get Y coordinate and move toggle button to it
  y=this.m_chart_stat_4.ProgressBarY1()+CELL_H+class="num">2;
  this.m_chart_stat_4.ButtonResult().MoveY(y);
  
  class=class="str">"cmt">//--- Move toggle button on tab class="num">4 to foreground, 
  class=class="str">"cmt">//--- and hide all other buttons on other tabs
  this.m_chart_stat_4.ButtonsResultBringToTop();
  this.m_chart_stat_1.ButtonsResultHide();
  this.m_chart_stat_2.ButtonsResultHide();
  this.m_chart_stat_3.ButtonsResultHide();
  break;
  class="kw">default:
    break;
  }
class=class="str">"cmt">//--- Redraw chart
  ::ChartRedraw();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Replaying frames after optimization completion                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameViewer::ReplayFrames(const class="type">int delay_ms)
  {
class=class="str">"cmt">//--- Variables to work with frames
  class="type">class="kw">string name;
  class="type">ulong  pass;
  class="type">long   id;
  class="type">class="kw">double value, data[];
  class="type">class="kw">string params[];
  class="type">uint   par_count;
  
class=class="str">"cmt">//--- Frame counter
  class="type">int frame_counter=class="num">0;
  
class=class="str">"cmt">//--- Reset progress bar counters
  this.m_progress_bar.Reset();
  this.m_progress_bar.Update(false);
  
class=class="str">"cmt">//--- Move frame pointer to the beginning and start iterating through frames
  ::FrameFirst();
  while(!::IsStopped() && ::FrameNext(pass, name, id, value, data))
    {
    class=class="str">"cmt">//--- Increase frame counter and prepare header text of optimization chart
    frame_counter++;
    class="type">class="kw">string text=::StringFormat("Playing with pause %d ms: frame %d", delay_ms, frame_counter);
    class=class="str">"cmt">//--- Get input parameters of Expert Advisor, for which frame is formed, frame data, and print them on chart
    if(this.DrawFrameData(class="num">0, text, clrNONE, class="num">0, pass, params, par_count, data))
      ::ChartRedraw();
    
    class=class="str">"cmt">//--- Wait for delay_ms milliseconds
    ::Sleep(delay_ms);
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Prints data of specified frame on optimization chart               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CFrameViewer::DrawFrameDataByPass(const class="type">uint tab_id, const class="type">ulong pass_num, const class="type">class="kw">string text, class="type">color clr, const class="type">uint line_width, class="type">class="kw">double &data[])
  {
class=class="str">"cmt">//--- Variables to work with frames
  class="type">class="kw">string name;
  class="type">ulong  pass;
  class="type">long   id;
  class="type">uint   par_count;
  class="type">class="kw">double value;
  class="type">class="kw">string params[];
  
class=class="str">"cmt">//--- Move frame pointer to the beginning and start search of pass_num frame
  ::FrameFirst();
  while(::FrameNext(pass, name, id, value, data))

「把优化前三名画到图表上」

CFrameViewer 的 DrawBestFrameData 方法负责把某一项优化标准下的前三名回测结果直接绘制到 MT5 图表。调用时传入 tab_id(1~4)和 res_index(-1~2),tab_id 超出 1~4 或 res_index 大于 2 会直接 PrintFormat 报错并 return,不会动图表。 tab_id 与标题的对应写死在三元表达式里:1=Sharpe Ratio、2=Net Profit、3=Profit Factor、4=Recovery Factor。也就是说,你点哪个分页标签,就决定图表头部显示哪种标准下的「Optimization Completed」字样。 FillArrayBestFrames 先 ZeroMemory 清空传入的 ulong array_passes[3],再填回三个最优 pass 的索引。若 res_index 为负数,循环把三趟 pass 全用 clrNONE 画出来(盈利/亏损序列自动配色);若 res_index 是 0/1/2,则只画对应那一条,颜色取 m_selected_color、线宽取 m_line_width。 实盘前先在策略测试器跑完优化,再切到对应 tab 点按钮验证:若三线重叠难以分辨,把 m_line_width 从 0 调成 1~2 能立刻看清回撤段差异。外汇与贵金属品种波动剧烈,优化结果仅代表历史样本,实盘复制可能显著偏离。

MQL5 / C++
  {
      class=class="str">"cmt">//--- If passe number matches the desired one -
      class=class="str">"cmt">//--- get frame data and print it in table
      class=class="str">"cmt">//--- and on chart on tab tab_id
      if(pass==pass_num)
        {
         if(DrawFrameData(tab_id, text, clr, line_width, pass, params, par_count, data))
            class="kw">return true;
        }
   }
class=class="str">"cmt">//--- Pass not found
   class="kw">return false;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Draws charts of top three passes by optimization criterion       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameViewer::DrawBestFrameData(const class="type">uint tab_id, const class="type">int res_index)
  {
class=class="str">"cmt">//--- If incorrect identifiers of table and pressed button are passed, exit
   if(tab_id<class="num">1 || tab_id>class="num">4 || res_index>class="num">2)
     {
      ::PrintFormat("%s: Error. Incorrect table(%u) or selected button(%d) identifiers passed",__FUNCTION__, tab_id, res_index);
      class="kw">return;
     }
     
class=class="str">"cmt">//--- Arrays for getting results of passes
   class="type">ulong array_passes[class="num">3];
   class="type">class="kw">double data[];

class=class="str">"cmt">//--- Create header text of pass chart
   class="type">class="kw">string res=
     (
      tab_id==class="num">1 ? "Results by Sharpe Ratio"      :
      tab_id==class="num">2 ? "Results by Net Profit"        :
      tab_id==class="num">3 ? "Results by Profit Factor"     :
      tab_id==class="num">4 ? "Results by Recovery Factor"   :
      ""
     );
   class="type">class="kw">string text="Optimization Completed: "+res;

class=class="str">"cmt">//--- Fill in array_passes array by indexes of three best passes
   this.FillArrayBestFrames(tab_id, array_passes);
class=class="str">"cmt">//--- If index of pass button is set to negative number -
   if(res_index<class="num">0)
     {
      class=class="str">"cmt">//--- print all three passes on chart
      class=class="str">"cmt">//--- (line class="type">color is specified as clrNONE for automatic line class="type">color selection of profitable or loss series)
      for(class="type">int i=class="num">0; i<(class="type">int)array_passes.Size(); i++)
         this.DrawFrameDataByPass(tab_id, array_passes[i], text, clrNONE, class="num">0, data);
     }
   class=class="str">"cmt">//--- Otherwise, print series indicated by index of pressed button on chart(res_index),
   class=class="str">"cmt">//--- by class="type">color set in m_selected_color, and width specified in m_line_width
   else
      this.DrawFrameDataByPass(tab_id, array_passes[res_index], text, this.m_selected_color, this.m_line_width, data);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fills array with frame indexes of top three passes                |
class=class="str">"cmt">//| for specified optimization criterion(by tab index)               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CFrameViewer::FillArrayBestFrames(const class="type">uint tab_id, class="type">ulong &array_passes[])
  {
class=class="str">"cmt">//--- Clear array of optimization pass indexes passed to method
   ::ZeroMemory(array_passes);

按优化指标挑出前三轮回测

在 MT5 优化框架里,想从一堆回测结果中自动捞出表现最好的几轮,核心是先按某个指标排序再取尾部。下面这段逻辑支持用 tab_id 在 1~4 之间指定指标类型:1 对应夏普比率、2 对应净利润、3 对应盈利因子、4 对应恢复因子,超出范围直接报错退出。 代码先校验 tab_id 合法性,再把它强转成 ENUM_FRAME_PROP 枚举,调用 Sort 按该属性升序排列帧列表。排完序后,最大值的那一轮会落在列表末尾,用 Total()-1 取下标就能拿到最优帧,把它的 Pass 号写进 array_passes[2]。 剩下两轮靠 FrameSearchLess 从最优帧往前找「属性值更小」的相邻帧,倒序填进 array_passes[1] 和 [0];若搜不到更小值的帧,就复用后一位的 Pass 号。这样 array_passes 三个槽位就存了前三好的优化批次,方便后续做参数对比。 外汇与贵金属品种做这类优化回测波动剧烈,历史最优不代表未来实盘概率占优,切换品种前建议先在小周期样本验证排序稳定性。

MQL5 / C++
class=class="str">"cmt">//FRAME_PROP_PASS_NUM,                // Pass number
class=class="str">"cmt">//FRAME_PROP_SHARPE_RATIO,         // Sharpe Ratio result
class=class="str">"cmt">//FRAME_PROP_NET_PROFIT,           // Net Profit result
class=class="str">"cmt">//FRAME_PROP_PROFIT_FACTOR,        // Profit Factor result
class=class="str">"cmt">//FRAME_PROP_RECOVERY_FACTOR,      // Recovery Factor result

class=class="str">"cmt">//--- By using tab ID, determine class="kw">property by which best optimization passes will be searched
class=class="str">"cmt">//--- Check tab ID to be within class="num">1 to class="num">4
   if(tab_id<FRAME_PROP_SHARPE_RATIO || tab_id>FRAME_PROP_RECOVERY_FACTOR)
      {
         ::PrintFormat("%s: Error: Invalid tab ID passed(%u)",__FUNCTION__, tab_id);
         class="kw">return false;
      }

class=class="str">"cmt">//--- Convert table ID to frame class="kw">property
   ENUM_FRAME_PROP prop=(ENUM_FRAME_PROP)tab_id;

class=class="str">"cmt">//--- Sort frame list in ascending order by class="kw">property,
class=class="str">"cmt">//--- which corresponds to tab_id value as ENUM_FRAME_PROP
   this.m_list_frames.Sort(prop);
class=class="str">"cmt">//--- After sorting, frame with best result will be at list end
class=class="str">"cmt">//--- Using index, get frame from list with maximum result value and
   class="type">int index=this.m_list_frames.Total()-class="num">1;
   CFrameData *frame_next=this.m_list_frames.At(index);
   if(frame_next==NULL)
      class="kw">return false;
      
class=class="str">"cmt">//--- register pass number to last cell of array_passes
   array_passes[class="num">2]=frame_next.Pass();

class=class="str">"cmt">//--- Now find objects for which optimization result in descending order is less than maximum found
class=class="str">"cmt">//--- In loop from class="num">1 to class="num">0 (remaining cells of array_passes)
   for(class="type">int i=class="num">1; i>=class="num">0; i--)
      {
         class=class="str">"cmt">//--- look for previous object with class="kw">property value less than that of frame_next object
         frame_next=this.FrameSearchLess(frame_next, prop);
         class=class="str">"cmt">//--- In next cell of array_passes, enter pass number of found object
         class=class="str">"cmt">//--- If object is not found, it means that there are no objects with value less than that of frame_next object,
         class=class="str">"cmt">//--- and in this case, enter its previous value to next cell of array_passes
         array_passes[i]=(frame_next!=NULL ? frame_next.Pass() : array_passes[i+class="num">1]);
      }
class=class="str">"cmt">//--- Success
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Searches for and returns pointer to frame object,              |
class=class="str">"cmt">//| with class="kw">property value less than sample                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
CFrameData *CFrameViewer::FrameSearchLess(CFrameData *frame, const class="type">int mode)
  {
class=class="str">"cmt">//--- Depending on type of frame class="kw">property
   class="kw">switch(mode)
      {
         class=class="str">"cmt">//--- to temporary object record corresponding class="kw">property of object passed to method
         case FRAME_PROP_SHARPE_RATIO      :   this.m_frame_tmp.SetSharpeRatio(frame.SharpeRatio());      break;
         case FRAME_PROP_NET_PROFIT        :   this.m_frame_tmp.SetNetProfit(frame.NetProfit());          break;
         case FRAME_PROP_PROFIT_FACTOR     :   this.m_frame_tmp.SetProfitFactor(frame.ProfitFactor());    break;

◍ 回测结果里捞最优帧的底层取法

这段逻辑干的事很直接:先把帧数组按指定属性排序,再用 SearchLess 找属性值刚好低于临时帧的那一帧,拿不到就返回 NULL。RecoveryFactor 这类属性在 switch 里被单独赋值,其余情况走 default 把 Pass 存进临时帧,说明排序键是可切换的。 DrawBestFrameDataAll 从标签 1 开始循环到 TabsTotal()-1,对每个标签调 DrawBestFrameData(i,-1),也就是每个优化结果页都画前三名 pass。想验证的话,开 MT5 跑完一轮 EA 优化,看右侧多标签图是否每页都带三条最优曲线。 DrawFrameData 一进来就卡 tab_id>4 直接 false 并 PrintFormat 报错,意味着自定义面板只支持 0~4 号标签。它内部取前景画布、两张数据表和一个统计图的指针,任一为 NULL 就退出,随后用 FrameInputs(pass,...) 拉该 pass 的 EA 输入参数,再画参数表——表起点 y 坐标是 table_stat.Y2()+4,单元格宽高走 CELL_W*2 与 CELL_H 常量。外汇与贵金属优化结果受点差滑点影响大,回测靠前帧实盘可能失效,属高风险验证。

MQL5 / C++
case FRAME_PROP_RECOVERY_FACTOR  :  this.m_frame_tmp.SetRecoveryFactor(frame.RecoveryFactor()); break;
    class="kw">default                            :  this.m_frame_tmp.SetPass(frame.Pass());                     break;
     }
class=class="str">"cmt">//--- Sort array of frames by specified class="kw">property and
   this.m_list_frames.Sort(mode);
class=class="str">"cmt">//--- get index of nearest object with lower class="kw">property value, or -class="num">1
   class="type">int index=this.m_list_frames.SearchLess(&this.m_frame_tmp);
class=class="str">"cmt">//--- Get object by index from list and class="kw">return pointer to it, or NULL
   CFrameData *obj=this.m_list_frames.At(index);
   class="kw">return obj;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Prints on optimization results charts                             |
class=class="str">"cmt">//| on each tab three best passes                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameViewer::DrawBestFrameDataAll(class="type">void)
  {
class=class="str">"cmt">//--- In a loop through all tabs from tab class="num">1, draw charts of top three passes for each tab
   for(class="type">int i=class="num">1; i<this.m_tab_control.TabsTotal(); i++)
      this.DrawBestFrameData(i,-class="num">1);
  }
class=class="str">"cmt">//+-------------------------------------------------------------------+
class=class="str">"cmt">//| Retrieving data of current frame and printing it on specified tab |
class=class="str">"cmt">//| in table and on optimization results chart                        |
class=class="str">"cmt">//+-------------------------------------------------------------------+
class="type">bool CFrameViewer::DrawFrameData(const class="type">uint tab_id, const class="type">class="kw">string text, class="type">color clr, const class="type">uint line_width, class="type">ulong &pass, class="type">class="kw">string &params[], class="type">uint &par_count, class="type">class="kw">double &data[])
  {
class=class="str">"cmt">//--- Check passed tab ID
   if(tab_id>class="num">4)
     {
     ::PrintFormat("%s: Error: Invalid tab ID passed(%u)",__FUNCTION__, tab_id);
     class="kw">return false;
     }
class=class="str">"cmt">//--- Get pointers to objects used on specified tab
   CCanvas         *foreground=this.m_tab_control.GetTabForeground(tab_id);
   CTableDataControl *table_stat=this.GetTableStats(tab_id);
   CTableDataControl *table_inp=this.GetTableInputs(tab_id);
   CStatChart      *chart_stat=this.GetChartStats(tab_id);
   if(foreground==NULL || table_stat==NULL || table_inp==NULL || chart_stat==NULL)
      class="kw">return false;
     
class=class="str">"cmt">//--- Get input parameters of Expert Advisor, for which frame is formed, frame data, and print them on chart
   ::ResetLastError();
   if(::FrameInputs(pass, params, par_count))
     {
     class=class="str">"cmt">//--- Draw table of input parameters on chart
     this.TableInpDraw(tab_id, class="num">4, table_stat.Y2()+class="num">4, CELL_W*class="num">2, CELL_H, par_count, false);
     
     class=class="str">"cmt">//--- Iterate through parameters, params[i], class="type">class="kw">string looks as "parameter=value"

「把输入参数刷进优化结果表」

在 MT5 自绘优化面板的逻辑里,输入参数表需要逐行填充名称和数值,同时用底色区分可优化与不可优化项。下面这段循环就是干这个的:先按等号切分参数字符串,再查该参数是否开放了优化区间。

MQL5 / C++
for(class="type">uint i=class="num">0; i<par_count; i++)
  {
   class=class="str">"cmt">//--- Fill in table with names and values of input parameters
   class="type">class="kw">string array[];
   class=class="str">"cmt">//--- Split class="type">class="kw">string in params[i] into two substrings and update cells in class="type">class="kw">string of test parameters table
   if(::StringSplit(params[i],&class="macro">#x27;=&class="macro">#x27;,array)==class="num">2)
     {
      class=class="str">"cmt">//--- Fill in strings of optimized parameters with pale yellow class="type">color,
      class=class="str">"cmt">//--- parameters that are not available for optimization - to pale pink, the rest - to class="kw">default colors
      class="type">bool enable=false;
      class="type">class="kw">double value=class="num">0, start=class="num">0, step=class="num">0, stop=class="num">0;
      class="type">color clr=clrMistyRose;
      if(::ParameterGetRange(array[class="num">0], enable, value, start, step, stop))
         clr=(enable ? clrLightYellow : clrNONE);
      
      class=class="str">"cmt">//--- Get two cells of table by parameter index and print text of parameter name and its value to them
      CTableCell *cell_0=table_inp.GetCell(i, class="num">0);
      CTableCell *cell_1=table_inp.GetCell(i, class="num">1);
      if(cell_0!=NULL && cell_1!=NULL)
        {
         class=class="str">"cmt">//--- Update captions in cells
         cell_0.SetText(array[class="num">0]);
         cell_1.SetText(array[class="num">1]);
         cell_0.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER);
         cell_1.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER);
        }
     }
  }

class=class="str">"cmt">//--- Update optimization statistics table
class=class="str">"cmt">//--- Table header class="type">class="kw">string
foreground.FillRectangle(table_stat.X1()+class="num">1, class="num">4+class="num">1, table_stat.X1()+CELL_W*class="num">2-class="num">1, class="num">4+CELL_H-class="num">1, 0x00FFFFFF);
foreground.FontSet("Calibri", -class="num">100, FW_BLACK);
foreground.TextOut(class="num">4+(CELL_W*class="num">2)/class="num">2, class="num">4+CELL_H/class="num">2, ::StringFormat("Optimization results(pass %I64u)", pass), ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);

class=class="str">"cmt">//--- In loop by number of strings in table
class="type">int total=table_stat.RowsTotal();
for(class="type">int i=class="num">0; i<total; i++)
  {
逐行拆解:循环变量 i 从 0 跑到 par_count-1,par_count 即输入参数总个数;StringSplit 以 '=' 切割,返回 2 才说明「名=值」格式合法。ParameterGetRange 拿到 enable 标记,若 true 则该参数参与优化,单元格染浅黄(clrLightYellow),否则透明(clrNONE),未走此分支的默认浅粉(clrMistyRose)。 表头绘制用 FillRectangle 涂白底,FontSet 设 Calibri 粗体,TextOut 以 pass 变量动态写「Optimization results (pass N)」,其中 %I64u 对应 64 位无符号遍数。外汇与贵金属 EA 优化涉及高杠杆与滑点风险,回测通过不代表实盘概率同向。 开 MT5 把这段塞进你的 CAppDialog 派生类,改 par_count 来源为自己 EA 的 InputsTotal(),就能在自定义面板里实时看每遍参数着色。

MQL5 / C++
for(class="type">uint i=class="num">0; i<par_count; i++)
  {
   class=class="str">"cmt">//--- Fill in table with names and values of input parameters
   class="type">class="kw">string array[];
   class=class="str">"cmt">//--- Split class="type">class="kw">string in params[i] into two substrings and update cells in class="type">class="kw">string of test parameters table
   if(::StringSplit(params[i],&class="macro">#x27;=&class="macro">#x27;,array)==class="num">2)
     {
      class=class="str">"cmt">//--- Fill in strings of optimized parameters with pale yellow class="type">color,
      class=class="str">"cmt">//--- parameters that are not available for optimization - to pale pink, the rest - to class="kw">default colors
      class="type">bool enable=false;
      class="type">class="kw">double value=class="num">0, start=class="num">0, step=class="num">0, stop=class="num">0;
      class="type">color clr=clrMistyRose;
      if(::ParameterGetRange(array[class="num">0], enable, value, start, step, stop))
         clr=(enable ? clrLightYellow : clrNONE);
      
      class=class="str">"cmt">//--- Get two cells of table by parameter index and print text of parameter name and its value to them
      CTableCell *cell_0=table_inp.GetCell(i, class="num">0);
      CTableCell *cell_1=table_inp.GetCell(i, class="num">1);
      if(cell_0!=NULL && cell_1!=NULL)
        {
         class=class="str">"cmt">//--- Update captions in cells
         cell_0.SetText(array[class="num">0]);
         cell_1.SetText(array[class="num">1]);
         cell_0.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER);
         cell_1.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER);
        }
     }
  }

class=class="str">"cmt">//--- Update optimization statistics table
class=class="str">"cmt">//--- Table header class="type">class="kw">string
foreground.FillRectangle(table_stat.X1()+class="num">1, class="num">4+class="num">1, table_stat.X1()+CELL_W*class="num">2-class="num">1, class="num">4+CELL_H-class="num">1, 0x00FFFFFF);
foreground.FontSet("Calibri", -class="num">100, FW_BLACK);
foreground.TextOut(class="num">4+(CELL_W*class="num">2)/class="num">2, class="num">4+CELL_H/class="num">2, ::StringFormat("Optimization results(pass %I64u)", pass), ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);

class=class="str">"cmt">//--- In loop by number of strings in table
class="type">int total=table_stat.RowsTotal();
for(class="type">int i=class="num">0; i<total; i++)
  {

回测面板里逐行填回测指标

在 MT5 的自定义统计面板里,每一行对应一个回测核心指标,需要把后台算好的 data[] 数组按行写进表格第二列。下面这段逻辑就是遍历行号、用 StringFormat 按精度格式化后塞进 cell_1,同时顺手做高亮。 Sharpe Ratio、Net Profit、Profit Factor、Recovery Factor 都保留两位小数,Trades 和 Deals 直接取整,Equity DD 带百分号,OnTester() 返回值用 %G 科学计数。若某一行指针为空就跳过,避免 EA 在初始化阶段崩。 高亮规则很直白:只有当 tab_id>0 且当前行号等于 tab_id-1 时才染成浅绿 C'223,242,231',其余保持 clrNONE。这样你在多品种切换时,能一眼看出当前标签对应哪行数据。外汇与贵金属回测受点差跳空影响大,这类面板数值仅反映历史样本,实盘表现可能有明显偏差。

MQL5 / C++
      class=class="str">"cmt">//--- get two cells of current class="type">class="kw">string and
      CTableCell *cell_0=table_stat.GetCell(i, class="num">0);
      CTableCell *cell_1=table_stat.GetCell(i, class="num">1);
      if(cell_0!=NULL && cell_1!=NULL)
        {
         class=class="str">"cmt">//--- update values of pass results in second cell
         class="type">class="kw">string text="---";
         class="kw">switch(i)
           {
            case class="num">0 : text=::StringFormat("%.2f", data[class="num">0]);   break;   class=class="str">"cmt">// Sharpe Ratio
            case class="num">1 : text=::StringFormat("%.2f", data[class="num">1]);   break;   class=class="str">"cmt">// Net Profit
            case class="num">2 : text=::StringFormat("%.2f", data[class="num">2]);   break;   class=class="str">"cmt">// Profit Factor
            case class="num">3 : text=::StringFormat("%.2f", data[class="num">3]);   break;   class=class="str">"cmt">// Recovery Factor
            case class="num">4 : text=::StringFormat("%.0f", data[class="num">4]);   break;   class=class="str">"cmt">// Trades
            case class="num">5 : text=::StringFormat("%.0f", data[class="num">5]);   break;   class=class="str">"cmt">// Deals
            case class="num">6 : text=::StringFormat("%.2f%%", data[class="num">6]); break;   class=class="str">"cmt">// Equity DD
            case class="num">7 : text=::StringFormat("%G", data[class="num">7]);     break;   class=class="str">"cmt">// OnTester()
            class="kw">default: break;
           }
         class=class="str">"cmt">//--- Highlight background of table class="type">class="kw">string corresponding to selected tab with class="type">color.
         class=class="str">"cmt">//--- Remaining strings will have class="kw">default class="type">color
         class="type">color clr=(tab_id>class="num">0 ? (i==tab_id-class="num">1 ? C&class="macro">#x27;class="num">223,class="num">242,class="num">231&class="macro">#x27; : clrNONE) : clrNONE);
         
         class=class="str">"cmt">//--- Update captions in cells
         cell_0.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER);
         cell_1.SetText(text);
         cell_1.TextOut(foreground, class="num">4, CELL_H/class="num">2, clr, class="num">0, TA_VCENTER);
        }

◍ 把优化结果画进自定义标签页

在 MT5 优化框架里,把每一帧的盈亏平衡数据塞进图表,靠的是一段数组拷贝加画布刷新。下面这段逻辑先把当前帧的余额序列截出来,再交给统计图表对象渲染。

MQL5 / C++
class="type">class="kw">double seria[];
ArrayCopy(seria, data, class="num">0, DATA_COUNT, ArraySize(data)-DATA_COUNT);
chart_stat.AddSeria(seria, data[class="num">1]>class="num">0);
chart_stat.Update(clr, line_width, false);
if(tab_id==class="num">0)
   this.m_progress_bar.AddResult(data[class="num">1]>class="num">0, false);
逐行看:seria 是接收当前帧余额值的数组;ArrayCopy 从 data 里按 DATA_COUNT 偏移截取尾部片段;AddSeria 的第二个参数用 data[1]>0 判断这一帧是盈是亏,决定线条着色;Update 带 false 表示不强制重绘整个背景。进度条只在 tab_id 为 0 时更新,避免多标签重复计数。 表头文字居中用的是画布坐标取中值:x1、y1 到 x2、y2 四点算出中点,FillRectangle 清掉旧底,再用 Calibri 粗体写 clrMidnightBlue 色文字。TableStatDraw 函数开头就卡了 tab_id>4 直接报错返回——说明这套标签控件最多挂 5 个页(0 到 4)。 开 MT5 自己写个 CFrameViewer 派生类时,注意背景色硬编码 C'195,209,223' 是浅蓝灰,想和暗色主题融合得改这一行。外汇与贵金属回测优化本身高风险,参数曲面漂亮不代表实盘能复现。

MQL5 / C++
class="type">class="kw">double seria[];
ArrayCopy(seria, data, class="num">0, DATA_COUNT, ArraySize(data)-DATA_COUNT);
chart_stat.AddSeria(seria, data[class="num">1]>class="num">0);
chart_stat.Update(clr, line_width, false);
if(tab_id==class="num">0)
   this.m_progress_bar.AddResult(data[class="num">1]>class="num">0, false);

class="type">int x1=chart_stat.HeaderX1();
class="type">int y1=chart_stat.HeaderY1();
class="type">int x2=chart_stat.HeaderX2();
class="type">int y2=chart_stat.HeaderY2();

class="type">int x=(x1+x2)/class="num">2;
class="type">int y=(y1+y2)/class="num">2;

foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF);
foreground.FontSet("Calibri", -class="num">100, FW_BLACK);
foreground.TextOut(x, y, text, ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);
foreground.Update(false);

class="kw">return true;
 }
else
   PrintFormat("%s: FrameInputs() failed. Error %d",__FUNCTION__, GetLastError());
 class="kw">return false;
}
class="type">void CFrameViewer::TableStatDraw(const class="type">uint tab_id, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">bool chart_redraw)
 {
 if(tab_id>class="num">4)
  {
   PrintFormat("%s: Error: Invalid tab ID passed(%u)",__FUNCTION__, tab_id);
   class="kw">return;
  }
 CCanvas *background=this.m_tab_control.GetTabBackground(tab_id);
 CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id);
 CTableDataControl *table_stat=this.GetTableStats(tab_id);

 if(background==NULL || foreground==NULL || table_stat==NULL)
   class="kw">return;

 background.FillRectangle(x, y, x+CELL_W*class="num">2, y+CELL_H, ColorToARGB(C&class="macro">#x27;class="num">195,class="num">209,class="num">223&class="macro">#x27;));

「优化结果面板的表头与网格绘制」

在 MT5 自定义面板里把优化结果做成两张列表,第一步是先铺好标题区和空表体。下面这段代码负责在前景层画一个宽两列的标题条,并在背景层按 2 列 × DATA_COUNT 行拉出灰阶网格,单元格高 CELL_H、宽 CELL_W,边框色用 C'200,200,200'。 标题条写死为 "Optimization results",字体 Calibri、粗体 FW_BLACK、深蓝 clrMidnightBlue,居中对齐。注意 FillRectangle 的填充色是 0x00FFFFFF,即不带透明通道的纯白,盖住底层避免重绘残影。 空表体只填左侧表头、不填数值:循环里 row 从 0 到 RowsTotal()-1,col 取 0 或 1。col 为偶数时按 row 映射成 Sharpe Ratio、Net Profit、Profit Factor、Recovery Factor、Trades、Deals、Equity DD 共 7 类指标名;col 为奇数时仅当 tab_id==0 写 " --- " 占位,其余标签留空。 实跑时若你改了 DATA_COUNT 或 CELL_W/CELL_H,要同步确认 TABLE_OPT_STAT_ID+10*tab_id 的 ID 偏移不与其他面板撞车,否则多标签切换可能互相覆盖。外汇与贵金属品种优化受点差和滑点影响大,回测面板数字仅代表历史样本,实盘表现可能明显偏离。

MQL5 / C++
  foreground.FillRectangle(x+class="num">1, y+class="num">1, x+CELL_W*class="num">2-class="num">1, y+CELL_H-class="num">1, 0x00FFFFFF);
  foreground.FontSet("Calibri", -class="num">100, FW_BLACK);
  foreground.TextOut(x+(CELL_W*class="num">2)/class="num">2, y+CELL_H/class="num">2, "Optimization results", ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);

class=class="str">"cmt">//--- Set table&class="macro">#x27;s identifier and draw table grid
  table_stat.SetID(TABLE_OPT_STAT_ID+class="num">10*tab_id);
  table_stat.DrawGrid(background, x, y+CELL_H, class="num">0, DATA_COUNT, class="num">2, CELL_H, CELL_W, C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;, false);

class=class="str">"cmt">//--- Draw empty table of optimization results - only headers, without values
class=class="str">"cmt">//--- In a loop by table rows
  class="type">int total=table_stat.RowsTotal();
  for(class="type">int row=class="num">0; row<total; row++)
    {
    class=class="str">"cmt">//--- iterate through columns of rows
    for(class="type">int col=class="num">0; col<class="num">2; col++)
      {
      class=class="str">"cmt">//--- Get table cell in current row and column
      CTableCell *cell=table_stat.GetCell(row, col);
      class=class="str">"cmt">//--- Define text in cell
      class=class="str">"cmt">//--- For left cell, these will be headers of results of parameters optimized
      if(col%class="num">2==class="num">0)
        {
        class="type">class="kw">string text="OnTester()";
        class="kw">switch(row)
          {
          case class="num">0 : text="Sharpe Ratio";      break;
          case class="num">1 : text="Net Profit";        break;
          case class="num">2 : text="Profit Factor";     break;
          case class="num">3 : text="Recovery Factor";  break;
          case class="num">4 : text="Trades";            break;
          case class="num">5 : text="Deals";             break;
          case class="num">6 : text="Equity DD";         break;
          class="kw">default: break;
          }
        cell.SetText(text);
        }
      class=class="str">"cmt">//--- For right cell, text will be strikeout for table initialized
      else
        cell.SetText(tab_id==class="num">0 ? " --- " : "");

      class=class="str">"cmt">//--- Print corresponding text in cell

参数表与优化图的画布落点

在 CFrameViewer 里,TableInpDraw 负责把优化输入参数画到指定标签页。tab_id 超过 4 会直接 PrintFormat 报错并返回,说明这套框架最多只预留了 5 个标签页的画布指针。 表头先用背景画布填一个 RGB(195,209,223) 的矩形,前景再叠一个 0x00FFFFFF 的透明矩形,然后用 Calibri 粗体在中间写“Input parameters”,颜色取 clrMidnightBlue。表格网格由 table_inp.DrawGrid 画出,列宽 CELL_W、行高 CELL_H,线色 C'200,200,200'。 ChartOptDraw 同样先卡 tab_id>4 的边界,再取该页的 background 与 foreground 指针。两个绘制函数末尾都调 background.Update(false) 和 foreground.Update(chart_redraw),后者决定是否重绘图表——接 MT5 调试时,把 chart_redraw 置 true 才能看到实时刷新。

MQL5 / C++
class="type">void CFrameViewer::TableInpDraw(const class="type">uint tab_id, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">uint rows, const class="type">bool chart_redraw)
  {
class=class="str">"cmt">//--- Check passed tab ID
   if(tab_id>class="num">4)
     {
      ::PrintFormat("%s: Error: Invalid tab ID passed(%u)",__FUNCTION__, tab_id);
      class="kw">return;
     }
class=class="str">"cmt">//--- Get pointers to objects used on specified tab
   CCanvas         *background=this.m_tab_control.GetTabBackground(tab_id);
   CCanvas         *foreground=this.m_tab_control.GetTabForeground(tab_id);
   CTableDataControl *table_inp=this.GetTableInputs(tab_id);

   if(background==NULL || foreground==NULL || table_inp==NULL)
      class="kw">return;

class=class="str">"cmt">//--- Draw header of optimization parameters table
   background.FillRectangle(x, y, x+CELL_W*class="num">2, y+CELL_H, ::ColorToARGB(C&class="macro">#x27;class="num">195,class="num">209,class="num">223&class="macro">#x27;));
   foreground.FillRectangle(x+class="num">1, y+class="num">1, x+CELL_W*class="num">2-class="num">1, y+CELL_H-class="num">1, 0x00FFFFFF);
   foreground.FontSet("Calibri", -class="num">100, FW_BLACK);
   foreground.TextOut(x+(CELL_W*class="num">2)/class="num">2, y+CELL_H/class="num">2, "Input parameters", ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);

class=class="str">"cmt">//--- Set table&class="macro">#x27;s identifier and draw table grid
   table_inp.SetID(TABLE_OPT_INP_ID+class="num">10*tab_id);
   table_inp.DrawGrid(background, x, y+CELL_H, class="num">0, rows, class="num">2, CELL_H, CELL_W, C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;, false);

class=class="str">"cmt">//--- Update background and foreground canvas
   background.Update(false);
   foreground.Update(chart_redraw);
  }

class="type">void CFrameViewer::ChartOptDraw(const class="type">uint tab_id, const class="type">bool opt_completed, const class="type">bool chart_redraw)
  {
class=class="str">"cmt">//--- Check passed tab ID
   if(tab_id>class="num">4)
     {
      ::PrintFormat("%s: Error: Invalid tab ID passed(%u)",__FUNCTION__, tab_id);
      class="kw">return;
     }
class=class="str">"cmt">//--- Get pointers to objects used on specified tab
   CCanvas         *background=this.m_tab_control.GetTabBackground(tab_id);
   CCanvas         *foreground=this.m_tab_control.GetTabForeground(tab_id);

◍ 优化结果图表的边界与表头绘制逻辑

在 MT5 自定义优化面板里,图表区域不是随便铺的。代码先取表格控件右边界加 10 像素作为图表左缘 x1,顶部对齐统计表 y1,右侧取到标签页右内边距减 10,底部则刨掉标签按钮高度再加 12 像素余量。 尺寸有硬下限:宽度不足 480 或高度不足 180 时直接拉到最小值。这意味着即便你把面板缩得很小,优化结果图也不会塌成不可读的一条线,最小可见面积固定为 480×180。 表头文案按 tab_id 切换——1 是 Sharpe Ratio、2 是 Net Profit、3 是 Profit Factor、4 是 Recovery Factor,其余显示 Click to Replay;优化未结束时整体染成灰蓝 C'195,209,223' 并提示 Waiting。背景用 LightGreen 标识跑完,前景透明叠加文字,字体锁 Calibri 加粗。 最后两步容易看漏:先整块擦成透明,再调 chart_stat.Update 重绘。如果跳过擦除,旧帧会叠在新帧上,MT5 里表现为残影。外汇与贵金属回测优化属高风险验证,参数空间结论仅代表历史样本,实盘可能显著偏离。

MQL5 / C++
  CTab                 *tab=this.m_tab_control.GetTab(tab_id);
  CTableDataControl *table_stat=this.GetTableStats(tab_id);
  CStatChart         *chart_stat=this.GetChartStats(tab_id);
  
  if(background==NULL || foreground==NULL || tab==NULL || table_stat==NULL || chart_stat==NULL)
     class="kw">return;

class=class="str">"cmt">//--- Calculate coordinates of four corners of optimization results chart
  class="type">int x1=table_stat.X2()+class="num">10;
  class="type">int y1=table_stat.Y1();
  class="type">int x2=tab.GetField().Right()-class="num">10;
  class="type">int y2=tab.GetField().Bottom()-tab.GetButton().Height()-class="num">12;

class=class="str">"cmt">//--- Check size limits by minimum width and height(class="num">480 x class="num">180)
  class="type">int w_min=class="num">480;
  if(x2-x1<w_min)
     x2=x1+w_min;
  if(y2-y1<class="num">180)
     y2=y1+class="num">180;

class=class="str">"cmt">//--- Set dimensions of bounding rectangle of optimization results chart
  chart_stat.SetChartBounds(x1, y1, x2, y2);

class=class="str">"cmt">//--- Color and text of chart header
  class="type">color clr=clrLightGreen;   class=class="str">"cmt">// header class="type">color at optimization completion
  class="type">class="kw">string suff=
     (
      tab_id==class="num">1 ? "Results by Sharpe Ratio"      :
      tab_id==class="num">2 ? "Results by Net Profit"        :
      tab_id==class="num">3 ? "Results by Profit Factor"     :
      tab_id==class="num">4 ? "Results by Recovery Factor"   :
                                    "Click to Replay"
     );
  class="type">class="kw">string text="Optimization Completed: "+suff;

class=class="str">"cmt">//--- If optimization is not completed, specify class="type">color and text of header
  if(!opt_completed)
     {
      clr=C&class="macro">#x27;class="num">195,class="num">209,class="num">223&class="macro">#x27;;
      text=::StringFormat("Optimization%sprogress%s", (tab_id==class="num">0 ? " " : " in "), (tab_id==class="num">0 ? "" : ": Waiting ... "));
     }

class=class="str">"cmt">//--- Draw header and text
  background.FillRectangle(x1, class="num">4, x2, y1, ::ColorToARGB(clr));
  foreground.FillRectangle(x1, class="num">4, x2, y2, 0x00FFFFFF);
  foreground.FontSet("Calibri", -class="num">100, FW_BLACK);
  foreground.TextOut((x1+x2)/class="num">2, class="num">4+CELL_H/class="num">2, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER);

class=class="str">"cmt">//--- Erase whole chart of optimization results
  background.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF);
  foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF);

class=class="str">"cmt">//--- Update optimization chart
  chart_stat.Update(clrNONE, class="num">0, chart_redraw);
}

「把帧查看器类挂进现成 EA」

把标准库里的 ExpertMAMA.mq5(路径 \MQL5\Experts\Advisors\)复制到新建的 \MQL5\Experts\FrameViewer\ 目录,改名 ExpertMAMA_Frames.mq5。这一步只是挪文件,不改逻辑,目的是给后续接入留干净的工作副本。 在文件末尾直接追加 CFrameViewer 类的源码,然后声明一个该类类型的对象,并在 EA 的 OnTick 或处理函数中调用与类名同名的成员函数。这样优化跑起来时,帧数据会被实时推到独立窗口。 输入变量名里的下划线('_')可以删掉,比如 InpSignalMAPeriod 写成 InpSignalMAPeriod 缩短为 InpSignalMAPeriod 去下划线形态,表格单元格能宽出几个字符,视觉优化面板不至于挤变形。 编译后跑优化,测试设置本身不重要,用默认 MA 周期 12、偏移 6、SMA 收价就能起。优化前会弹出一个新图表窗口承载所有控件,可拖到副屏,不必在结果图和可视化聊天窗之间来回切。外汇与贵金属优化存在过拟合高风险,实盘前需多周期验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ExpertMAMA.mq5 |
class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Copyright class="num">2000-class="num">2025, MetaQuotes Ltd. |
class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2000-class="num">2025, MetaQuotes Ltd."
class="macro">#class="kw">property link&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"[MQL5官方文档]
class="macro">#class="kw">property version&nbsp;&nbsp; "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Include&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Expert\Expert.mqh>
class="macro">#include <Expert\Signal\SignalMA.mqh>
class="macro">#include <Expert\Trailing\TrailingMA.mqh>
class="macro">#include <Expert\Money\MoneyNone.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- inputs for expert
input class="type">class="kw">string&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; InpExpertTitle&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;&nbsp;"ExpertMAMA";
class="type">int&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Expert_MagicNumber&nbsp;&nbsp;=&nbsp;&nbsp;class="num">12003;
class="type">bool&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Expert_EveryTick&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;&nbsp;false;
class=class="str">"cmt">//--- inputs for signal
input class="type">int&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;InpSignalMAPeriod&nbsp;&nbsp; =&nbsp;&nbsp;class="num">12;
input class="type">int&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;InpSignalMAShift&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;&nbsp;class="num">6;
input ENUM_MA_METHOD&nbsp;&nbsp;&nbsp;&nbsp; InpSignalMAMethod&nbsp;&nbsp; =&nbsp;&nbsp;MODE_SMA;
input ENUM_APPLIED_PRICE InpSignalMAApplied&nbsp;&nbsp;=&nbsp;&nbsp;PRICE_CLOSE;
class=class="str">"cmt">//--- inputs for trailing
input class="type">int&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;InpTrailingMAPeriod =&nbsp;&nbsp;class="num">12;
input class="type">int&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;InpTrailingMAShift&nbsp;&nbsp;=&nbsp;&nbsp;class="num">0;

在 OnInit 里把信号与跟踪挂到专家对象

EA 启动阶段最关键的一步,是把均线信号和均线跟踪止损这两个模块正确挂到全局的 CExpert 实例上。任何一步返回失败,OnInit 都会带负代码退出,MT5 终端日志里能看到具体是哪一号错误。 下面这段初始化代码,先尝试用当前品种和周期初始化专家对象,失败就打印函数名并 Deinit 返回 -1。随后 new 一个 CSignalMA,若指针为空返回 -2,这在外存紧张或基类构造异常时可能出现。 signal 挂进专家用 InitSignal,返回 false 则 -3;接着设 PeriodMA / Shift / Method / Applied 四个参数,再跑 ValidationSettings 做合法性校验,不通过返回 -4。均线跟踪对象 CTrailingMA 的挂载逻辑完全一致,创建失败 -5、InitTrailing 失败 -6。 把这些负返回值记下来很有用:在策略测试器里若 EA 起不来,对照 -1 到 -6 能直接定位是信号层还是跟踪层的问题,不用逐行断点。外汇与贵金属品种波动剧烈,EA 初始化异常可能让你错过关键价位,上实盘前务必在模拟环境跑通整套 OnInit。

MQL5 / C++
input ENUM_MA_METHOD    InpTrailingMAMethod =  MODE_SMA;
input ENUM_APPLIED_PRICE InpTrailingMAApplied=  PRICE_CLOSE;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global expert object                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
CExpert ExtExpert;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialization function of the expert                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit(class="type">void)
  {
class=class="str">"cmt">//--- Initializing expert
   if(!ExtExpert.Init(Symbol(),Period(),Expert_EveryTick,Expert_MagicNumber))
     {
class=class="str">"cmt">//--- failed
       printf(__FUNCTION__+": error initializing expert");
       ExtExpert.Deinit();
       class="kw">return(-class="num">1);
     }
class=class="str">"cmt">//--- Creation of signal object
   CSignalMA *signal=new CSignalMA;
   if(signal==NULL)
     {
class=class="str">"cmt">//--- failed
       printf(__FUNCTION__+": error creating signal");
       ExtExpert.Deinit();
       class="kw">return(-class="num">2);
     }
class=class="str">"cmt">//--- Add signal to expert(will be deleted automatically))
   if(!ExtExpert.InitSignal(signal))
     {
class=class="str">"cmt">//--- failed
       printf(__FUNCTION__+": error initializing signal");
       ExtExpert.Deinit();
       class="kw">return(-class="num">3);
     }
class=class="str">"cmt">//--- Set signal parameters
   signal.PeriodMA(InpSignalMAPeriod);
   signal.Shift(InpSignalMAShift);
   signal.Method(InpSignalMAMethod);
   signal.Applied(InpSignalMAApplied);
class=class="str">"cmt">//--- Check signal parameters
   if(!signal.ValidationSettings())
     {
class=class="str">"cmt">//--- failed
       printf(__FUNCTION__+": error signal parameters");
       ExtExpert.Deinit();
       class="kw">return(-class="num">4);
     }
class=class="str">"cmt">//--- Creation of trailing object
   CTrailingMA *trailing=new CTrailingMA;
   if(trailing==NULL)
     {
class=class="str">"cmt">//--- failed
       printf(__FUNCTION__+": error creating trailing");
       ExtExpert.Deinit();
       class="kw">return(-class="num">5);
     }
class=class="str">"cmt">//--- Add trailing to expert(will be deleted automatically))
   if(!ExtExpert.InitTrailing(trailing))
     {
class=class="str">"cmt">//--- failed
       printf(__FUNCTION__+": error initializing trailing");
       ExtExpert.Deinit();
       class="kw">return(-class="num">6);
     }
class=class="str">"cmt">//--- Set trailing parameters

◍ EA初始化失败的逐级拦截

这段代码是 MT5 专家顾问初始化尾段与事件转发的核心。先把 trailing 对象的四个参数(周期、偏移、方法、应用价)灌进去,紧接着就调 ValidationSettings() 做校验,不通过直接打印错误并返回 -7,同时把 ExtExpert 反初始化掉。 资金模块用 CMoneyNone 占位,new 出来若为 NULL 返回 -8;挂到 Expert 上若 InitMoney 失败返回 -9;连 money.ValidationSettings() 也单独再卡一道,失败返回 -10。指标初始化失败则返回 -11。任何一步报错都不会带着半成品跑策略。 OnDeinit、OnTick、OnTrade 三个事件函数都只是薄转发层:卸载时调 ExtExpert.Deinit(),每笔报价和每笔成交分别丢给 ExtExpert.OnTick() 与 ExtExpert.OnTrade()。你拷这段进 MT5 时,重点看自己改了 InpTrailingMAPeriod 等外部输入后,校验返回值是不是还走 -7 到 -11 的递增编号,方便定位是哪层配置崩了。外汇与贵金属品种波动剧烈,这类初始化拦截能降低实盘带病启动的概率,但无法消除滑点与跳空风险。

MQL5 / C++
  trailing.Period(InpTrailingMAPeriod);
  trailing.Shift(InpTrailingMAShift);
  trailing.Method(InpTrailingMAMethod);
  trailing.Applied(InpTrailingMAApplied);
class=class="str">"cmt">//--- Check trailing parameters
  if(!trailing.ValidationSettings())
    {
      class=class="str">"cmt">//--- failed
      printf(__FUNCTION__+": error trailing parameters");
      ExtExpert.Deinit();
      class="kw">return(-class="num">7);
    }
class=class="str">"cmt">//--- Creation of money object
  CMoneyNone *money=new CMoneyNone;
  if(money==NULL)
    {
      class=class="str">"cmt">//--- failed
      printf(__FUNCTION__+": error creating money");
      ExtExpert.Deinit();
      class="kw">return(-class="num">8);
    }
class=class="str">"cmt">//--- Add money to expert(will be deleted automatically))
  if(!ExtExpert.InitMoney(money))
    {
      class=class="str">"cmt">//--- failed
      printf(__FUNCTION__+": error initializing money");
      ExtExpert.Deinit();
      class="kw">return(-class="num">9);
    }
class=class="str">"cmt">//--- Set money parameters
class=class="str">"cmt">//--- Check money parameters
  if(!money.ValidationSettings())
    {
      class=class="str">"cmt">//--- failed
      printf(__FUNCTION__+": error money parameters");
      ExtExpert.Deinit();
      class="kw">return(-class="num">10);
    }
class=class="str">"cmt">//--- Tuning of all necessary indicators
  if(!ExtExpert.InitIndicators())
    {
      class=class="str">"cmt">//--- failed
      printf(__FUNCTION__+": error initializing indicators");
      ExtExpert.Deinit();
      class="kw">return(-class="num">11);
    }
class=class="str">"cmt">//--- succeed
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Deinitialization function of the expert                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
   ExtExpert.Deinit();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Function-event handler "tick"                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick(class="type">void)
  {
   ExtExpert.OnTick();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Function-event handler "trade"                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTrade(class="type">void)
  {
   ExtExpert.OnTrade();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「把优化结果画进图表的那几行钩子」

EA 在 MT5 里跑批量优化时,默认会把中断后的残次 pass 缓存下来,下次点优化可能从断点续跑。想每次都干净重跑,加一行 #property tester_no_cache 就能强制清缓存,这个细节不注意容易误以为参数空间没扫全。 可视化部分靠 CFrameViewer 对象接手。宏里 REPLAY_DELAY_MS=100 控制回放延迟毫秒数,STAT_LINES=1 只画一条权益线,SELECTED_LINE_WD=3SELECTED_LINE_CLR=clrDodgerBlue 决定选中 pass 的线宽与颜色,改这几个数就能调观察体验。 OnTester 里自定义了优化判据:用夏普比率绝对值乘净利润,净利润为负时把判据取反,再丢给 fw.OnTester()。外汇与贵金属杠杆高、滑点跳空频繁,这套判据只是筛选候选集,实盘前仍要在历史分段里手动复核。 OnTesterInit 在测试初始化时调 fw.OnTesterInit 把上面宏参传进去布局图表,OnTesterDeinit 负责收尾——把这两处和 FrameViewer.mqh 里的实现对照看,就能知道线是怎么刷出来的。

MQL5 / C++
class=class="str">"cmt">//| Function-event handler "timer"                                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer(class="type">void)
  {
   ExtExpert.OnTimer();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Code required to visualize optimization                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- When debugging, if press "Stop" during optimization, next run of optimization will class="kw">continue incomplete passes from stop point
class=class="str">"cmt">//--- In order for each new optimization run to start anew, define preprocessor directive
class="macro">#class="kw">property tester_no_cache
class=class="str">"cmt">//--- Define macro substitutions
class="macro">#define   REPLAY_DELAY_MS        class="num">100            class=class="str">"cmt">// Optimization replay delay in milliseconds
class="macro">#define   STAT_LINES             class="num">1              class=class="str">"cmt">// Number of optimization statistics lines displayed
class="macro">#define   SELECTED_LINE_WD       class="num">3              class=class="str">"cmt">// Thickness of line of selected optimization passage
class="macro">#define   SELECTED_LINE_CLR      clrDodgerBlue  class=class="str">"cmt">// Color of line of selected optimization passage
class=class="str">"cmt">//--- Connect code to work with the optimization results by frame viewer
class="macro">#include  "FrameViewer.mqh"
class=class="str">"cmt">//--- Declare frame viewer object
CFrameViewer fw;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Tester function                                                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OnTester()
  {
class=class="str">"cmt">//--- here insert your own function to calculate optimization criterion
   class="type">class="kw">double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT));
   TesterCritetia=TesterStatistics(STAT_PROFIT)>class="num">0?TesterCritetia:(-TesterCritetia);
class=class="str">"cmt">//--- call at each end of testing and pass optimization criterion as parameter
   fw.OnTester(TesterCritetia);
class=class="str">"cmt">//---
   class="kw">return(TesterCritetia);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterInit function                                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterInit()
  {
class=class="str">"cmt">//--- prepare chart for displaying balance lines
class=class="str">"cmt">//--- STAT_LINES sets number of balance lines on chart,
class=class="str">"cmt">//--- SELECTED_LINE_WD - sets width, SELECTED_LINE_CLR - sets class="type">color of line of selected passage
   fw.OnTesterInit(STAT_LINES, SELECTED_LINE_WD, SELECTED_LINE_CLR);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterDeinit function                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterDeinit()
  {
class=class="str">"cmt">//--- completing optimization

把优化结果变成可回放画面

在 MT5 策略测试器的框架里,OnTesterDeinit 和 OnTesterPass 是两个容易被忽略的生命周期钩子。前者在优化收尾时触发,后者在每一组参数通过测试时触发,把可视化框架的对应方法接进去,就能在测试结束后直接看图形输出而不是只盯净值曲线。 OnChartEvent 里那行把 REPLAY_DELAY_MS 传进去的调用,决定的是回放帧之间的间隔毫秒数。这个值设太小,CPU 占用会飙;设太大,复盘动画卡顿,肉眼难捕捉价格行为细节。外汇与贵金属市场高杠杆、高波动,回测画面仅作概率参考,不构成方向判断。 下面这段是接好框架后的最小可用骨架,复制进 EA 即可在优化完点表头重播:

MQL5 / C++
  fw.OnTesterDeinit();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterPass function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterPass()
  {
class=class="str">"cmt">//--- handle test results and display graphics
   fw.OnTesterPass();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Event handling on chart                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
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">//--- starts playback of frames upon completion of optimization when clicking on header
   fw.OnChartEvent(id,lparam,dparam,sparam,REPLAY_DELAY_MS); class=class="str">"cmt">// REPLAY_DELAY_MS - pause in ms between replay frames
  }

◍ 把这条线请下神坛

这套可视化优化框架说到底只是给策略测试器加了一层自选显示层:Table.mqh 66.62 KB、Controls.mqh 183.33 KB、FrameViewer.mqh 187.03 KB 三个类库加上 ExpertMAMA_Frames.mq5(19.26 KB)的 EA,解压 MQL5.zip(52 KB)进终端目录就能跑。它能把测试报告里的任意字段或每次优化 pass 后自算的数据画到图表上,但画什么、怎么画完全看你自己的交易逻辑,不是官方给的圣杯。 fxsaber 在 2025 年 3 月 21 日的评论点得很直白:opt 格式公开之后,只有传输 opt 文件里没有的数据才值得用帧。也就是说,如果你要的只是参数组合和权益曲线,原生 opt 够了;只有当你要在优化过程里塞入自建指标或中间计算结果,这套 GUI 才有存在价值。 外汇和贵金属市场高杠杆、滑点诡变,任何优化画面都只是历史数据的投影,实盘仍可能偏离。把代码下下来,先拿 MAMA 这个样例 EA 在 MT5 里走一遍优化,确认你的自定义字段真能落进 FrameViewer 再谈改造,比对着文章脑补有用得多。

交给小布盯盘看盘口
这些多标准优化结果的诊断视图,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只需判断哪组参数更贴合自己的风险偏好。

常见问题

除优化标签页在过程中逐次刷新外,夏普、净利润、盈利因子、恢复因子四个标签页需等整轮优化结束才会载入前三通过的数据与曲线。
可以,小布盯盘已将类似的多标准结果视图做成内置模块,无需自己写控件类,切换品种就能看对应优化的分布。
它只重放已执行过的优化操作记录,不会重新计算,方便你对照不同标签下的曲线形态。
对应按恢复因子排前三的通过,拨到哪个位置下方表格与余额图就锁定那一组输入与结果。
本篇定位综合运用,控件类封装留待前篇铺垫,这里专注把实例接到 EA 并实现多标签展示。