可视化使用选定标准优化的结果·进阶篇
📊

可视化使用选定标准优化的结果·进阶篇

(2/3)· 合并结果页、用代码高亮表格行、键盘控选——把 MT5 优化后的海量结果变成一眼可判的图表

实战向进阶 第 2/3 篇
优化跑完几千组参数,多数人还在手动翻表格找那条最稳的权益曲线。把行高亮和余额图拆成两页看,切换一次就丢一次上下文。本篇把结果页和余额页合并,并用键盘直接控选,省掉来回跳转的损耗。

◍ 把优化结果画成可刷新的曲线

在 MT5 优化器跑完之前,图形框架会逐帧把每组参数对应的权益序列塞进曲线对象。盈利帧用 lime green、亏损帧用 red,颜色由 m_data 里利润下标的值实时决定,肉眼能直接区分好参数和坏参数。 帧循环里每推一帧就调用 ArraySize 取序列长度存进 m_curve_max,同时 m_frames_counter 自增。循环结束统一把计数器归零,再用 ArrayMaximum 找出最长序列长度 x_max,拿它去设 X 轴范围与步长——步长按 x_max/8.0 取整,轴上一屏大约铺 8 个刻度。 真正驱动最佳结果可视化的入口是 GetBestOptimizationResults:先亮进度条,然后在 UpdateBestResultsGraph 返回 true 且未点停止的窗口里滚动刷新,标签写「Selection of results: 当前帧/总帧数」。OnTesterDeinitEvent 里优化一结束就调它,随后解锁界面、统计盈亏、把帧数据倒进结果表。外汇与贵金属优化高风险,过拟合的「最佳」参数实盘可能迅速衰减。 让小布替你跑这套 把上面循环逻辑抄进你自己的 CFrameGenerator 派生类,改 clrLimeGreen / clrRed 成你盯盘配色,开 MT5 策略测试器跑一组 EURUSD 参数扫描,看进度条走完能否出图。

MQL5 / C++
  curve.Color((m_data[m_profit_index]>=class="num">0)? ::ColorToARGB(clrLimeGreen) : ::ColorToARGB(clrRed));
  curve.Update(serie);
  class=class="str">"cmt">//--- 取得序列的大小
  m_curve_max[i]=::ArraySize(serie);
  class=class="str">"cmt">//--- 增加帧计数器
  m_frames_counter++;
  class="kw">return(true);
   }
 }
class=class="str">"cmt">//--- 重置帧计数器
  m_frames_counter=class="num">0;
class=class="str">"cmt">//--- 使用元件最大数量定义行
  class="type">class="kw">double x_max=m_curve_max[::ArrayMaximum(m_curve_max)];
class=class="str">"cmt">//--- 水平轴属性
  CAxis *x_axis=m_graph_best.XAxis();
  x_axis.Min(class="num">0);
  x_axis.Max(x_max);
  x_axis.DefaultStep((class="type">int)(x_max/class="num">8.0));
class=class="str">"cmt">//--- 更新图形
  m_graph_best.CalculateMaxMinValues();
  m_graph_best.CurvePlotAll();
  m_graph_best.Update();
  class="kw">return(class="kw">false);
  }
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
  class=class="str">"cmt">//--- 取得最佳优化结果
  class="type">void                GetBestOptimizationResults(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 取得最佳优化结果                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::GetBestOptimizationResults(class="type">void)
  {
class=class="str">"cmt">//--- 显示进度条
  m_progress_bar.Show();
class=class="str">"cmt">//--- 可视化取得最佳结果
  class="type">int best_results_total=m_frame_gen.BestResultsTotal();
  while(m_frame_gen.UpdateBestResultsGraph() && !::IsStopped())
    {
    class=class="str">"cmt">//--- 更新进度条
    m_progress_bar.LabelText("Selection of results: "+class="type">class="kw">string(m_frame_gen.CurrentFrame())+"/"+class="type">class="kw">string(best_results_total));
    m_progress_bar.Update((class="type">int)m_frame_gen.CurrentFrame(),best_results_total);
    }
class=class="str">"cmt">//--- 隐藏进度条
  m_progress_bar.Hide();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 优化完成事件                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnTesterDeinitEvent(class="type">void)
  {
class=class="str">"cmt">//--- 优化完成
  m_frame_gen.OnTesterDeinitEvent();
class=class="str">"cmt">//--- 可视化接收到的最佳结果
  GetBestOptimizationResults();
class=class="str">"cmt">//--- 使得界面可用
  IsLockedGUI(true);
class=class="str">"cmt">//--- 计算正面和负面的结果
  CalculateProfitsAndLosses();
class=class="str">"cmt">//--- 把数据输出到优化结果表格
  GetFrameDataToTable();
class=class="str">"cmt">//--- 初始化 GUI 核心
  CWndEvents::InitializeCore();
  }

「用代码接管表格行的选中高亮」

手动点选表格行只是交互的起点,实盘里更常见的是用键盘 Up / Down / Home / End 让程序自动跳行高亮。CTable 类里已经给了公有的 SelectRow() 方法,只接收一个参数——目标行索引,就能把高亮挪过去。 SelectRow() 内部先判越界、判重复高亮,再把当前索引和上一帧高亮索引一起丢给抽出来的 DrawRow()。DrawRow() 把背景、网格、图片、文字按序重绘,避免和鼠标事件、悬停重绘的逻辑写两遍。 下面的 DrawRow() 片段里有个硬数据点:编程模式(is_user=false)下,如果 prev 和 item 不同且都有效,rows_total 直接置 2,也就是一次重画新旧两行;否则只画 1 行。自定义模式则要求 prev 也非 WRONG_VALUE 才计为 2。 把这段塞进你自己的面板类,接上键盘消息回调,就能在 MT5 里不碰鼠标也能扫表。外汇与贵金属行情跳得快,这类键盘导航在盯盘时可能降低误操作概率,但杠杆品种仍属高风险,参数自己压测。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据指定模式画出指定的表格行                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTable::DrawRow(class="type">int &indexes[],class="kw">const class="type">int item_index,class="kw">const class="type">int prev_item_index,class="kw">const class="type">bool is_user=true)
  {
   class="type">int x1=class="num">0,x2=m_table_x_size-class="num">2;
   class="type">int y1[class="num">2]={class="num">0},y2[class="num">2]={class="num">0};
class=class="str">"cmt">//--- 要画的行数和列数
   class="type">uint rows_total     =class="num">0;
   class="type">uint columns_total =m_columns_total-class="num">1;
class=class="str">"cmt">//--- 如果突出显示行的方法是编程
   if(!is_user)
      rows_total=(prev_item_index!=WRONG_VALUE && item_index!=prev_item_index)? class="num">2 : class="num">1;
   else
      rows_total=(item_index!=WRONG_VALUE && prev_item_index!=WRONG_VALUE && item_index!=prev_item_index)? class="num">2 : class="num">1;
class=class="str">"cmt">//--- 画出行的背景
   for(class="type">uint r=class="num">0; r<rows_total; r++)
     {
      class=class="str">"cmt">//--- 计算行的顶部和底部边界坐标
      y1[r] =m_rows[indexes[r]].m_y+class="num">1;
      y2[r] =m_rows[indexes[r]].m_y2-class="num">1;
      class=class="str">"cmt">//--- 定义相对突出显示模式的行的焦点
      class="type">bool is_item_focus=class="kw">false;
      if(!m_lights_hover)
         is_item_focus=(indexes[r]==item_index && item_index!=WRONG_VALUE);
      else
         is_item_focus=(item_index==WRONG_VALUE)?(indexes[r]==prev_item_index) :(indexes[r]==item_index);
      class=class="str">"cmt">//--- 画出行的背景
      m_table.FillRectangle(x1,y1[r],x2,y2[r],RowColorCurrent(indexes[r],(is_user)? is_item_focus : class="kw">false));
     }
class=class="str">"cmt">//--- 画出边界
   for(class="type">uint r=class="num">0; r<rows_total; r++)
     {
      for(class="type">uint c=class="num">0; c<columns_total; c++)
         m_table.Line(m_columns[c].m_x2,y1[r],m_columns[c].m_x2,y2[r],::ColorToARGB(m_grid_color));
     }
class=class="str">"cmt">//--- 画出图片
   for(class="type">uint r=class="num">0; r<rows_total; r++)
     {
      for(class="type">uint c=class="num">0; c<m_columns_total; c++)
       {
       class=class="str">"cmt">//--- 画出图片,如果 (class="num">1) 它位于表格单元中 以及 (class="num">2) 文字在列中是左对齐的
       if(ImagesTotal(c,indexes[r])>class="num">0 && m_columns[c].m_text_align==ALIGN_LEFT)
          CTable::DrawImage(c,indexes[r]);
       }
     }
class=class="str">"cmt">//--- 用于计算坐标For calculating the coordinates
   class="type">int x=class="num">0,y=class="num">0;
class=class="str">"cmt">//--- 文字对齐模式
   class="type">uint text_align=class="num">0;

表格行高亮与滚动联动的实现细节

在自绘表格控件里,选中某一行不只是改个底色,还要同步滚动条位置,否则用户高亮了却看不见。CTable::SelectRow 这个函数就把「记录选中状态 → 重绘 → 滚动对齐」三件事绑在了一起。 先看状态管理:m_selected_item 存当前行,m_prev_selected_item 存上一行。第一次选中时(prev 为 WRONG_VALUE)只重绘自己;之后则把相邻区间放进 indexes[2],交给 DrawRow 一次性刷新新旧两行,避免整表重画。 滚动对齐是这段最容易被抄错的地方。row_index==0 就滚到顶,>= m_rows_total-1 就滚到底;若在可见区上方就 CurrentPos()-1,在下方就 +1。注意这里加减的是「1 个滚动步长」而非像素,所以滚动条分辨率要和行高匹配,否则高亮行会差一行露在边界外。 外汇与贵金属图表上挂这类自定义面板属高风险操作,参数不匹配可能导致面板错位或卡顿,建议在 MT5 策略测试器里先用 EURUSD 1 分钟数据验证滚动逻辑再上真仓。

MQL5 / C++
class=class="str">"cmt">//--- 画出文字
  for(class="type">uint c=class="num">0; c<m_columns_total; c++)
    {
      class=class="str">"cmt">//--- 取得文字的 (class="num">1) X 坐标 和(class="num">2) 对齐模式
      x       =TextX(c);
      text_align =TextAlign(c,TA_TOP);
      class=class="str">"cmt">//---
      for(class="type">uint r=class="num">0; r<rows_total; r++)
        {
         class=class="str">"cmt">//--- (class="num">1) 计算坐标 以及 (class="num">2) 画出文字
         y=m_rows[indexes[r]].m_y+m_label_y_gap;
         m_table.TextOut(x,y,m_columns[c].m_rows[indexes[r]].m_short_text,TextColor(c,indexes[r]),text_align);
        }
    }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在表格中突出显示指定的行                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTable::SelectRow(class="kw">const class="type">int row_index)
  {
class=class="str">"cmt">//--- 检查是否超出范围
   if(!CheckOutOfRange(class="num">0,(class="type">uint)row_index))
      class="kw">return;
class=class="str">"cmt">//--- 如果这一行已经突出显示了
   if(m_selected_item==row_index)
      class="kw">return;
class=class="str">"cmt">//--- 行的当前和之前的索引
   m_prev_selected_item =(m_selected_item==WRONG_VALUE)? row_index : m_selected_item;
   m_selected_item       =row_index;
class=class="str">"cmt">//--- 某种顺序的值的数组
   class="type">int indexes[class="num">2];
class=class="str">"cmt">//--- 如果是第一次
   if(m_prev_selected_item==WRONG_VALUE)
      indexes[class="num">0]=m_selected_item;
   else
     {
       indexes[class="num">0] =(m_selected_item>m_prev_selected_item)? m_prev_selected_item : m_selected_item;
       indexes[class="num">1] =(m_selected_item>m_prev_selected_item)? m_selected_item : m_prev_selected_item;
     }
class=class="str">"cmt">//--- 根据设置的模式画出指定的表格行
   DrawRow(indexes,m_selected_item,m_prev_selected_item,class="kw">false);
class=class="str">"cmt">//--- 取得边界内可见区域的索引
   VisibleTableIndexes();
class=class="str">"cmt">//--- 把滚动条移动到指定行
   if(row_index==class="num">0)
     {
       VerticalScrolling(class="num">0);
     }
   else if((class="type">uint)row_index>=m_rows_total-class="num">1)
     {
       VerticalScrolling(WRONG_VALUE);
     }
   else if(row_index<(class="type">int)m_visible_table_from_index)
     {
       VerticalScrolling(m_scrollv.CurrentPos()-class="num">1);
     }
   else if(row_index>=(class="type">int)m_visible_table_to_index-class="num">1)
     {
       VerticalScrolling(m_scrollv.CurrentPos()+class="num">1);
     }
   }

◍ 用侧栏表格和曲线联动看清多品种回撤

早期版本里,多交易品种余额和存款回撤只有在结果表格高亮某行时才画到图上,且右侧标曲线名;当测试品种一多,固定 Y 轴尺寸塞不下所有曲线。现在的做法是右侧挂一个带滚动条的 CTable 列表,把所有余额名称列出来,用颜色组件和图形曲线一一对应,解决辨识问题。 取品种靠 CProgram::GetFrameSymbolsToTable(),帧数据到手后从 string 参数里扒出交易品种数组;若品种数大于 1,余额数组要多开一个元素,下标 0 存总余额(BALANCE)。表格只设 1 列,行数等于曲线数,列宽写死 111 像素,表头叫 “Balances”,循环里填名并刷 ARGB 颜色。 光高亮表格不够直观,CProgram::SelectCurve() 按传入的 pass 索引搜曲线:曲线名直接等于 pass 索引字符串,循环比对就能定位。找到后不能只改色——会淹在别的线里,得把目标曲线和最后画的曲线指针调换名称与 Y 数据集,再把最新曲线线宽颜色设成黑色压到顶层。 联动做完后,点表格任一行,图里对应余额曲线就自动浮到最前。外汇与贵金属回测涉及杠杆与滑点,多品种资金曲线仅反映历史测试概率,实盘偏差风险高,开 MT5 把下面代码塞进 EA 面板类里即可验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 把帧交易品种填充到表格                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::GetFrameSymbolsToTable(class="type">void)
  {
class=class="str">"cmt">//--- 取得交易品种列表和曲线的数量
   class="type">class="kw">string symbols[];
   class="type">int symbols_total  =m_frame_gen.CopySymbols(symbols);
   class="type">int balances_total =(symbols_total>class="num">1)? symbols_total+class="num">1 : symbols_total;
class=class="str">"cmt">//--- 设置表格大小
   m_table_symbols.Rebuilding(class="num">1,balances_total,true);
class=class="str">"cmt">//--- 列表的列宽
   class="type">int width[]={class="num">111};
   m_table_symbols.ColumnsWidth(width);
class=class="str">"cmt">//--- 设置抬头
   m_table_symbols.SetHeaderText(class="num">0,"Balances");
class=class="str">"cmt">//--- 使用帧中的数据填充表格
   for(class="type">uint r=class="num">0;r<m_table_symbols.RowsTotal(); r++)
     {
      class="type">uint clr=m_graph3.GetGraphicPointer().CurveGetByIndex(r).Color();
      m_table_symbols.TextColor(class="num">0,r,::ColorToARGB(clr));
      m_table_symbols.SetValue(class="num">0,r,(symbols_total>class="num">1)?(r<class="num">1)? "BALANCE" : symbols[r-class="num">1]: symbols[r],class="num">0);
     }
class=class="str">"cmt">//--- 更新表格
   m_table_symbols.Update(true);
   m_table_symbols.GetScrollHPointer().Update(true);
   m_table_symbols.GetScrollVPointer().Update(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 优化结束事件                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::SelectCurve(class="kw">const class="type">class="kw">ulong pass)
  {
   CGraphic *graph=m_graph5.GetGraphicPointer();
class=class="str">"cmt">//--- 根据通过索引来搜索曲线
   class="type">class="kw">ulong curve_index =class="num">0;
   class="type">int curves_total  =graph.CurvesTotal();
   for(class="type">int i=class="num">0; i<curves_total; i++)
     {
      if(pass==(class="type">class="kw">ulong)graph.CurveGetByIndex(i).Name())
        {
         curve_index=i;
         break;
        }
     }
class=class="str">"cmt">//--- 图表中突出显示的和最新的曲线
   CCurve *selected_curve =graph.CurveGetByIndex((class="type">int)curve_index);
   CCurve *last_curve     =graph.CurveGetByIndex((class="type">int)curves_total-class="num">1);
class=class="str">"cmt">//--- 复制突出显示和最新曲线的数据数组
   class="type">class="kw">double y1[],y2[];
   class="type">class="kw">string name1=selected_curve.Name();
   class="type">class="kw">string name2=last_curve.Name();
   selected_curve.GetY(y1);
   last_curve.GetY(y2);
class=class="str">"cmt">//---
   last_curve.Name(name1);
   selected_curve.Name(name2);
   last_curve.Update(y1);
   selected_curve.Update(y2);
class=class="str">"cmt">//---

「把曲线画进图表的最后几步」

在 MQL5 里画出自定义曲线,光算好点不够,还得把样式和刷新动作补上。上面这段代码把最近一条曲线线宽设成 2、颜色设为黑色,视觉上能和背景里的其他元素拉开层次。 真正让图出来的关键是后面两句:CurvePlotAll() 把缓存里的所有曲线一次性送进图形资源,Update() 触发客户端重绘。少掉任意一个,MT5 子窗口里都可能看不到新线,或者只显示半截。 实操时建议把这两句放在数据填充循环之外、函数收尾前,避免每填一个点就重绘一次拖慢回测。外汇与贵金属行情跳空频繁,这类自定义图形仅作辅助参考,下单仍须自担高风险。

MQL5 / C++
  last_curve.LinesWidth(class="num">2);
  last_curve.Color(clrBlack);
class=class="str">"cmt">//--- 更新图形
  graph.CurvePlotAll();
  graph.Update();
}
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到优化结果的权益与回撤叠加视图,把重复劳动交给小布,你专注决策。

常见问题

分开查看需要在高亮表格行后再切页面才能看余额曲线,上下文容易断。合并后高亮即出图,能更连续地评估某标准下的优化结果,概率上更利于快速排除差解。
文章在开发库强化了 CTable 类,通过捕获键盘事件改变选中行状态并触发重绘,具体映射逻辑见操作帧数据辅助方法与事件处理小节,属于进阶实战改造。
它用来回放某一组最佳结果在测试中的权益变化过程,配合下拉框切换标准,可横向对比不同优化准则下的表现,外汇贵金属测试仍属高风险验证。
可以,小布已内置对应品种的权益与回撤诊断视图,你不必自己写 GUI,打开品种页就能看到类似合并页的呈现,节省搭建时间。
从统一的帧数组里取,前文已讲过多数据种类存帧与展开,本篇用 CComboBox 绑定帧列表来生成最佳结果表格,属于延续前篇结构。