使用图形界面处理优化结果·进阶篇
(2/3)· 当优化跑出成千上万组参数,靠肉眼翻日志挑最优组合基本等于浪费时间
◍ 从成交记录里抠出交易品种清单
做多品种回测报告时,第一步是把账户里真实跑过的品种名捞出来,而不是拍脑袋写死。下面这段逻辑遍历成交索引,用 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 扩,保证绘图帧里回撤段不丢。
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 加错误码,方便你当场排查是帧符号不对还是数组长度踩线。 外汇与贵金属品种做这类优化回测波动剧烈,最大回撤可能瞬间超过历史均值,任何统计结论只代表过去样本,实盘仍属高风险。
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 的匹配串,就能控制表格左侧留几列固定参数、右侧收几列优化维度。外汇与贵金属优化属高风险验证,回测吻合不代表实盘概率同向。
::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 起算)排序。想换排序依据,改这一处调用即可,不用动循环体。
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 里跑一遍确认计数与排序是否符合预期。
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(); }