目录
- 简介
- 开发图形界面
- 保存优化结果
- 从数据帧中展开数据
- 使用图形界面对数据进行可视化和交互
- 结论
简介
这是处理和分析优化结果想法的续篇,前一篇文章包含了使用 MQL5 应用程序图形界面来可视化优化结果的方法描述,这一次,任务会更复杂一点: 我们将选择100个最佳的优化结果并且在图形界面的表格中显示它们。
另外,我们继续开发多交易品种余额图的想法,它也在另一篇文章中谈到了。让我们把这两篇文章的思路综合起来,使用户可以在优化结果表格中选择一行并且在独立的图表中得到多交易品种的余额和回撤图。在优化了EA交易的参数之后,交易者将可以进行结果的快速分析并且选择合适的工作参数。
开发图形界面
测试 EA 交易的 GUI 将包含下面的原件:
- 用于控件的表单
- 用于显示额外总体信息的状态栏
- 用于把元件分组的页面:
- 框架
- 用于管理在优化之后滚动结果所显示余额结果的数量
- 在滚动结果时的延迟毫秒数
- 用于开始滚动结果的按钮
- 显示余额结果数量的图形
- 全部结果的图形
- 结果
- 最佳结果的表格
- 余额
- 用于在表格中选中结果的多交易品种余额图形
- 用于在表格中选中结果的回撤图形
- 数据帧重放过程指示
用于创建上面所列出元件的方法的代码位于独立的包含文件(include 文件) 中,可以在 MQL 程序类中使用:
//+------------------------------------------------------------------+ //| 用于创建应用程序的类 | //+------------------------------------------------------------------+ class CProgram : public CWndEvents { private: //--- 窗口 CWindow m_window1; //--- 状态条 CStatusBar m_status_bar; //--- 页面 CTabs m_tabs1; //--- 文本框 CTextEdit m_curves_total; CTextEdit m_sleep_ms; //--- 按钮 CButton m_reply_frames; //--- 图表 CGraph m_graph1; CGraph m_graph2; CGraph m_graph3; CGraph m_graph4; //--- 表格 CTable m_table_param; //--- 进度条 CProgressBar m_progress_bar; //--- public: //--- 创建图形界面 bool CreateGUI(void); //--- private: //--- 表单 bool CreateWindow(const string text); //--- 状态条 bool CreateStatusBar(const int x_gap,const int y_gap); //--- 页面 bool CreateTabs1(const int x_gap,const int y_gap); //--- 文本框 bool CreateCurvesTotal(const int x_gap,const int y_gap,const string text); bool CreateSleep(const int x_gap,const int y_gap,const string text); //--- 按钮 bool CreateReplyFrames(const int x_gap,const int y_gap,const string text); //--- 图表 bool CreateGraph1(const int x_gap,const int y_gap); bool CreateGraph2(const int x_gap,const int y_gap); bool CreateGraph3(const int x_gap,const int y_gap); bool CreateGraph4(const int x_gap,const int y_gap); //--- 按钮 bool CreateUpdateGraph(const int x_gap,const int y_gap,const string text); //--- 表格 bool CreateMainTable(const int x_gap,const int y_gap); //--- 进度条 bool CreateProgressBar(const int x_gap,const int y_gap,const string text); }; //+------------------------------------------------------------------+ //| 用于创建控件的方法 | //+------------------------------------------------------------------+ #include "CreateGUI.mqh" //+------------------------------------------------------------------+
正如上面描述的,表格将显示100个最佳优化结果 (从最大结果利润角度考虑)。因为 GUI 是在优化开始之前创建的,表格最开始是空的。列的数量和抬头文字是在优化数据帧处理类中确定的,
让我们创建一个含有以下功能的表格:
- 显示表头
- 排序选项
- 可以选择一行
- 固定选中的行 (没有去除选择的功能)
- 人工调节列的宽度
- 斑马风格的格式
用于创建表格的代码显示在下面,为了使表格固定在第二个页面, 表格对象传到页面对象的时候要指定页面的索引,在这种情况下,表格的 主类是 'Tabs' 元件. 这样,如果页面区域的大小有了改变,表格的大小将也会相对它的主元件而改变,这是在 'Table' 元件的属性中指定的.
//+------------------------------------------------------------------+ //| 创建主表格 | //+------------------------------------------------------------------+ bool CProgram::CreateMainTable(const int x_gap,const int y_gap) { //--- 保存主控件的指针 m_table_param.MainPointer(m_tabs1); //--- 附加到页面 m_tabs1.AddToElementsArray(1,m_table_param); //--- 属性 m_table_param.TableSize(1,1); m_table_param.ShowHeaders(true); m_table_param.IsSortMode(true); m_table_param.SelectableRow(true); m_table_param.IsWithoutDeselect(true); m_table_param.ColumnResizeMode(true); m_table_param.IsZebraFormatRows(clrWhiteSmoke); m_table_param.AutoXResizeMode(true); m_table_param.AutoYResizeMode(true); m_table_param.AutoXResizeRightOffset(2); m_table_param.AutoYResizeBottomOffset(2); //--- 创建控件 if(!m_table_param.CreateTable(x_gap,y_gap)) return(false); //--- 把对象加到对象组的统一数组中 CWndContainer::AddToElementsArray(0,m_table_param); return(true); }
保存优化结果
CFrameGenerator 类是实现用于操作优化结果的,我们将使用来自文章 在 MetaTrader 5 中可视化交易策略的优化 中的版本,并且在其中加入所需的方法。除了在帧中保存总余额和最终统计结果外,我们还需要单独保存每个交易品种的余额和回撤。CSymbolBalance 独立数组结构将用于保存余额,该结构有两个目的,数据被保存到它的数组中,然后会在一个通用数组中传给一个数据帧,在优化之后,数据将会从数据帧数组中展开再传回给这个结构的数组中,被显示在多交易品种图形中。
//--- 用于所有交易品种余额的数组 struct CSymbolBalance { double m_data[]; }; //+------------------------------------------------------------------+ //| 用于操作优化结果的类 | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- 余额结构 CSymbolBalance m_symbols_balance[]; };
交易品种的枚举使用‘;’来分隔,将会作为字符串型参数传给数据帧。最开始,数据是以字符串数组的形式全部保存到数据桢的,但是字符串数组是不能传给数据帧的,尝试给 FrameAdd() 函数传一个字符串数组,将会在编译的时候产生错误:
字符串数组和包含对象的结构不被允许
另一个选项是把报告写到一个文件中,再把文件传给数据帧。然而,这个选项是不合适的: 我们将必须把结果特别频繁地记录到硬盘中。
所以,我决定把所有所需数据收集到一个数组中,然后根据帧参数中包含的键值来展开数据。统计变量将包含在数组的最开始,随后是总的余额,以及每个交易品种分别的独立余额,最后是两个轴上分别的回撤数据。
下面的结构图显示了数组中封装数据的顺序,只使用了两个交易品种,这样是为了使结构足够短。
图 1. 数组中数据分布的顺序.
所以,我们需要键值来确定数组中每个范围的索引,统计变量的数量是常数,会进一步判断。我们将在表格中显示5个变量和通过数,确保这个结果的数组可以在优化之后访问到:
//--- 统计参数的数量 #define STAT_TOTAL 6
- 通过数
- 测试结果
- 利润 (STAT_PROFIT)
- 交易数量 (STAT_TRADES)
- 回撤 (STAT_EQUITY_DDREL_PERCENT)
- 采收率 (STAT_RECOVERY_FACTOR)
余额数据的大小对总数据和单独的交易品种数据是相同的,这个值将以一个double 类型参数传给 FrameAdd()函数。为了确定测试中使用的交易品种,我们将在每个通过中定义它们,具体是根据交易历史在 OnTester() 函数中实现的。这个信息将会以字符串型参数传给 FrameAdd() 函数。
::FrameAdd(m_report_symbols,1,data_count,stat_data);
字符串参数中交易品种的顺序与数组中数据顺序是匹配的,就这样,有了所有这些参数,我们就可以正确展开数组中封装的数据了。
CFrameGenerator::GetHistorySymbols() 方法是用于在交易历史中确定交易品种的,代码显示如下:
#include <Trade\DealInfo.mqh> //+------------------------------------------------------------------+ //| 用于操作优化结果的类 | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- 操作交易 CDealInfo m_deal_info; //--- 报告中的交易品种 string m_report_symbols; //--- private: //--- 从账户历史中取得交易品种并返回它们的数量 int GetHistorySymbols(void); }; //+------------------------------------------------------------------+ //| 从账户历史中取得交易品种并返回它们的数量 | //+------------------------------------------------------------------+ int CFrameGenerator::GetHistorySymbols(void) { //--- 第一次在循环中遍历,取得交易品种 int deals_total=::HistoryDealsTotal(); for(int i=0; i<deals_total; i++) { //--- 取得交易编号 if(!m_deal_info.SelectByIndex(i)) continue; //--- 如果有交易品种名称 if(m_deal_info.Symbol()=="") continue; //--- 如果没有这样的字符串,就把它加上 if(::StringFind(m_report_symbols,m_deal_info.Symbol(),0)==-1) ::StringAdd(m_report_symbols,(m_report_symbols=="")? m_deal_info.Symbol() : ","+m_deal_info.Symbol()); } //--- 根据分隔符取得字符串元素 ushort u_sep=::StringGetCharacter(",",0); int symbols_total=::StringSplit(m_report_symbols,u_sep,m_symbols_name); //--- 返回交易品种数量 return(symbols_total); }
如果交易历史中包含超过一种交易品种,数组的大小就 增加1,第一个元素保留,用于总的余额。
//--- 把余额数组的大小设为交易品种数量+1,考虑到总余额 ::ArrayResize(m_symbols_balance,(m_symbols_total>1)? m_symbols_total+1 : 1);
当交易历史中所有数据都保存到独立的数组中之后,它们应当被放置到一个通用数组中,CFrameGenerator::CopyDataToMainArray() 方法就是用于此目的。在这里,我们在循环中按顺序把通用数组增加相应数据的数量,然后,在最后一次迭代的时候,我们复制回撤数据。
class CFrameGenerator { private: //--- 余额结果 double m_balances[]; //--- private: //--- 把余额数据复制到主数组中 void CopyDataToMainArray(void); }; //+------------------------------------------------------------------+ //| 把余额数据复制到主数组中 | //+------------------------------------------------------------------+ void CFrameGenerator::CopyDataToMainArray(void) { //--- 余额曲线的数量 int balances_total=::ArraySize(m_symbols_balance); //--- 余额数组的大小 int data_total=::ArraySize(m_symbols_balance[0].m_data); //--- 使用数据填充通用数组 for(int i=0; i<=balances_total; i++) { //--- 当前余额的数量 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*2)); ::ArrayCopy(m_balances,m_dd_x,array_size); ::ArrayCopy(m_balances,m_dd_y,array_size+data_total); } } }
在 CFrameGenerator::GetStatData() 方法中,统计变量的值加在通用数组的开始,数组最终会保存在数据帧中,然后通过引用传给这个方法。它的大小设为余额数据数组大小加上统计变量的数量,余额数据是从统计变量范围的最后一个索引之后放置的。
class CFrameGenerator { private: //--- 取得统计数据 void GetStatData(double &dst_array[],double on_tester_value); }; //+------------------------------------------------------------------+ //| 取得统计数据 | //+------------------------------------------------------------------+ void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value) { //--- 复制数组 ::ArrayResize(dst_array,::ArraySize(m_balances)+STAT_TOTAL); ::ArrayCopy(dst_array,m_balances,STAT_TOTAL,0); //--- 使用测试结果填充数组的第一个值 (STAT_TOTAL) dst_array[0] =0; // 总余额 dst_array[1] =on_tester_value; // 自定义优化标准的数值 dst_array[2] =::TesterStatistics(STAT_PROFIT); // 净利润 dst_array[3] =::TesterStatistics(STAT_TRADES); // 交易数量 dst_array[4] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // 最大回撤 % dst_array[5] =::TesterStatistics(STAT_RECOVERY_FACTOR); // 采收率 }
上面所述的操作是在 CFrameGenerator::OnTesterEvent() 方法中进行的, 它在主程序文件中的 OnTester() 函数中调用。
//+------------------------------------------------------------------+ //| 准备数组用于余额数值并且把它通过数据帧发送 | //| 这个函数应当在 EA 交易的 OnTester() 处理函数中调用 | //+------------------------------------------------------------------+ void CFrameGenerator::OnTesterEvent(const double on_tester_value) { //--- 取得余额数据 int data_count=GetBalanceData(); //--- 用于把数据发送到数据桢的数组 double stat_data[]; GetStatData(stat_data,on_tester_value); //--- 创建数据帧并且发送到终端 if(!::FrameAdd(m_report_symbols,1,data_count,stat_data)) ::Print(__FUNCTION__," > Frame add error: ",::GetLastError()); else ::Print(__FUNCTION__," > Frame added, OK"); }
表格数组将在优化最后填充,在 FinalRecalculateFrames() 方法中进行, 而这个函数是在 CFrameGenerator::OnTesterDeinitEvent() 方法中调用的。在此会执行以下操作: 最后重新计算优化结果,确定优化参数的数量,填充表头数组,以及收集数据填充表格数组。随后,数据会根据指定的标准来排序。
让我们讨论一些辅助方法,它们将会在最后的数据帧处理循环中调用。让我们从 CFrameGenerator::GetParametersTotal() 开始, 它确定在优化中使用的 EA 参数的数量。
调用了 FrameInputs() 方法来从数据帧中取得 EA 交易的参数。通过在这个函数中传入通过数量,我们可以得到参数的数组和它们的数量,首先会列出在优化中使用的参数,然后再指出其它参数。只有优化参数会显示在表格中,所以我们需要确定第一个没有优化的参数的索引 - 这将有助于我们删除不应该包含在表格中的组。我们可以进一步制定第一个没有优化的外部 EA 参数,这个程序将会使用它。在这种情况下,它就是 Symbols. 知道了索引,我们就可以 计算 EA 交易优化的参数数量。
class CFrameGenerator { private: //--- 第一个不进行优化的参数 string m_first_not_opt_param; //--- private: //--- 取得优化参数的数量 void GetParametersTotal(void); }; //+------------------------------------------------------------------+ //| 构造函数 | //+------------------------------------------------------------------+ CFrameGenerator::CFrameGenerator(void) : m_first_not_opt_param("Symbols") { } //+------------------------------------------------------------------+ //| 取得优化参数的数量 | //+------------------------------------------------------------------+ void CFrameGenerator::GetParametersTotal(void) { //--- 在第一个数据帧,确定优化参数的数量 if(m_frames_counter<1) { //--- 在构成的数据帧中取得 EA 交易的输入参数 ::FrameInputs(m_pass,m_param_data,m_par_count); //--- 找到第一个不优化参数的索引 int limit_index=0; int params_total=::ArraySize(m_param_data); for(int i=0; i<params_total; i++) { if(::StringFind(m_param_data[i],m_first_not_opt_param)>-1) { limit_index=i; break; } } //--- 优化参数的数量 m_param_total=(m_par_count-(m_par_count-limit_index)); } }
表格数据将会保存在 CReportTable 数组结构中。在我们得到 EA 优化参数的数量之后,我们就能确定和设置表格的列数了,这是在 CFrameGenerator::SetColumnsTotal() 方法中完成的。行数一开始等于0.
//--- 表格数组 struct CReportTable { string m_rows[]; }; //+------------------------------------------------------------------+ //| 用于操作优化结果的类 | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- 报告表格 CReportTable m_columns[]; //--- private: //--- 设置表格列数 void SetColumnsTotal(void); }; //+------------------------------------------------------------------+ //| 设置表格的列数 | //+------------------------------------------------------------------+ void CFrameGenerator::SetColumnsTotal(void) { //--- 确定结果表格的列数 if(m_frames_counter<1) { int columns_total=int(STAT_TOTAL+m_param_total); ::ArrayResize(m_columns,columns_total); for(int i=0; i<columns_total; i++) ::ArrayFree(m_columns[i].m_rows); } }
增加表格行使用的是 CFrameGenerator::AddRow() 方法,在处理数据帧的过程中,只会把有交易的结果加到表格中。表格的第一列将显示通过编号,然后是统计变量和 EA 交易的优化参数,当从数据帧取得参数时,它们的格式是 "参数N=数值N" [参数名称][分隔符][参数值]. 我们只需要参数值,将其加到表格中。所以,我们要根据分隔符‘=’来把字符串切分,并且保存数组的第二个元素。
class CFrameGenerator { private: //--- 增加数据行 void AddRow(void); }; //+------------------------------------------------------------------+ //| 增加一个数据行 | //+------------------------------------------------------------------+ void CFrameGenerator::AddRow(void) { //--- 设置表格的列数 SetColumnsTotal(); //--- 如果没有交易就退出 if(m_data[3]<1) return; //--- 填充表格 int columns_total=::ArraySize(m_columns); for(int i=0; i<columns_total; i++) { //--- 增加一行 int prev_rows_total=::ArraySize(m_columns[i].m_rows); ::ArrayResize(m_columns[i].m_rows,prev_rows_total+1,RESERVE); //--- 通过编号 if(i==0) { m_columns[i].m_rows[prev_rows_total]=string(m_pass); continue; } //--- 统计参数 if(i<STAT_TOTAL) m_columns[i].m_rows[prev_rows_total]=string(m_data[i]); //--- EA 优化参数 else { string array[]; if(::StringSplit(m_param_data[i-STAT_TOTAL],'=',array)==2) m_columns[i].m_rows[prev_rows_total]=array[1]; } } }
表头是使用特定的 CFrameGenerator::GetHeaders() 方法获得的 - 也就是在分割这一行时数组元素中的第一个:
class CFrameGenerator { private: //--- 取得表头 void GetHeaders(void); }; //+------------------------------------------------------------------+ //| 取得表头 | //+------------------------------------------------------------------+ void CFrameGenerator::GetHeaders(void) { int columns_total =::ArraySize(m_columns); //--- 表头 ::ArrayResize(m_headers,STAT_TOTAL+m_param_total); for(int c=STAT_TOTAL; c<columns_total; c++) { string array[]; if(::StringSplit(m_param_data[c-STAT_TOTAL],'=',array)==2) m_headers[c]=array[0]; } }
让我们使用简单的 CFrameGenerator::ColumnSortIndex() 方法来通知程序使用什么标准来选择100个优化结果填充表格。列的索引传给这个函数,在优化结束之后,结果表格将根据这个索引来下降排序,而前100个结果将包含在表格中并在图形界面中显示。第三列 (索引 2) 被设为默认,也就是说结果将根据最大利润来排序。
class CFrameGenerator { private: //--- 排序表格的索引 uint m_column_sort_index; //--- public: //--- 设置表格按照排序的列的索引 void ColumnSortIndex(const uint index) { m_column_sort_index=index; } }; //+------------------------------------------------------------------+ //| 构造函数 | //+------------------------------------------------------------------+ CFrameGenerator::CFrameGenerator(void) : m_column_sort_index(2) { }
如果您需要根据其他标准选择结果,就应该在 CProgram::OnTesterInitEvent() 方法中,在优化的最开始调用 CFrameGenerator::ColumnSortIndex() 方法:
//+------------------------------------------------------------------+ //| 优化过程开始事件 | //+------------------------------------------------------------------+ void CProgram::OnTesterInitEvent(void) { ... m_frame_gen.ColumnSortIndex(3); ... }
这样,CFrameGenerator::FinalRecalculateFrames() 方法就会在最后重新计算数据帧的时候根据下面的算法.
- 把数据帧指针移动到列表起点,重置数据帧和数组计数器,
- 在循环中迭代所有的数据帧以及:
- 取得优化参数的数量,
- 把正反方的结果分散到数组中,
- 在表格中增加一个数据行。
- 在帧迭代循环之后,取得表头,
- 然后 根据设置中指定的列对表格排序.
- 这个方法会在优化结果图更新前完成。
CFrameGenerator::FinalRecalculateFrames() 的代码:
class CFrameGenerator { private: //--- 在优化后最后重新计算帧数据 void FinalRecalculateFrames(void); }; //+------------------------------------------------------------------+ //| 在优化之后最后重新计算所有帧的数据 | //+------------------------------------------------------------------+ void CFrameGenerator::FinalRecalculateFrames(void) { //--- 把帧指针移动到开始 ::FrameFirst(); //--- 重置计数器和数组 ArraysFree(); m_frames_counter=0; //--- 开始遍历帧 while(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { //--- 取得优化参数的数量 GetParametersTotal(); //--- 负面结果 if(m_data[m_profit_index]<0) AddLoss(m_data[m_profit_index]); //--- 正面结果 else AddProfit(m_data[m_profit_index]); //--- 增加数据行 AddRow(); //--- 增加处理帧的计数器 m_frames_counter++; } //--- 取得表头 GetHeaders(); //--- 行和列的数量 int rows_total =::ArraySize(m_columns[0].m_rows); //--- 根据指定的列来排序表格 QuickSort(0,rows_total-1,m_column_sort_index); //--- 更新图表中的序列 CCurve *curve=m_graph_results.CurveGetByIndex(0); curve.Name("P: "+(string)ProfitsTotal()); curve.Update(m_profit_x,m_profit_y); //--- curve=m_graph_results.CurveGetByIndex(1); curve.Name("L: "+(string)LossesTotal()); curve.Update(m_loss_x,m_loss_y); //--- 水平轴属性 CAxis *x_axis=m_graph_results.XAxis(); x_axis.Min(0); x_axis.Max(m_frames_counter); x_axis.DefaultStep((int)(m_frames_counter/8.0)); //--- 刷新图形 m_graph_results.CalculateMaxMinValues(); m_graph_results.CurvePlotAll(); m_graph_results.Update(); }
下面,让我们探讨用于根据用户的请求,从数据帧中取得数据的方法。
从数据帧中展开数据
我们已经探讨了通用数组的结构以及不同种类的数据序列,现在我们需要了解数据是如何从数组中展开的。数据帧包含了余额数组的大小以及交易品种枚举作为键值,如果余额数组的大小等于回撤数组的大小,我们就能根据一个公式,在循环中确定范围中所有封装数据的索引,就像下面的框架,但是数组的大小是不同的,所以,在循环的最后一次迭代时,我们需要确定数据中的多少元素是与回撤相关的,然后除以二,因为回撤数组的大小是一样的。
图 2. 含有用于计算下一个种类数组索引的参数框架
CFrameGenerator::GetFrameData() 公有方法就是实现用于从数据帧中取得数据的,让我们详细探讨一下。
在方法的开始,我们需要把帧指针移动到列表开始。然后开始 处理所有优化结果的数据帧迭代,我们需要找到在方法的参数中传入的 通过编号所对应的数据帧,如果找到了,程序就继续根据下面的算法执行:
- 取得包含帧数据的通用数组的大小,
- 取得字符串参数行的元素以及这种元素的数量,如果有多于一个交易品种,数组中的余额数量就增加1。这样,第一个范围就是总的余额,其他范围应用于根据交易品种计算的余额。
- 随后,数据需要移动到余额数组中。我们运行一个循环来从通用数组中展开数据(迭代的数量等于余额的数量)为了确定开始复制数据的第一个索引, 我们根据统计变量的数量 (STAT_TOTAL) 做平移,并且把迭代索引 (i) 与余额数组 (m_value)的大小相乘. 这样,在每次迭代中,我们都可以把余额数据放到独立的数组中。
- 在最后一次迭代中,我们取得回车数据,放到独立的数组中。这些是数组中的最新数据,所以我们只需要找到剩余的元素数量,再除以2。随后, 在连续的两个步骤中,我们取得回撤数据.
- 最后一步是使用新数据刷新图表,并且停止帧迭代的循环。
class CFrameGenerator { public: //--- 根据指定帧编号取得数据 void GetFrameData(const ulong pass_number); }; //+------------------------------------------------------------------+ //| 根据指定的帧编号取得数据 | //+------------------------------------------------------------------+ void CFrameGenerator::GetFrameData(const ulong pass_number) { //--- 把帧指针移动到开始 ::FrameFirst(); //--- 展开数据 while(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { //--- 通过编号不匹配,转向下一个 if(m_pass!=pass_number) continue; //--- 数据数组的大小 int data_total=::ArraySize(m_data); //--- 根据分隔符取得字符串元素 ushort u_sep =::StringGetCharacter(",",0); int symbols_total =::StringSplit(m_name,u_sep,m_symbols_name); int balances_total =(symbols_total>1)? symbols_total+1 : symbols_total; //--- 设置余额数组的数量大小 ::ArrayResize(m_symbols_balance,balances_total); //--- 在数组中发送数据 for(int i=0; i<balances_total; i++) { //--- 释放数据数组 ::ArrayFree(m_symbols_balance[i].m_data); //--- 定义开始复制源数据的索引 int src_index=STAT_TOTAL+int(i*m_value); //--- 把数据复制到余额结构数组中 ::ArrayCopy(m_symbols_balance[i].m_data,m_data,0,src_index,(int)m_value); //--- 如果这是最后一次迭代,取得回撤数据 if(i+1==balances_total) { //--- 取得剩余数据数量以及在两个轴上的数组大小 double dd_total =data_total-(src_index+(int)m_value); double array_size =dd_total/2.0; //--- 开始复制的索引 src_index=int(data_total-dd_total); //--- 设置回撤数组的大小 ::ArrayResize(m_dd_x,(int)array_size); ::ArrayResize(m_dd_y,(int)array_size); //--- 按顺序复制数据 ::ArrayCopy(m_dd_x,m_data,0,src_index,(int)array_size); ::ArrayCopy(m_dd_y,m_data,0,src_index+(int)array_size,(int)array_size); } } //--- 刷新图形并停止循环 UpdateMSBalanceGraph(); UpdateDrawdownGraph(); break; } }
为了取得表格数组单元中的数据,我们要调用 CFrameGenerator::GetValue() 公有方法,需要在参数中指定表格列和行的索引。
class CFrameGenerator { public: //--- 从指定单元返回数值 string GetValue(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| 从指定单元返回数值 | //+------------------------------------------------------------------+ string CFrameGenerator::GetValue(const uint column_index,const uint row_index) { //--- 检查是否超出列的范围 uint csize=::ArraySize(m_columns); if(csize<1 || column_index>=csize) return(""); //--- 检查是否超过行的范围 uint rsize=::ArraySize(m_columns[column_index].m_rows); if(rsize<1 || row_index>=rsize) return(""); //--- return(m_columns[column_index].m_rows[row_index]); }
使用图形界面对数据进行可视化和交互
在 CFrameGenerator 类中还声明了另外两个 CGraphic类型的对象,它们是用于根据余额和回撤数据刷新图表的。和 CFrameGenerator 中相同类型的其他对象类似,我们需要向它们 传递指向 GUI 元件的指针, 这要加在 CFrameGenerator::OnTesterInitEvent() 方法中优化的最开始.
#include <Graphics\Graphic.mqh> //+------------------------------------------------------------------+ //| 用于操作优化结果的类 | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- 数据可视化的图形指针 CGraphic *m_graph_ms_balance; CGraphic *m_graph_drawdown; //--- public: //--- 策略测试器事件处理函数 void OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results,CGraphic *graph_ms_balance,CGraphic *graph_drawdown); }; //+------------------------------------------------------------------+ //| 应当在 OnTesterInit() 处理函数中调用 | //+------------------------------------------------------------------+ 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; }
图形界面表格中的数据是使用 CProgram::GetFrameDataToTable() 方法来显示的,让我们通过把表头放到数组中来判断列的数量,表头是从 CFrameGenerator 对象中取得的,随后我们在图形界面中设置表格大小 (100 行). 然后就设置表头和数据类型。
现在,我们需要使用优化结果来初始化图表,表格中的值是通过 CTable::SetValue() 来设置的,CFrameGenerator::GetValue() 方法是用来从表格单元中取得数据的。刷新表格以应用改变。
class CProgram { private: //--- 取得优化结果表格的帧数据 void GetFrameDataToTable(void); }; //+------------------------------------------------------------------+ //| 取得优化结果表格中的数据 | //+------------------------------------------------------------------+ void CProgram::GetFrameDataToTable(void) { //--- 取得表头 string headers[]; m_frame_gen.CopyHeaders(headers); //--- 设置表格大小 uint columns_total=::ArraySize(headers); m_table_param.Rebuilding(columns_total,100,true); //--- 设置表头和数据类型 for(uint c=0; c<columns_total; c++) { m_table_param.DataType(c,TYPE_DOUBLE); m_table_param.SetHeaderText(c,headers[c]); } //--- 使用帧中的数据填充表格 for(uint c=0; c<columns_total; c++) { for(uint r=0; r<m_table_param.RowsTotal(); r++) { if(c==1 || c==2 || c==4 || c==5) m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),2); else m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),0); } } //--- 刷新表格 m_table_param.Update(true); m_table_param.GetScrollHPointer().Update(true); m_table_param.GetScrollVPointer().Update(true); }
CProgram::GetFrameDataToTable() 方法是在完成了 EA 参数优化过程之后调用的,位于 OnTesterDeinit() 中。在图形界面变得更好用之后,结果优化表格页面是根据指定标准选择的,在我们的例子中,结果是根据第二列 (利润) 的值来选择的。
图 3. 图形界面中的优化结果表格。
用户可以在这个表格中看到多个交易品种的余额结果,如果您选择任何表格行,会使用表格 ID 生成 ON_CLICK_LIST_ITEM 的自定义事件。这样就可以确定是哪个表格接收到了消息 (如果有多个表格). 第一列保存了通过编号,这样我们就能通过把这个编号传到CFrameGenerator::GetFrameData() 方法中就能得到结果.
//+------------------------------------------------------------------+ //| 事件处理函数 | //+------------------------------------------------------------------+ void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- 点击表格行的事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM) { if(lparam==m_table_param.Id()) { //--- 从表格中取得通过编号 ulong pass=(ulong)m_table_param.GetValue(0,m_table_param.SelectedItem()); //--- 根据通过编号取得数据 m_frame_gen.GetFrameData(pass); } //--- return; } ... }
每次用户在表格中选择一行时,多交易品种余额图就在 余额 页面刷新。
图 4. 获得结果的演示
我们已经有了好的工具,就可以快速浏览多交易品种的测试结果了。
结论
我已经展示了一种可行方法来处理优化结果,这个主题还没有结束研究,而会进一步开发。GUI 创建库可以创建范围很广而有趣方便的解决方案。欢迎您在本文的留言部分提出您自己的想法,也许,以后的文章将会描述您所需要的优化结果处理工具。
在下面,您可以下载文件来测试和仔细学习本文提供的代码。
文件名 | 注释 |
---|---|
MacdSampleMSFrames.mq5 | 修改过的标准发布的 EA 交易 - MACD Sample |
Program.mqh | 含有程序类的文件 |
CreateGUI.mqh | 实现 Program.mqh 文件中程序类方法的文件 |
Strategy.mqh | 含有修改过的 MACD Sample 策略类 (多交易品种版本) 的文件 |
FormatString.mqh | 用于字符串格式辅助函数的文件 |
FrameGenerator.mqh | 含有用于操作优化结果类的文件 |