使用图形界面处理优化结果·综合运用
从通用数组里拆出余额与回撤
做 MT5 优化结果后处理时,帧数据被压在一个通用数组里,键值是帧编号和品种枚举。余额数组长度若和回撤数组一致,循环里能用公式直接定位;但实际两者大小不同,最后一次迭代必须单独算剩余元素再除以 2,因为回撤在数组尾部且 X、Y 两轴各占一半。 GetFrameData() 先调 FrameFirst() 把指针拨回列表头,随后 FrameNext() 遍历所有帧。传入的 pass_number 不匹配就 continue 跳过,命中后才继续拆包。 拆包第一步取 m_data 总大小,用逗号切 m_name 得到品种数;品种多于 1 时余额范围数 = 品种数 +1,多出来的那段是总余额。循环次数等于余额段数,每次按 STAT_TOTAL + i*m_value 做源偏移,把一段 m_value 长度的数据拷进独立余额结构。 末次迭代时,尾部剩余量 dd_total = data_total - (src_index + m_value),array_size = dd_total / 2.0,从 data_total - dd_total 起连续两步拷出回撤的 X、Y 轴。表格取数另调 GetValue(列,行),不在本方法内。 开 MT5 把下面类方法跑一遍,断点看 m_symbols_balance 和 m_dd_x 的 ArraySize,能验证尾部除以 2 的必要性。外汇与贵金属优化含高风险,回测分布不代表实盘概率。
<span class="keyword">class</span> CFrameGenerator { <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//--- 根据指定帧编号取得数据</span> <span class="keyword">class="type">void</span> GetFrameData(<span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">const</span> <span class="keyword">class="type">ulong</span> pass_number</span>); }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 根据指定的帧编号取得数据 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> CFrameGenerator::GetFrameData(<span class="keyword">const</span> <span class="keyword">class="type">ulong</span> pass_number) { <span class="comment">class=class="str">"cmt">//--- 把帧指针移动到开始</span> ::<span class="functions">FrameFirst</span>(); <span class="comment">class=class="str">"cmt">//--- 展开数据</span> <span style="background-class="type">color:rgb(class="num">255, class="num">242, class="num">153);"><span class="keyword">while</span>(::<span class="functions">FrameNext</span>(m_pass,m_name,m_id,m_value,m_data))</span> { <span class="comment">class=class="str">"cmt">//--- 通过编号不匹配,转向下一个</span> <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"> <span class="keyword">if</span>(m_pass!=pass_number) <span class="keyword">class="kw">continue</span>;</span> <span class="comment">class=class="str">"cmt">//--- 数据数组的大小</span> <span class="keyword">class="type">int</span> data_total=::<span class="functions">ArraySize</span>(m_data); <span class="comment">class=class="str">"cmt">//--- 根据分隔符取得字符串元素</span> <span class="keyword">class="type">class="kw">ushort</span> u_sep =::<span class="functions">StringGetCharacter</span>(<span class="class="type">class="kw">string">","</span>,<span class="number">class="num">0</span>); <span class="keyword">class="type">int</span> symbols_total =::<span class="functions">StringSplit</span>(m_name,u_sep,m_symbols_name); <span class="keyword">class="type">int</span> balances_total =(symbols_total><span class="number">class="num">1</span>)? symbols_total+<span class="number">class="num">1</span> : symbols_total; <span class="comment">class=class="str">"cmt">//--- 设置余额数组的数量大小</span> ::<span class="functions">ArrayResize</span>(m_symbols_balance,balances_total); <span class="comment">class=class="str">"cmt">//--- 在数组中发送数据</span> <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>; i<balances_total; i++) { <span class="comment">class=class="str">"cmt">//--- 释放数据数组</span> ::<span class="functions">ArrayFree</span>(m_symbols_balance[i].m_data); <span class="comment">class=class="str">"cmt">//--- 定义开始复制源数据的索引</span> <span style="background-class="type">color:rgb(class="num">164, class="num">192, class="num">228);"><span class="keyword">class="type">int</span> src_index=STAT_TOTAL+<span class="keyword">class="type">int</span>(i*m_value);</span> <span class="comment">class=class="str">"cmt">//--- 把数据复制到余额结构数组中</span> ::<span class="functions">ArrayCopy</span>(m_symbols_balance[i].m_data,m_data,<span class="number">class="num">0</span>,src_index,(<span class="keyword">class="type">int</span>)m_value); <span class="comment">class=class="str">"cmt">//--- 如果这是最后一次迭代,取得回撤数据</span> <span class="keyword">if</span>(i+<span class="number">class="num">1</span>==balances_total) { <span class="comment">class=class="str">"cmt">//--- 取得剩余数据数量以及在两个轴上的数组大小</span> <span class="keyword">class="type">class="kw">double</span> dd_total =data_total-(src_index+(<span class="keyword">class="type">int</span>)m_value); <span class="keyword">class="type">class="kw">double</span> array_size =dd_total/<span class="number">class="num">2.0</span>; <span class="comment">class=class="str">"cmt">//--- 开始复制的索引</span> src_index=<span class="keyword">class="type">int</span>(data_total-dd_total); <span class="comment">class=class="str">"cmt">//--- 设置回撤数组的大小</span> ::<span class="functions">ArrayResize</span>(m_dd_x,(<span class="keyword">class="type">int</span>)array_size);
◍ 把双数组塞进绘图缓冲区的实操
这段逻辑干的事很直接:先按 array_size 给 m_dd_y 动态扩容量,再用 ArrayCopy 把 m_data 里前半段拷给 m_dd_x、后半段拷给 m_dd_y,偏移量靠 src_index 和 src_index+array_size 卡位。 ::ArrayResize(m_dd_y,(int)array_size); 这一行决定 y 轴缓冲长度,若 array_size 算错,后面拷贝会静默截断。 ArrayCopy 的三参数用法里,目标、源、起始偏移必须对齐:m_data 被当成 [x串 | y串] 的扁平结构,src_index 指向 x 起点,加 array_size 才到 y 起点。外汇与贵金属行情跳空多,这种拷贝若用在实时回测图,建议先打印 array_size 与 ::ArraySize(m_data) 比对。 CFrameGenerator::GetValue 是另一个落点:用列、行双下标取字符串,越界直接返回空串。它不抛错,调用方拿空串当正常值是常见 bug 源。 在 MT5 里把这段粘进 EA 的 OnTimer,先手动设 array_size=100 跑一轮,看 UpdateDrawdownGraph 出来的点是否和 m_dd_x/m_dd_y 一一对应,就能验证缓冲逻辑对不对。
::ArrayResize(m_dd_y,(class="type">int)array_size); class=class="str">"cmt">//--- 按顺序复制数据 ::ArrayCopy(m_dd_x,m_data,class="num">0,src_index,(class="type">int)array_size); ::ArrayCopy(m_dd_y,m_data,class="num">0,src_index+(class="type">int)array_size,(class="type">int)array_size); } } class=class="str">"cmt">//--- 刷新图形并停止循环 UpdateMSBalanceGraph(); UpdateDrawdownGraph(); break; } } class CFrameGenerator { class="kw">public: class=class="str">"cmt">//--- 从指定单元返回数值 class="type">class="kw">string GetValue(const class="type">uint column_index,const class="type">uint row_index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从指定单元返回数值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CFrameGenerator::GetValue(const class="type">uint column_index,const class="type">uint row_index) { class=class="str">"cmt">//--- 检查是否超出列的范围 class="type">uint csize=::ArraySize(m_columns); if(csize<class="num">1 || column_index>=csize) class="kw">return(""); class=class="str">"cmt">//--- 检查是否超过行的范围 class="type">uint rsize=::ArraySize(m_columns[column_index].m_rows); if(rsize<class="num">1 || row_index>=rsize) class="kw">return(""); class=class="str">"cmt">//--- class="kw">return(m_columns[column_index].m_rows[row_index]); }
「把多品种优化结果塞进GUI表格和图表」
CFrameGenerator 里除了常规图形对象,还挂了 m_graph_ms_balance 和 m_graph_drawdown 两个 CGraphic 指针,专门负责把余额和回撤画出来。它们必须在 OnTesterInitEvent() 最开头就接好 GUI 元件指针,否则后续刷新会指向空对象。 表格数据由 CProgram::GetFrameDataToTable() 填充:先通过 CopyHeaders() 拿表头数组,算出列数,再把表格重建成 100 行。注意代码中 m_table_param.Rebuilding(columns_total,100,true) 的 100 是硬写的行数上限,品种或参数组合超过这个数会截断。 填值时按列循环,对第 1、2、4、5 列(利润等相关项)用两位小数显示,其余取整。选结果页默认按第二列即利润排序,这是在 OnTesterDeinit() 调完该方法后发生的。 用户点表格任意行会发 ON_CLICK_LIST_ITEM 自定义事件,第一列存的编号可回传 CFrameGenerator::GetFrameData() 取完整结果,多品种余额图随即在余额页刷新。外汇与贵金属测试受点差滑点影响大,GUI 看到的“优解”只是历史样本拟合,实盘可能显著偏离。 让小布替你跑这套 打开 MT5 策略测试器,把表格行数从 100 改到 500 试试,若你的优化组合超百组,原代码会静默丢数据。
class="macro">#include <Graphics\Graphic.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于操作优化结果的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CFrameGenerator { class="kw">private: class=class="str">"cmt">//--- 数据可视化的图形指针 CGraphic *m_graph_ms_balance; CGraphic *m_graph_drawdown; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 策略测试器事件处理函数 class="type">void OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results,CGraphic *graph_ms_balance,CGraphic *graph_drawdown); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 应当在 OnTesterInit() 处理函数中调用 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results, CGraphic *graph_ms_balance,CGraphic *graph_drawdown) { m_graph_balance =graph_balance; m_graph_results =graph_results; m_graph_ms_balance =graph_ms_balance; m_graph_drawdown =graph_drawdown; } class CProgram { class="kw">private: class=class="str">"cmt">//--- 取得优化结果表格的帧数据 class="type">void GetFrameDataToTable(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 取得优化结果表格中的数据 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::GetFrameDataToTable(class="type">void) { class=class="str">"cmt">//--- 取得表头 class="type">class="kw">string headers[]; m_frame_gen.CopyHeaders(headers); class=class="str">"cmt">//--- 设置表格大小 class="type">uint columns_total=::ArraySize(headers); m_table_param.Rebuilding(columns_total,class="num">100,true); class=class="str">"cmt">//--- 设置表头和数据类型 for(class="type">uint c=class="num">0; c<columns_total; c++) { m_table_param.DataType(c,TYPE_DOUBLE); m_table_param.SetHeaderText(c,headers[c]); } class=class="str">"cmt">//--- 使用帧中的数据填充表格 for(class="type">uint c=class="num">0; c<columns_total; c++) { for(class="type">uint r=class="num">0; r<m_table_param.RowsTotal(); r++) { if(c==class="num">1 || c==class="num">2 || c==class="num">4 || c==class="num">5) m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),class="num">2); else m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),class="num">0); } } class=class="str">"cmt">//--- 刷新表格
表格点击如何联动框架数据
在 MT5 自定义面板里,参数表(m_table_param)只是前端展示,真正触发分析动作的是事件回调。上面这段逻辑挂在 CProgram::OnEvent 里,靠 CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM 识别「用户点了表格某行」这一事件。 先判断 lparam 是否等于参数表自身 ID,避免和其他自定义控件事件混淆。确认命中后,用 m_table_param.GetValue(0, m_table_param.SelectedItem()) 取第 0 列、当前选中行的数值,强制转成 ulong 当作「通过编号」pass。 拿到 pass 之后直接调 m_frame_gen.GetFrameData(pass),把编号喂给生成框架去拉对应帧数据。整套链路没有轮询,纯事件驱动,CPU 占用只在点击瞬间冒头。 调试时若发现点了行没反应,优先打印 m_table_param.Id() 和 lparam 是否相等;不少 bug 出在多个 CCustomTable 共用同一个自定义事件偏移,导致 id 对得上但 lparam 串台。外汇与贵金属图表加载这类 EA 面板时波动快,事件可能密集触发,建议在 OnEvent 内加去重时间戳。
m_table_param.Update(true); m_table_param.GetScrollHPointer().Update(true); m_table_param.GetScrollVPointer().Update(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件处理函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::OnEvent(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(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM) { if(lparam==m_table_param.Id()) { class=class="str">"cmt">//--- 从表格中取得通过编号 class="type">ulong pass=(class="type">ulong)m_table_param.GetValue(class="num">0,m_table_param.SelectedItem()); class=class="str">"cmt">//--- 根据通过编号取得数据 m_frame_gen.GetFrameData(pass); } class=class="str">"cmt">//--- class="kw">return; } ... }
◍ 把这套多品种优化框架跑起来
这套处理优化结果的思路还没走到头,作者明确说后续会继续扩展 GUI 创建库,做出更顺手的多品种分析工具。当前版本已经把标准 MACD Sample 改成了多交易品种框架,配套 6 个 mqh 文件分别管程序类、GUI 实现、策略类、字符串格式和帧操作,压缩包 Experts.zip 体积 23.7 KB,可直接下到 MT5 的 Experts 目录里测。 想验证就开 MT5 加载 MacdSampleMSFrames.mq5,用多品种回测跑一遍优化,再切到 Balance 标签页看多品种权益曲线是否随表格行联动刷新。外汇和贵金属品种波动大、杠杆高,回测顺不代表实盘能复现,参数过拟合的概率不低。 留言区里 fxsaber 点过一句:标准优化器缺的就是这种能把曲线和表格绑在一起的方案。真要自己加功能,先改 FrameGenerator.mqh 里的优化结果类,比重写 GUI 库省事得多。