Визуализация результатов оптимизации по выбранному критерию

Anatoli Kazharski | 26 апреля, 2018

Содержание

Введение

Продолжаем развивать приложение для работы с результатами оптимизации, которое мы начали разрабатывать в предыдущих статьях. Разберем пример, когда таблицу лучших результатов можно сформировать уже после оптимизации параметров, указав через графический интерфейс другой критерий. В предыдущей статье я показал, как работать с множеством категорий данных: сохранять их в массив фрейма, а потом извлекать из него. Так мы работали со статистическими показателями, массивами балансов символов и с просадками депозита.

После оптимизации мы можем увидеть эти данные на отдельных графиках, просто выделяя соответствующие строки в таблице. Но нет предела совершенству. Чтобы увидеть результаты, отобранные по тому или иному критерию, хотелось бы видеть их балансы все сразу на отдельном графике. Выделение строк в таблице будет формировать выделенную кривую баланса на этом совокупном графике. Так мы сможем лучше оценить результат оптимизации. 

Также, по просьбе некоторых участников сообщества, я покажу, как с клавиатуры управлять выделением строк в таблице. Для этого пришлось доработать класс CTable в нашей библиотеке.

Разработка графического интерфейса

В нашей предыдущей версии приложения в графическом интерфейсе три вкладки: Frames, Results и Balance.

На вкладке Frames расположены элементы для работы и просмотра всех результатов в процессе оптимизации и после неё.

А вот вторую (Results) и третью (Balance) вкладки мы сейчас совместим. Теперь, выделяя строку в таблице, мы сразу увидим результат на графике, и нам не нужно будет переходить на другую вкладку.

На вкладке Results расположим еще одну группу вкладок: Balances и Favorites. На вкладке Balances будут графики для просмотра мультисимвольных балансов и просадок депозита, а также список символов, участвовавших в тесте. На вкладке Favorites расположим график всех лучших результатов из таблицы. Кроме этого, добавим элемент типа CComboBox (выпадающий список). Он поможет выбирать критерий для отбора лучших результатов из общего списка фреймов.

Полная иерархия элементов графического интерфейса теперь выглядит так:

Код методов для создания этих элементов вынесен в отдельный файл и подключается к файлу с классом 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:
   //--- Создаёт графический интерфейс
   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() должна быть отсортирована по указанному критерию. Далее, когда номер прохода найден, получаем параметры эксперта на этом проходе и их количество. После этого получаем баланс результата текущего прохода из его массива данных (m_data[]). Нужно помнить, что данные общего баланса содержатся в массиве фрейма после статистических показателей, а размер массива будет равен значению в double-параметре фрейма. Этот массив, как и серия данных, помещается на график балансов лучших результатов. Если конечный результат этого теста выше начального депозита, то линия будет зелёной, а в противном случае — красной. Размер серии сохраняем в отдельном массиве, чтобы после завершения цикла можно было определить серию с максимальным количеством элементов для установки границ оси X. И наконец, счётчик фреймов нужно увеличить на единицу, чтобы в следующий раз можно было продолжить цикл, исключая этот проход.

Если цикл пройден полностью, то далее:

После этого метод CFrameGenerator::UpdateBestResultsGraph() вернёт false, Это значит, что отбор результатов закончен.

//+------------------------------------------------------------------+
//| Класс для работы с результатами оптимизации                      |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Количество лучших результатов
   int               m_best_results_total;
   //---
public:
   //--- Обновить график лучших результатов
   bool              UpdateBestResultsGraph(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
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);
  }

Чтобы найти результаты, нужно пройтись по всем фреймам в общем списке. Это занимает время. Поэтому, чтобы отслеживать стадию поиска, используем элемент "Индикатор выполнения" (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. По умолчанию в таблицу добавляется 100 лучших результатов по критерию Profit. В любой момент из выпадающего списка 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]);
        }
     }
//--- Для расчёта координат
   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-параметр события. Этот параметр —  идентификатор элемента, из которого было сгенерировано это событие. Если идентификатор не относится к элементу, то программа выйдет из метода и проверит следующий элемент в обработчике событий элементов приложения. Если идентификатор совпадает с тем, что и у таблицы результатов, то далее получаем номер прохода из первого столбца таблицы. Поэтому, чтобы получить номер прохода, нужно просто указать индексы столбца и только что выделенного ряда, передав эти значения в метод 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(). После успешной проверки идентификатора элемента получаем индекс выбранного пункта в выпадающем списке. В этой версии приложения в выпадающем списке предлагаются три критерия:

Затем определяем индекс столбца с данными, относящимися к выбранному критерию. Первый пункт относится к столбцу с индексом 1, второй — к столбцу с индексом 2, а третий — к столбцу с индексом 5. Далее получаем фреймы с лучшими результатами по выбранному критерию. Для этого надо вызвать метод CFrameGenerator::OnChangedSelectionCriteria() передав в него индекс столбца. Теперь всё готово, чтобы получить балансы лучших результатов на график. Этот процесс визуализируется на индикаторе выполнения. Последним вызовом здесь будет получение всех данных в таблицу лучших результатов.

//+------------------------------------------------------------------+
//| Показывает результаты по указанному критерию                     |
//+------------------------------------------------------------------+
bool CProgram::ShowResultsBySelectedCriteria(const long element_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(). В него нужно передать код нажатой клавиши. Он приходит в long-параметре события CHARTEVENT_KEYDOWN. В начале метода получаем индекс текущей выделенной строки в таблице. Далее в операторе-переключателе 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);
  }

Метод CProgram::SelectingResultsUsingKeys() вызывается по приходу события нажатия клавиатуры (CHARTEVENT_KEYDOWN) в обработчике событий программы:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
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. Очевидно, что визуальная составляющая очень важна для анализа результатов тестов. Представленный глубокий и обширный взгляд на массивы этих результатов тестов может натолкнуть на новые идеи. Некоторые из них уже были предложены участниками MQL-сообщества.

Например, в массиве фреймов можно хранить полные отчёты тестов, а не только статистические показатели и данные балансов. Ещё одна обсуждавшаяся на форуме идея — использовать в отборе результатов пользовательский критерий. Например, можно использовать несколько выпадающих списков или чек-боксов для формирования пользовательского критерия, который будет рассчитываться по указанной в настройках формуле. Сложно представить, как всё это можно было бы реализовать без графического интерфейса.

Идеи, предложенные вами в комментариях к статье, могут быть реализованы в одной из следующих версий. Поэтому не стесняйтесь предлагать свои варианты того, как можно дальше развивать приложение для работы с результатами оптимизации.

Ниже вы можете загрузить к себе на компьютер файлы для тестов и более подробного изучения представленного в статье кода.

Наименование файла Комментарий
MacdSampleCFrames.mq5 Модифицированный эксперт из стандартной поставки - MACD Sample
Program.mqh Файл с классом программы
CreateGUI.mqh Файл с реализацией методов из класса программы в файле Program.mqh
Strategy.mqh Файл с модифицированным классом стратегии MACD Sample (мультисимвольная версия)
FormatString.mqh Файл со вспомогательными функциями для форматирования строк
FrameGenerator.mqh Файл с классом для работы с результатами оптимизации.