使用图形界面处理优化结果·进阶篇
🖥️

使用图形界面处理优化结果·进阶篇

(2/3)· 当优化跑出成千上万组参数,靠肉眼翻日志挑最优组合基本等于浪费时间

进阶 第 2/3 篇
把优化结果导成 CSV 再手动筛,不仅慢还容易漏掉回撤异常的组合。用 GUI 直接把前 100 个最优结果钉在表格里,点一行就能拉出对应多品种余额和回撤,分析效率完全是两个量级。

◍ 从成交记录里抠出交易品种清单

做多品种回测报告时,第一步是把账户里真实跑过的品种名捞出来,而不是拍脑袋写死。下面这段逻辑遍历成交索引,用 StringFind 判断品种是否已在逗号分隔的 m_report_symbols 里,没有就追加,最后用 StringSplit 按逗号切开得到品种数。 关键点在于 m_report_symbols 为空时直接放品种名,否则前缀加逗号,避免首元素出现空串。StringSplit 返回的 symbols_total 就是后续余额数组维度的基准,多品种时总余额行要占一个额外槽位,所以 ArrayResize 用 m_symbols_total+1。 外汇与贵金属杠杆高,回测品种清单若漏掉某个实跑品种,统计的权益曲线和回撤就可能失真,开 MT5 跑一遍 SelectByIndex 遍历能立刻验证你历史成交覆盖了多少符号。 把余额塞进主数组时,CopyDataToMainArray 先按 balances_total 循环拷贝各品种 m_data,末尾再把回撤的 m_dd_x 与 m_dd_y 各拷一份,长度按 data_total*2 扩,保证绘图帧里回撤段不丢。

MQL5 / C++
if(!m_deal_info.SelectByIndex(i))
   class="kw">continue;
if(m_deal_info.Symbol()=="")
   class="kw">continue;
if(::StringFind(m_report_symbols,m_deal_info.Symbol(),class="num">0)==-class="num">1)
   ::StringAdd(m_report_symbols,(m_report_symbols=="")? m_deal_info.Symbol() : ","+m_deal_info.Symbol());
class="type">class="kw">ushort u_sep=::StringGetCharacter(",",class="num">0);
class="type">int symbols_total=::StringSplit(m_report_symbols,u_sep,m_symbols_name);
class="kw">return(symbols_total);
::ArrayResize(m_symbols_balance,(m_symbols_total>class="num">1)? m_symbols_total+class="num">1 : class="num">1);
class="type">void CFrameGenerator::CopyDataToMainArray(class="type">void)
{
 class="type">int balances_total=::ArraySize(m_symbols_balance);
 class="type">int data_total=::ArraySize(m_symbols_balance[class="num">0].m_data);
 for(class="type">int i=class="num">0; i<=balances_total; i++)
   {
    class="type">int array_size=::ArraySize(m_balances);
    if(i<balances_total)
      {
       ::ArrayResize(m_balances,array_size+data_total);
       ::ArrayCopy(m_balances,m_symbols_balance[i].m_data,array_size);
      }
    else
      {
       data_total=::ArraySize(m_dd_x);
       ::ArrayResize(m_balances,array_size+(data_total*class="num">2));
       ::ArrayCopy(m_balances,m_dd_x,array_size);
       ::ArrayCopy(m_balances,m_dd_y,array_size+data_total);
      }
   }
}

把回测统计塞进数据帧的实操写法

在 MT5 的遗传优化里,想让每一代候选参数附带自己的净值曲线与关键指标,就得在 OnTester() 里主动抓取 TesterStatistics 并推给 FrameAdd。下面这段 CFrameGenerator 的代码就是干这个的:它先把 m_balances 复制进目标数组,再从偏移 STAT_TOTAL 开始存余额序列,数组头 6 个槽位留给汇总值。 GetStatData 里 dst_array[0] 到 [5] 分别填了总余额、自定义优化标准值、STAT_PROFIT 净利润、STAT_TRADES 交易次数、STAT_EQUITY_DDREL_PERCENT 最大回撤百分比、STAT_RECOVERY_FACTOR 采收率。注意 STAT_TOTAL 这个常量决定了余额数据从哪儿起头,改它就能腾出更多头部空间放你自己的字段。 OnTesterEvent 负责串流程:先 GetBalanceData 拿点数,再 GetStatData 装数据,最后 FrameAdd(m_report_symbols,1,data_count,stat_data) 发帧。若返回 false,终端会打印 Frame add error 加错误码,方便你当场排查是帧符号不对还是数组长度踩线。 外汇与贵金属品种做这类优化回测波动剧烈,最大回撤可能瞬间超过历史均值,任何统计结论只代表过去样本,实盘仍属高风险。

MQL5 / C++
class="type">void CFrameGenerator::GetStatData(class="type">class="kw">double &dst_array[],class="type">class="kw">double on_tester_value)
  {
class=class="str">"cmt">//--- 复制数组
  ::ArrayResize(dst_array,::ArraySize(m_balances)+STAT_TOTAL);
  ::ArrayCopy(dst_array,m_balances,STAT_TOTAL,class="num">0);
class=class="str">"cmt">//--- 使用测试结果填充数组的第一个值 (STAT_TOTAL) 
  dst_array[class="num">0] =class="num">0;                                                        class=class="str">"cmt">// 总余额
  dst_array[class="num">1] =on_tester_value;                                          class=class="str">"cmt">// 自定义优化标准的数值
  dst_array[class="num">2] =::TesterStatistics(STAT_PROFIT);                          class=class="str">"cmt">// 净利润
  dst_array[class="num">3] =::TesterStatistics(STAT_TRADES);                          class=class="str">"cmt">// 交易数量
  dst_array[class="num">4] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT);            class=class="str">"cmt">// 最大回撤 %
  dst_array[class="num">5] =::TesterStatistics(STAT_RECOVERY_FACTOR);                 class=class="str">"cmt">// 采收率
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 准备数组用于余额数值并且把它通过数据帧发送                               |
class=class="str">"cmt">//| 这个函数应当在 EA 交易的 OnTester() 处理函数中调用                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameGenerator::OnTesterEvent(const class="type">class="kw">double on_tester_value)
  {
class=class="str">"cmt">//--- 取得余额数据
   class="type">int data_count=GetBalanceData();
class=class="str">"cmt">//--- 用于把数据发送到数据桢的数组
   class="type">class="kw">double stat_data[];
   GetStatData(stat_data,on_tester_value);
class=class="str">"cmt">//--- 创建数据帧并且发送到终端
   if(!::FrameAdd(m_report_symbols,class="num">1,data_count,stat_data))
       ::Print(__FUNCTION__," > Frame add error: ",::GetLastError());
   else
       ::Print(__FUNCTION__," > Frame added, OK");
  }
class CFrameGenerator
  {
class="kw">private:
  class=class="str">"cmt">//--- 第一个不进行优化的参数
   class="type">class="kw">string            m_first_not_opt_param;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 取得优化参数的数量
   class="type">void              GetParametersTotal(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(class="type">void) : m_first_not_opt_param("Symbols")
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 取得优化参数的数量                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameGenerator::GetParametersTotal(class="type">void)
  {
class=class="str">"cmt">//--- 在第一个数据帧,确定优化参数的数量
   if(m_frames_counter<class="num">1)
     {
     class=class="str">"cmt">//--- 在构成的数据帧中取得 EA 交易的输入参数

「优化结果表格的列式装配」

在把 MT5 优化结果落表时,先得算出到底要几列。代码里用 STAT_TOTAL 加上待优化参数个数 m_param_total 得到 columns_total,再对 m_columns 做 ArrayResize;若 m_frames_counter 小于 1 才执行,意味着首帧之前只初始化一次列结构。 找不优化参数的边界靠字符串匹配:从 m_param_data 里搜 m_first_not_opt_param,命中即记 limit_index 并 break,随后 m_param_total 用 m_par_count 减去(m_par_count - limit_index)得出,也就是只保留前面参与优化的那一段。 AddRow 里有个硬过滤:若 m_data[3] 小于 1 直接 return,说明成交笔数为 0 的跑次不会进表,能省掉大量空行。之后对每一列 m_columns[i] 用 ArraySize 取原行数,再 ArrayResize 加 1 并带 RESERVE 标志,避免频繁重分配。 开 MT5 把这段塞进自己的优化报告类,改 m_first_not_opt_param 的匹配串,就能控制表格左侧留几列固定参数、右侧收几列优化维度。外汇与贵金属优化属高风险验证,回测吻合不代表实盘概率同向。

MQL5 / C++
::FrameInputs(m_pass,m_param_data,m_par_count);
class=class="str">"cmt">//--- 找到第一个不优化参数的索引
class="type">int limit_index=class="num">0;
class="type">int params_total=::ArraySize(m_param_data);
for(class="type">int i=class="num">0; i<params_total; i++)
  {
   if(::StringFind(m_param_data[i],m_first_not_opt_param)>-class="num">1)
     {
      limit_index=i;
      break;
     }
  }
class=class="str">"cmt">//--- 优化参数的数量
m_param_total=(m_par_count-(m_par_count-limit_index));
 }
}
class=class="str">"cmt">//--- 表格数组
class="kw">struct CReportTable
  {
   class="type">class="kw">string            m_rows[];
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于操作优化结果的类                                                 | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class CFrameGenerator
  {
class="kw">private:
   class=class="str">"cmt">//--- 报告表格
   CReportTable      m_columns[];
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 设置表格列数
   class="type">void              SetColumnsTotal(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置表格的列数                             					          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameGenerator::SetColumnsTotal(class="type">void)
  {
class=class="str">"cmt">//--- 确定结果表格的列数
   if(m_frames_counter<class="num">1)
     {
      class="type">int columns_total=class="type">int(STAT_TOTAL+m_param_total);
      ::ArrayResize(m_columns,columns_total);
      for(class="type">int i=class="num">0; i<columns_total; i++)
        ::ArrayFree(m_columns[i].m_rows);
     }
  }
class CFrameGenerator
  {
class="kw">private:
   class=class="str">"cmt">//--- 增加数据行
   class="type">void              AddRow(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 增加一个数据行      												          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameGenerator::AddRow(class="type">void)
  {
class=class="str">"cmt">//--- 设置表格的列数
   SetColumnsTotal();
class=class="str">"cmt">//--- 如果没有交易就退出
   if(m_data[class="num">3]<class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- 填充表格
   class="type">int columns_total=::ArraySize(m_columns);
   for(class="type">int i=class="num">0; i<columns_total; i++)
     {
      class=class="str">"cmt">//--- 增加一行
      class="type">int prev_rows_total=::ArraySize(m_columns[i].m_rows);
      ::ArrayResize(m_columns[i].m_rows,prev_rows_total+class="num">1,RESERVE);

◍ 把优化参数拆进表格列里

在回测框架里,编号 0 的列专门放遍历次数(m_pass),遇到 i==0 直接写值并 continue,不进后续统计分支。这样每帧的第一列永远是 pass 编号,方便事后按遍次对齐数据。 统计类字段(i<STAT_TOTAL)直接把 m_data[i] 转字符串落表;EA 优化参数走 else 分支,用 StringSplit 按 '=' 切 m_param_data,切出两段才取 array[1] 作为单元格值。注意这里下标偏移是 i-STAT_TOTAL,错位就会读到别的参数。 表头生成逻辑对称:GetHeaders 里同样用 StringSplit 取 array[0] 当列名,ArrayResize 长度为 STAT_TOTAL+m_param_total。若某参数串没等号,该列表头留空,但列宽仍占着。 排序列索引默认构造为 2,OnTesterInitEvent 里又显式设为 3,意味着实际优化结果按第 4 列(0 起算)排序。想换排序依据,改这一处调用即可,不用动循环体。

MQL5 / C++
if(i==class="num">0)
  {
   m_columns[i].m_rows[prev_rows_total]=class="type">class="kw">string(m_pass);
   class="kw">continue;
  }
if(i<STAT_TOTAL)
   m_columns[i].m_rows[prev_rows_total]=class="type">class="kw">string(m_data[i]);
else
  {
   class="type">class="kw">string array[];
   if(::StringSplit(m_param_data[i-STAT_TOTAL],&class="macro">#x27;=&class="macro">#x27;,array)==class="num">2)
      m_columns[i].m_rows[prev_rows_total]=array[class="num">1];
  }

class="type">void CFrameGenerator::GetHeaders(class="type">void)
  {
   class="type">int columns_total =::ArraySize(m_columns);
   ::ArrayResize(m_headers,STAT_TOTAL+m_param_total);
   for(class="type">int c=STAT_TOTAL; c<columns_total; c++)
    {
     class="type">class="kw">string array[];
     if(::StringSplit(m_param_data[c-STAT_TOTAL],&class="macro">#x27;=&class="macro">#x27;,array)==class="num">2)
        m_headers[c]=array[class="num">0];
    }
  }

CFrameGenerator::CFrameGenerator(class="type">void) : m_column_sort_index(class="num">2)
  {
  }

class="type">void CProgram::OnTesterInitEvent(class="type">void)
  {
   m_frame_gen.ColumnSortIndex(class="num">3);
  }

收尾时把优化帧重排进图表

这个函数干的活是:在遍历完所有优化帧之后,把内存里的数据重新整理、排序并刷到可视图表上。开头先调 FrameFirst() 把帧指针拨回起点,ArraysFree() 清空旧数组,m_frames_counter 归零,避免上一次回测的残留污染本次统计。 循环里每取一帧就做三件事:GetParametersTotal() 抓该帧的优化参数数量;按 m_data[m_profit_index] 的正负分别塞进 AddLoss 或 AddProfit;AddRow() 把这一行写进二维表。m_frames_counter 同步自增,最后这个数会直接喂给水平轴的 Max 和步长。 遍历结束后 GetHeaders() 拿表头,再用 QuickSort(0, rows_total-1, m_column_sort_index) 按指定列做快排——rows_total 来自 m_columns[0].m_rows 的实际大小,不是写死的。排完把盈亏两条曲线(索引 0 和 1)的名字改成带总值的字符串,并 Update 坐标序列。 水平轴 Min 锁 0、Max 用帧总数、DefaultStep 取 m_frames_counter/8.0 的整数部分,相当于把横轴切成约 8 格。最后 CalculateMaxMinValues + CurvePlotAll + Update 三步把图重绘。外汇与贵金属品种做这类批量优化时杠杆与滑点敏感,重算结果仅反映历史参数空间,实盘存在显著回撤风险,建议开 MT5 把这段直接挂到自己的 CFrameGenerator 里跑一遍确认计数与排序是否符合预期。

MQL5 / C++
class="type">void CFrameGenerator::FinalRecalculateFrames(class="type">void)
  {
class=class="str">"cmt">//--- 把帧指针移动到开始
   ::FrameFirst();
class=class="str">"cmt">//--- 重置计数器和数组
   ArraysFree();
   m_frames_counter=class="num">0;
class=class="str">"cmt">//--- 开始遍历帧
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
     class=class="str">"cmt">//--- 取得优化参数的数量
     GetParametersTotal();
     class=class="str">"cmt">//--- 负面结果
     if(m_data[m_profit_index]<class="num">0)
        AddLoss(m_data[m_profit_index]);
     class=class="str">"cmt">//--- 正面结果
     else
        AddProfit(m_data[m_profit_index]);
     class=class="str">"cmt">//--- 增加数据行
     AddRow();
     class=class="str">"cmt">//--- 增加处理帧的计数器
     m_frames_counter++;
     }
class=class="str">"cmt">//--- 取得表头
   GetHeaders();
class=class="str">"cmt">//--- 行和列的数量
   class="type">int rows_total =::ArraySize(m_columns[class="num">0].m_rows);
class=class="str">"cmt">//--- 根据指定的列来排序表格
   QuickSort(class="num">0,rows_total-class="num">1,m_column_sort_index);
class=class="str">"cmt">//--- 更新图表中的序列
   CCurve *curve=m_graph_results.CurveGetByIndex(class="num">0);
   curve.Name("P: "+(class="type">class="kw">string)ProfitsTotal());
   curve.Update(m_profit_x,m_profit_y);
class=class="str">"cmt">//---
   curve=m_graph_results.CurveGetByIndex(class="num">1);
   curve.Name("L: "+(class="type">class="kw">string)LossesTotal());
   curve.Update(m_loss_x,m_loss_y);
class=class="str">"cmt">//--- 水平轴属性
   CAxis *x_axis=m_graph_results.XAxis();
   x_axis.Min(class="num">0);
   x_axis.Max(m_frames_counter);
   x_axis.DefaultStep((class="type">int)(m_frames_counter/class="num">8.0));
class=class="str">"cmt">//--- 刷新图形
   m_graph_results.CalculateMaxMinValues();
   m_graph_results.CurvePlotAll();
   m_graph_results.Update();
   }
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多品种余额与回撤联动视图,你把精力留给参数决策而非数据搬运。

常见问题

倾向按利润排序取前 100,但可改阈值;太多行会拖慢 GUI 渲染,太少则漏掉次优但稳健的组合。
选中行触发事件,从数据帧展开对应品种余额与回撤数组,重绘独立图表,核心在 CSymbolBalance 结构回传。
表格主类指定为 Tabs 元件,尺寸随主页相对变化,在 Table 属性里声明依赖即可避免错位。
目前小布盯盘内置多品种视图与回撤诊断,优化帧需先导出;后续可能支持直连终端帧缓存做自动百优筛选。
有必要,总回撤掩盖单品种黑天鹅,分开存才能在选行时暴露某品种巨幅回撤的真实风险。