可视化使用选定标准优化的结果

Anatoli Kazharski | 3 九月, 2018

目录

简介

我们继续开发用于操作优化结果的应用程序,让我们考虑这样的情况,我们要在通过图形界面指定了其它标准而优化参数之后,生成最佳结果的表格。在前一篇文章中, 我展示了如何操作多个数据种类,我们可以把它们保存到帧数组中,然后再从数组中展开它们。通过这种方法,我们就可以操作统计参数、交易品种余额数组以及存款回撤等。

在优化之后,我们可以简单通过突出显示对应的表格行来在独立的图标中看到数据。但是完美是没有止境的,为了看到使用某个标准的结果,最好能在独立的图标中立刻看到它们的余额。在表格中突出显示行将可以在累计图中生成选中的余额曲线,这可以使我们更好地评估优化结果。 

另外,应一些论坛成员的要求,我将向您展示如何使用键盘来控制表格行的突出显示。为此,我已经在我们的开发库中加强了 CTable 类。

开发图形界面

The graphical interface (GUI) of the 我们应用程序的前一版本的图形界面(GUI)包含了三个页面: Frames(数据帧), Results(结果) Balance(余额)

Frames 页面包含了用于在优化过程中和完成后操作及查看所有结果的元件。

对于剩下的两个页面 (ResultsBalance), 我们将会合并它们。现在,我们在突出显示了表格行之后可以立刻在图中看到结果,不需要再切换到另外的页面了。

Results页面将包含另一组页面: Balances(余额) Favorites(收藏)。Balances 页面包含了多个交易品种的余额和存款回撤,以及在测试中使用的交易品种列表。Favorites 页面显示了表格中所有最佳结果的图形。另外,加上了 CComboBox 类型的元件 (下拉列表),它将会帮助您从统一的数据桢列表中选择最佳结果的标准,

完整的 GUI 元件继承图现在看起来就像这样:

用于创建元件的方法代码是在独立文件中提供的,并且和 MQL 程序类一起 包含在文件中:

//+------------------------------------------------------------------+
//| 用于创建应用程序的类                                                 |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- 窗口
   CWindow           m_window1;
   //--- 状态条
   CStatusBar        m_status_bar;
   //--- 页面
   CTabs             m_tabs1;
   CTabs             m_tabs2;
   //--- 输入栏位
   CTextEdit         m_curves_total;
   CTextEdit         m_sleep_ms;
   //--- 按钮
   CButton           m_reply_frames;
   //--- 组合框
   CComboBox         m_criterion;
   //--- 图形
   CGraph            m_graph1;
   CGraph            m_graph2;
   CGraph            m_graph3;
   CGraph            m_graph4;
   CGraph            m_graph5;
   //--- 表格
   CTable            m_table_main;
   CTable            m_table_symbols;
   //--- 进度条
   CProgressBar      m_progress_bar;
   //---
public:
   //--- 创建 GUI
   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              CreateTabs2(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              CreateCriterion(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              CreateGraph5(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              CreateSymbolsTable(const int x_gap,const int y_gap);
   //--- 进度条
   bool              CreateProgressBar(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| 用于创建控件的方法                                                  |
//+------------------------------------------------------------------+
#include "CreateGUI.mqh"
//+------------------------------------------------------------------+

选择优化结果

为了在一个图表中显示所有最佳的优化结果,我们需要一个方法,直到找到了指定数量的结果后会返回 true,一旦找到它们后,这个方法会返回 false. 这就是下面所提供的 CFrameGenerator::UpdateBestResultsGraph() 方法。默认会显示100 个最佳优化结果

方法中使用了双重循环,第一个循环是根据显示最佳结果的数量来限制的,并且表格中的行数要排除表格数据数组结构中超出范围的部分。帧的指针在每个循环迭代中都要移动到帧列表的开头。

在第二个循环中,我们在帧中迭代,来寻找我们之前保存在数据结构中通过的索引,数组结构应当是在调用 CFrameGenerator::UpdateBestResultsGraph() 方法前使用指定标准排序的。在通过索引找到之后,取得 EA 的参数以及它们在通过中的编号。在那以后,再从它的数据数组 (m_data[]) 中取得当前通过的余额结果。请记住,总的余额数据是包含在统计指标之后的帧数组中,而数组的大小等于帧的 double 参数的值。和数据序列类似,这个数组放在最佳余额图结果中。如果测试的最终结果超过了初始存款,线就是绿色的,否则就是红色的。序列的大小保存在独立的数组中,这样在循环结束之后就可以定义一系列元件的最大数量来设置 X 轴的边界。最后,应当把帧计数器增加1,这样下一次就可以继续循环而排除这次通过。

如果循环完全通过:

在那之后,CFrameGenerator::UpdateBestResultsGraph() 方法返回 false,意思是选择结束。

//+------------------------------------------------------------------+
//| 用于操作优化结果的类                                                 |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- 最佳结果的数量
   int               m_best_results_total;
   //---
public:
   //--- 更新结果图
   bool              UpdateBestResultsGraph(void);
  };
//+------------------------------------------------------------------+
//| 构造函数                                                           |
//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(void) : m_best_results_total(100)
  {
  }
//+------------------------------------------------------------------+
//| 更新最佳结果图                                                      |
//+------------------------------------------------------------------+
bool CFrameGenerator::UpdateBestResultsGraph(void)
  {
   for(int i=(int)m_frames_counter; i<m_best_results_total && i<m_rows_total; i++)
     {
      //--- 把帧指针移动到开始位置
      ::FrameFirst();
      //--- 数据的展开
      while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
        {
         //--- 通过索引不匹配,转到下一个
         if(m_pass!=(ulong)m_columns[0].m_rows[i])
            continue;
         //--- 取得参数和它们的数值
         GetParametersTotal();
         //--- 取得当前余额结果
         double serie[];
         ::ArrayCopy(serie,m_data,0,STAT_TOTAL,(int)m_value);
         //--- 发送数组,以在余额图表上显示
         CCurve *curve=m_graph_best.CurveGetByIndex(i);
         curve.Name((string)m_pass);
         curve.Color((m_data[m_profit_index]>=0)? ::ColorToARGB(clrLimeGreen) : ::ColorToARGB(clrRed));
         curve.Update(serie);
         //--- 取得序列的大小
         m_curve_max[i]=::ArraySize(serie);
         //--- 增加帧计数器
         m_frames_counter++;
         return(true);
        }
     }
//--- 重置帧计数器
   m_frames_counter=0;
//--- 使用元件最大数量定义行
   double x_max=m_curve_max[::ArrayMaximum(m_curve_max)];
//--- 水平轴属性
   CAxis *x_axis=m_graph_best.XAxis();
   x_axis.Min(0);
   x_axis.Max(x_max);
   x_axis.DefaultStep((int)(x_max/8.0));
//--- 更新图形
   m_graph_best.CalculateMaxMinValues();
   m_graph_best.CurvePlotAll();
   m_graph_best.Update();
   return(false);
  }

为了取得结果,要在总列表的所有帧中遍历,这会花费很多时间。所以,使用 "Progress bar(进度条)" 元件 (CProgressBar) 来跟踪搜索的过程。应用程序类 (CProgram) 含有 CProgram::GetBestOptimizationResults() 方法,就是做这个任务的。在此, 在 while 循环中, 会根据条件调用 CFrameGenerator::UpdateBestResultsGraph() 方法。在开始循环之前,要使进度条可见。因为帧计数器在 CFrameGenerator::UpdateBestResultsGraph() 方法中使用, 我们可以取得它的当前值。在完成了循环之后,进度条应当被隐藏。

class CProgram : public CWndEvents
  {
private:
   //--- 取得最佳优化结果
   void              GetBestOptimizationResults(void);
  };
//+------------------------------------------------------------------+
//| 取得最佳优化结果                                                    |
//+------------------------------------------------------------------+
void CProgram::GetBestOptimizationResults(void)
  {
//--- 显示进度条
   m_progress_bar.Show(); 
//--- 可视化取得最佳结果
   int best_results_total=m_frame_gen.BestResultsTotal();
   while(m_frame_gen.UpdateBestResultsGraph() && !::IsStopped())
     {
      //--- 更新进度条
      m_progress_bar.LabelText("Selection of results: "+string(m_frame_gen.CurrentFrame())+"/"+string(best_results_total));
      m_progress_bar.Update((int)m_frame_gen.CurrentFrame(),best_results_total);
     }
//--- 隐藏进度条
   m_progress_bar.Hide();
  }

在优化完成方法中应当调用 CProgram::GetBestOptimizationResults() 方法,这样,用户就会知道程序被激活了。其它的方法在前面的文章中已经探讨过了,所以这里就不再继续讨论它们了。

//+------------------------------------------------------------------+
//| 优化完成事件                                                       |
//+------------------------------------------------------------------+
void CProgram::OnTesterDeinitEvent(void)
  {
//--- 优化完成
   m_frame_gen.OnTesterDeinitEvent();
//--- 可视化接收到的最佳结果
   GetBestOptimizationResults();
//--- 使得界面可用
   IsLockedGUI(true);
//--- 计算正面和负面的结果
   CalculateProfitsAndLosses();
//--- 把数据输出到优化结果表格
   GetFrameDataToTable();
//--- 初始化 GUI 核心
   CWndEvents::InitializeCore();
  }

在优化结束或者被强制停止之后,进度条出现于状态条上,它向用户显示正在选择结果:

 图 1. 可视化结果的选择

图 1. 可视化结果的选择

为了看到所有选中的结果,应该转到 Results 页面并再选择 Favorites 页面。默认情况下,会根据Profit(利润)标准选择100个结果,加到表格中。需要按照另外的标准选择100个最佳结果的话,可以在任何时候在Criterion(标准)下拉列表中做选择。我们晚点再讨论这个,现在让我们探讨组织这个过程的方法。

 图 2. 最佳优化结果图

图 2. 最佳优化结果图

通过编程突出显示表格行

直到现在,我们都只能使用鼠标左键点击表格来突出显示表格行,但是有的时候需要使用程序这样做 — 例如,通过 Up(上), Down(下), Home(起始)End(结束) 按键来做到这样。为了使用程序选择一行,CTable 类中含有 CTable::SelectRow() 公有方法。它的代码和 CTable::RedrawRow() 私有方法类似,这个方法是用于根据鼠标点击事件以及在开启了突出显示模式时光标掠过表格时来重画表格行的,

很多代码在这两个方法中可以重用。所以,我把它放在独立的 CTable::DrawRow() 方法中。要传入的内容有:

方法本身定义了重画行的坐标,而它们的元件是按照顺序画的: 背景,网格,图片以及文字.
//+------------------------------------------------------------------+
//| 根据指定模式画出指定的表格行                                          |
//+------------------------------------------------------------------+
void CTable::DrawRow(int &indexes[],const int item_index,const int prev_item_index,const bool is_user=true)
  {
   int x1=0,x2=m_table_x_size-2;
   int y1[2]={0},y2[2]={0};
//--- 要画的行数和列数
   uint rows_total    =0;
   uint columns_total =m_columns_total-1;
//--- 如果突出显示行的方法是编程
   if(!is_user)
      rows_total=(prev_item_index!=WRONG_VALUE && item_index!=prev_item_index)? 2 : 1;
   else
      rows_total=(item_index!=WRONG_VALUE && prev_item_index!=WRONG_VALUE && item_index!=prev_item_index)? 2 : 1;
//--- 画出行的背景
   for(uint r=0; r<rows_total; r++)
     {
      //--- 计算行的顶部和底部边界坐标
      y1[r] =m_rows[indexes[r]].m_y+1;
      y2[r] =m_rows[indexes[r]].m_y2-1;
      //--- 定义相对突出显示模式的行的焦点
      bool is_item_focus=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);
      //--- 画出行的背景
      m_table.FillRectangle(x1,y1[r],x2,y2[r],RowColorCurrent(indexes[r],(is_user)? is_item_focus : false));
     }
//--- 画出边界
   for(uint r=0; r<rows_total; r++)
     {
      for(uint c=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));
     } 
//--- 画出图片
   for(uint r=0; r<rows_total; r++)
     {
      for(uint c=0; c<m_columns_total; c++)
        {
         //--- 画出图片,如果 (1) 它位于表格单元中 以及 (2) 文字在列中是左对齐的
         if(ImagesTotal(c,indexes[r])>0 && m_columns[c].m_text_align==ALIGN_LEFT)
            CTable::DrawImage(c,indexes[r]);
        }
     }
//--- 用于计算坐标For calculating the coordinates
   int x=0,y=0;
//--- 文字对齐模式
   uint text_align=0;
//--- 画出文字
   for(uint c=0; c<m_columns_total; c++)
     {
      //--- 取得文字的 (1) X 坐标 和(2) 对齐模式
      x          =TextX(c);
      text_align =TextAlign(c,TA_TOP);
      //---
      for(uint r=0; r<rows_total; r++)
        {
         //--- (1) 计算坐标 以及 (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);
        }
     }
  }

只有一个参数 — 将要突出显示行的索引 — 应当被传给 CTable::SelectRow() 方法。在此,我们首先应当检查是否超出表格范围,以及这个索引是否已经突出显示了,随后,定义当前和之前突出显示的行的索引,再把取得的值传给 CTable::DrawRow() 方法。在得到表格边界可见区域的索引之后,我们应当定义怎样 移动滚动条滑轨

//+------------------------------------------------------------------+
//| 在表格中突出显示指定的行                                              |
//+------------------------------------------------------------------+
void CTable::SelectRow(const int row_index)
  {
//--- 检查是否超出范围
   if(!CheckOutOfRange(0,(uint)row_index))
      return;
//--- 如果这一行已经突出显示了
   if(m_selected_item==row_index)
      return;
//--- 行的当前和之前的索引
   m_prev_selected_item =(m_selected_item==WRONG_VALUE)? row_index : m_selected_item;
   m_selected_item      =row_index;
//--- 某种顺序的值的数组
   int indexes[2];
//--- 如果是第一次
   if(m_prev_selected_item==WRONG_VALUE)
      indexes[0]=m_selected_item;
   else
     {
      indexes[0] =(m_selected_item>m_prev_selected_item)? m_prev_selected_item : m_selected_item;
      indexes[1] =(m_selected_item>m_prev_selected_item)? m_selected_item : m_prev_selected_item;
     }
//--- 根据设置的模式画出指定的表格行
   DrawRow(indexes,m_selected_item,m_prev_selected_item,false);
//--- 取得边界内可见区域的索引
   VisibleTableIndexes();
//--- 把滚动条移动到指定行
   if(row_index==0)
     {
      VerticalScrolling(0);
     }
   else if((uint)row_index>=m_rows_total-1)
     {
      VerticalScrolling(WRONG_VALUE);
     }
   else if(row_index<(int)m_visible_table_from_index)
     {
      VerticalScrolling(m_scrollv.CurrentPos()-1);
     }
   else if(row_index>=(int)m_visible_table_to_index-1)
     {
      VerticalScrolling(m_scrollv.CurrentPos()+1);
     }
  }

更新过的 CTable 类的版本可以在下面下载,最新版的 EasyAndFast 开发库位于代码库中。

操作帧数据的辅助方法

前一篇文章中提供的应用程序版本中, 多交易品种余额和存款回撤是在结果表格中突出显示一行的时候显示在图形中的,为了了解某条曲线属于哪个交易品种,在图表的右边会显示出曲线的名称,在当前的版本中,Y 轴方向上的图形大小是固定的,如果有多个测试的交易品种,不可能在选定的区域内把它们全部容纳进来,所以,要在图形的右侧加上 CTable 类型的列表,并且带有滚动条。列表中将包含所有余额名称。

CProgram::GetFrameSymbolsToTable() 方法是用于取得交易品种的,在得到了帧数据之后,就可以从 string 参数中取得结果中的交易品种了。在传入字符串数组后取得交易品种列表. 如果有多于一个交易品种,余额的数量就应当增加一个元素,其中第一个用来保存总的余额。

下一步,设置表格的大小。在此我们只需要一个列,而行数等于图形中曲线的数量。指定列宽并设置表格抬头的名称。在循环中用余额名称填充表格。为了知道哪个曲线关联着哪个特定名称,我们会使用颜色组件来关联它们. 更新件以显示最新的变化。

//+------------------------------------------------------------------+
//| 把帧交易品种填充到表格                                               |
//+------------------------------------------------------------------+
void CProgram::GetFrameSymbolsToTable(void)
  {
//--- 取得交易品种列表和曲线的数量
   string symbols[];
   int symbols_total  =m_frame_gen.CopySymbols(symbols);
   int balances_total =(symbols_total>1)? symbols_total+1 : symbols_total;
//--- 设置表格大小
   m_table_symbols.Rebuilding(1,balances_total,true);
//--- 列表的列宽
   int width[]={111};
   m_table_symbols.ColumnsWidth(width);
//--- 设置抬头
   m_table_symbols.SetHeaderText(0,"Balances");
//--- 使用帧中的数据填充表格
   for(uint r=0; r<m_table_symbols.RowsTotal(); r++)
     {
      uint clr=m_graph3.GetGraphicPointer().CurveGetByIndex(r).Color();
      m_table_symbols.TextColor(0,r,::ColorToARGB(clr));
      m_table_symbols.SetValue(0,r,(symbols_total>1)?(r<1)? "BALANCE" : symbols[r-1]: symbols[r],0);
     }
//--- 更新表格
   m_table_symbols.Update(true);
   m_table_symbols.GetScrollHPointer().Update(true);
   m_table_symbols.GetScrollVPointer().Update(true);
  }

如果能够在表格突出显示结果的时候也能在图形中突出显示对应的曲线,那将会非常方便。为此,我们要开发 CProgram::SelectCurve() 方法。它取得通过的索引来搜索所需的图形曲线,曲线名称对应着它们所属的通过索引。这样, 它们就可以简单通过在循环中比较通过的索引和曲线名称来侦测到了。当找到了所需的曲线后,记住它的索引并结束循环。

现在,曲线应当被移动到顶层,如果我们只是简单改变曲线的颜色来标记它,它可能会混杂在其他曲线之中,我们需要的是切换找到的曲线和最后所画出的曲线,

为此,我们要根据索引来取得这两条曲线的指针,然后复制它们的名称和数据集。然后,再交换它们。为最新的曲线设置线宽和颜色。让我们使用黑色,这样它在其他线中也很突出。更新图形来使修改起效。

//+------------------------------------------------------------------+
//| 优化结束事件                                                       |
//+------------------------------------------------------------------+
void CProgram::SelectCurve(const ulong pass)
  {
   CGraphic *graph=m_graph5.GetGraphicPointer();
//--- 根据通过索引来搜索曲线
   ulong curve_index =0;
   int curves_total  =graph.CurvesTotal();
   for(int i=0; i<curves_total; i++)
     {
      if(pass==(ulong)graph.CurveGetByIndex(i).Name())
        {
         curve_index=i;
         break;
        }
     }
//--- 图表中突出显示的和最新的曲线
   CCurve *selected_curve =graph.CurveGetByIndex((int)curve_index);
   CCurve *last_curve     =graph.CurveGetByIndex((int)curves_total-1);
//--- 复制突出显示和最新曲线的数据数组
   double y1[],y2[];
   string name1=selected_curve.Name();
   string name2=last_curve.Name();
   selected_curve.GetY(y1);
   last_curve.GetY(y2);
//---
   last_curve.Name(name1);
   selected_curve.Name(name2);
   last_curve.Update(y1);
   selected_curve.Update(y2);
//---
   last_curve.LinesWidth(2);
   last_curve.Color(clrBlack);
//--- 更新图形
   graph.CurvePlotAll();
   graph.Update();
  }

现在,当我们突出显示表格行的时候,我们也可以在图形中看到对应的余额曲线。

 图 3. 在图形中突出显示曲线

图 3. 在图形中突出显示曲线


处理与图形界面交互的事件

现在,让我们探讨当与我们的应用程序图形界面交互时生成的事件处理方法,这里是这些方法。

当点击表格行来突出显示它的时候,会生成 ON_CLICK_LIST_ITEM 自定义事件,然后会调用 CProgram::TableRowSelection() 方法来处理它。传给它事件的 long(长整型) 参数。这个参数是生成事件的元件的 ID。如果 ID 与元件无关,程序会退出此方法,并在应用程序元件事件的处理函数中检查下一个元件。如果 ID 与结果表格的 ID 匹配,我们就能从表格的第一列取得通过的索引,然后,为了取得通过编号,您只需要指定列和新选择的行的索引,只要把这些数值传给 CTable :: GetValue() 方法,

这样,我们就得到了通过的索引。现在,我们可以从帧中展开数据,它的后面是在结果中的交易品种。把它们加到第二组中第一个页面的表格中. 最后,突出显示所有结果图中的余额曲线。

//+------------------------------------------------------------------+
//| 通过鼠标左键点击突出显示表格行                                         |
//+------------------------------------------------------------------+
bool CProgram::TableRowSelection(const long element_id)
  {
//--- 突出显示表格行
   if(element_id!=m_table_main.Id())
      return(false);
//--- 从表格中取得通过索引
   ulong pass=(ulong)m_table_main.GetValue(0,m_table_main.SelectedItem());
//--- 根据通过索引取得数据
   m_frame_gen.GetFrameData(pass);
//--- 把交易品种加入表格
   GetFrameSymbolsToTable();
//--- 根据通过索引在图形中突出显示曲线
   SelectCurve(pass);
   return(true);
  }

ON_CLICK_LIST_ITEM 自定义事件一到达, 在组合框(CComboBox)的下拉列表中选择结果选择标准的事件也会处理。这是通过 CProgram::ShowResultsBySelectedCriteria() 方法来做的。在成功检查了元件的 ID 之后,在下拉列表取得选中项目的索引,在这个应用版本中,在下拉列表中有三个可用的标准。

下面,使用与选中标准相关的数据来定义列的索引。第一个项目对应索引为 1 的列, 第二个项目 — 对应列索引为 2, 第三个 — 列的索引为 5. 下面,根据选中的标准来取得最佳结果的帧数据。为此,要调用 CFrameGenerator::OnChangedSelectionCriteria() 方法,并传给它列的索引。现在,一切都已准备就绪,可以在图形中取得最佳结果的余额了。进度条是用来可视化这个过程的,这里最后调用是在最佳结果表格中读取所有数据。

//+------------------------------------------------------------------+
//| 根据指定的标准显示结果                                               |
//+------------------------------------------------------------------+
bool CProgram::ShowResultsBySelectedCriteria(const long element_id)
  {
//--- 检查元件 ID
   if(element_id!=m_criterion.Id())
      return(false);
//--- 定义取得最佳结果的标准索引
   int index=m_criterion.GetListViewPointer().SelectedItemIndex();
   int column_index=(index<1)? 1 : (index==1)? 2 : 5;
   m_frame_gen.OnChangedSelectionCriteria(column_index);
//--- 可视化接收到的最佳结果
   GetBestOptimizationResults();
//--- 把数据输出到优化结果表格
   GetFrameDataToTable();
   return(true);
  }

在程序的事件处理函数中,上面所描述的方法会在 ON_CLICK_LIST_ITEM 事件到来的时候依次调用, 直到它们其中的一个返回 true.

//+------------------------------------------------------------------+
//| 事件处理函数                                                       |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- 点击表格行的事件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      //--- 突出显示表格行
      if(TableRowSelection(lparam))
         return;
      //--- 选择结果选择的标准
      if(ShowResultsBySelectedCriteria(lparam))
         return;
      //---
      return;
     }
...
  }

下面的图片显示了下拉列表中作选择、读取数据并在图形中绘制它们:

 图 4. 根据指定标准选择结果

图 4. 根据指定标准选择结果

为了从键盘选择一行, 需要 CProgram::SelectingResultsUsingKeys() 方法,应当传给它按下按钮的代码。它位于 CHARTEVENT_KEYDOWN 事件的 'long'型参数中。在方法的开始,取得表格中当前突出显示的行的索引,然后,在 'switch' 操作符中定义按下的按钮,下面是一个处理四个按钮的例子:

现在,是时候进行检查了. 程序在下面的情况下会退出这个方法:

如果检查通过了,指定的行就应该在表格中突出显示,而如有必要还要移动滚动条。

在突出显示行之后,会进行下面的操作。

  1. 从第一个表格列中取得通过索引.
  2. 根据通过索引取得数据。
  3. 把交易品种加到多交易品种图的列表中.
  4. 最后,在全部选中结果图中突出显示余额曲线。

CProgram::SelectingResultsUsingKeys() 方法的代码:

//+------------------------------------------------------------------+
//| 使用按键选择结果                                                    |
//+------------------------------------------------------------------+
bool CProgram::SelectingResultsUsingKeys(const long key)
  {
//--- 取得突出显示行的索引
   int selected_row=m_table_main.SelectedItem();
//--- 定义方向和行数,用于移动滚动条
   switch((int)key)
     {
      case KEY_UP :
         selected_row--;
         break;
      case KEY_DOWN :
         selected_row++;
         break;
      case KEY_HOME :
         selected_row=0;
         break;
      case KEY_END :
         selected_row=(int)m_table_main.RowsTotal()-1;
         break;
     }
//--- 如果 (1) 没有突出显示的行 或者 (2) 之前选择的行已经突出显示 或者 (3) 我们超出了列表范围
   if(selected_row==WRONG_VALUE || selected_row==m_table_main.SelectedItem() || 
      selected_row<0 || selected_row>=(int)m_table_main.RowsTotal())
      return(false);
//--- 突出显示行,并移动滚动条
   m_table_main.SelectRow(selected_row);
   m_table_main.Update();
   m_table_main.GetScrollVPointer().Update(true);
//--- 从突出显示表格行中取得通过国的索引
   ulong pass=(ulong)m_table_main.GetValue(0,m_table_main.SelectedItem());
//--- 根据通过索引取得数据
   m_frame_gen.GetFrameData(pass);
//--- 把交易品种加入表格
   GetFrameSymbolsToTable();
//--- 根据通过索引在图形中突出显示曲线
   SelectCurve(pass);
   return(true);
  }

在程序的事件处理函数中,当有按键事件 (CHARTEVENT_KEYDOWN)出现的时候,就会调用 CProgram::SelectingResultsUsingKeys() 方法:

//+------------------------------------------------------------------+
//| 事件处理函数                                                       |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 按下了按钮
   if(id==CHARTEVENT_KEYDOWN)
     {
      //--- 使用按键选择结果
      if(SelectingResultsUsingKeys(lparam))
         return;
      //---
      return;
     }
...
  }

这里是工作原理:

 图 5. 根据键盘突出选择表格行

图 5. 根据键盘突出选择表格行

结论

这篇文章演示了 EasyAndFast GUI 开发库应用的另一个实例,很明显,可视化组件对测试结果的分析非常重要。对测试结果的数组进行深入和广泛的研究可能会产生新的想法,它们之中的一些已经由 MQL 社区的参与者提出来了。

例如,您可以在帧数组中保存整个测试报告,而不仅仅是统计指标或者余额数据。在论坛上讨论过的另一个想法是使用自定义的标准来选择结果。例如,您可以使用几个下拉列表或者复选框来构建自定义的标准,使用设置中指定的公式进行计算。很难想象没有 GUI 的话如何才能实现这些。

您对本文的建议可以写在留言中,可能会在后续的版本中实现,所以请不必犹豫,向我们提出建议,看如何进一步开发操作优化结果的应用程序。

在下面,您可以下载文件来测试和仔细学习本文提供的代码。

文件名 注释
MacdSampleCFrames.mq5 修改过的标准发布的 EA 交易 - MACD Sample
Program.mqh 含有程序类的文件
CreateGUI.mqh 实现 Program.mqh 文件中程序类方法的文件
Strategy.mqh 含有修改过的 MACD Sample 策略类 (多交易品种版本) 的文件
FormatString.mqh 用于字符串格式辅助函数的文件
FrameGenerator.mqh 含有用于操作优化结果类的文件