可视化使用选定标准优化的结果·进阶篇
(2/3)· 合并结果页、用代码高亮表格行、键盘控选——把 MT5 优化后的海量结果变成一眼可判的图表
◍ 把优化结果画成可刷新的曲线
在 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 参数扫描,看进度条走完能否出图。
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 里不碰鼠标也能扫表。外汇与贵金属行情跳得快,这类键盘导航在盯盘时可能降低误操作概率,但杠杆品种仍属高风险,参数自己压测。
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 分钟数据验证滚动逻辑再上真仓。
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 面板类里即可验证。
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 子窗口里都可能看不到新线,或者只显示半截。 实操时建议把这两句放在数据填充循环之外、函数收尾前,避免每填一个点就重绘一次拖慢回测。外汇与贵金属行情跳空频繁,这类自定义图形仅作辅助参考,下单仍须自担高风险。
last_curve.LinesWidth(class="num">2); last_curve.Color(clrBlack); class=class="str">"cmt">//--- 更新图形 graph.CurvePlotAll(); graph.Update(); }