Работаем с результатами оптимизации через графический интерфейс

5 апреля 2018, 09:57
Anatoli Kazharski
7
1 933

Содержание

Введение

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

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

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

Графический интерфейс тестового эксперта будет состоять из следующих элементов.

  • Форма для элементов управления
  • Строка состояния для показа дополнительной итоговой информации
  • Вкладки для распределения элементов по группам:
    • Frames
      • Поле ввода для управления количеством отображаемых балансов результатов во время повторной прокрутки результатов после оптимизации
      • Задержка в миллисекундах во время прокрутки результатов
      • Кнопка для запуска повторной прокрутки результатов
      • График для отображения указанного количества балансов результатов
      • График для отображения всех результатов
    • Results
      • Таблица лучших результатов
    • Balance
      • График для отображения мультисимвольного баланса результата, выбранного в таблице
      • График для отображения просадок результата, выбранного в таблице
  • Индикатор выполнения процесса повторного проигрывания фреймов

Код методов для создания перечисленных в списке выше элементов вынесен в отдельный файл и подключается к файлу с классом 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 лучших результатов оптимизации (по наибольшей итоговой прибыли). Поскольку графический интерфейс создается до начала оптимизации, то изначально таблица пуста. Количество столбцов и текст для заголовков будем определять в классе обработки фреймов оптимизации.

Таблицу создадим со следующим набором функций.

  • Показ заголовков
  • Возможность сортировки
  • Выделение ряда
  • Фиксация выделенного ряда (без возможности снять выделение)
  • Изменение ширины столбца вручную
  • Формат в стиле «зебра»

Код создания таблицы показан ниже. Чтобы таблица была закреплена за второй вкладкой, нужно передать объект таблицы в объект вкладок, указав индекс вкладки. Для таблицы в таком случае главным классом будет элемент "Вкладки". Таким образом, при изменении размеров области вкладок размер таблицы будет изменяться относительно ее главного элемента, при условии, что это задано в свойствах элемента "Таблица".

//+------------------------------------------------------------------+
//| Создаёт основную таблицу                                         |
//+------------------------------------------------------------------+
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() массив строкового типа при компиляции выйдет соообщение об ошибке: строковые массивы и структуры, содержащие объекты, не допускаются.

string arrays and structures containing objects are not allowed

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

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

На схеме ниже показано, в какой последовательности будут упакованы данные в массиве. Для краткости показан вариант из двух символов.

 Рис. 1 – Последовательность расположения данных в массиве.

Рис. 1. Последовательность расположения данных в массиве.

Для определения индексов каждого диапазона в этом массиве, как уже было сказано выше, понадобятся ключи. Количество статистических показателей постоянно и определено заранее. В данном случае будем отображать в таблице пять показателей и номер прохода, чтобы обеспечить возможность доступа к данным этого результата уже после оптимизации:

//--- Количество статистических показателей
#define STAT_TOTAL 6

Количество данных баланса, общего и отдельно для каждого символа, будет одинаковым. Это значение будем отправлять в функцию FrameAdd(), как double-параметр. Чтобы определить, какие символы участвовали в тесте, будем на каждом проходе в функции 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 для общего баланса
   ::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()

//+------------------------------------------------------------------+
//| Готовит массив значений баланса и отправляет его во фрейме       |
//| Функция должна вызываться в эксперте в обработчике OnTester()    |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterEvent(const double on_tester_value)
  {
//--- Получим данные баланса
   int data_count=GetBalanceData();
//--- Массив для отправки данных во фрейм
   double stat_data[];
   GetStatData(stat_data,on_tester_value);
//--- Cоздадим фрейм с данными и отправим его в терминал
   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(), в котором определяется количество параметров эксперта, участвующих в оптимизации.

Для получения параметров эксперта из фрейма вызывается функция FrameInputs(). Передав в эту функцию номер прохода, мы получаем массив параметров и их количество. В их списке сначала идут участвовавшие в оптимизации, а затем уже все остальные. Поскольку в таблице будут показываться только оптимизируемые параметры, нужно определить индекс первого неоптимизируемого, чтобы отсечь группу, которая не должна попасть в таблицу. В нашем случае можно заранее указать первый неоптимизируемый внешний параметр эксперта, по которому будет ориентироваться программа. В данном случае это Symbols. Определив индекс, можно рассчитать количество оптимизируемых параметров эксперта.

class CFrameGenerator
  {
private:
   //--- Первый неоптимизируемый параметр
   string            m_first_not_opt_param;
   //---
private:
   //--- Получает количество оптимизируемых параметров
   void              GetParametersTotal(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(void) : m_first_not_opt_param("Symbols")
  {
  }
//+------------------------------------------------------------------+
//| Получает количество оптимизируемых параметров                    |
//+------------------------------------------------------------------+
void CFrameGenerator::GetParametersTotal(void)
  {
//--- На первом фрейме определим количество оптимизируемых параметров
   if(m_frames_counter<1)
     {
      //--- Получим входные параметры эксперта, для которых сформирован фрейм
      ::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. После того, как мы выяснили количество оптимизируемых параметров эксперта, появляется возможность определить и установить количество столбцов таблицы. Это делается в методе CFrameGenerator::SetColumnsTotal(). Изначально количество рядов равно нулю

//--- Массивы таблицы
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(). В процессе перебора всех фреймов в таблицу попадут только те результаты, в которых есть сделки. В первых столбцах таблицы, начиная с номера прохода, будут расположены статистические показатели, а затем — оптимизируемые параметры эксперта. При получении параметров из фрейма они выдаются в формате "parameterN=valueN" [название параметра][разделитель][значение параметра]. Нам нужны только значения параметров, которые должны попасть в таблицу. Поэтому расщепляем строку по разделителю ‘=’ и сохраняем значение из второго элемента массива.

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]);
      //--- Оптимизируемые параметры эксперта
      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];
     }
  }

Чтобы указать программе, по какому критерию нужно выбирать в таблицу 100 результатов оптимизации, используем простой метод CFrameGenerator::ColumnSortIndex(). В него передается индекс столбца. После окончания оптимизации таблица результатов будет отсортирована на убывание именно по этому индексу, а 100 верхних результатов попадут в таблицу для отображения в графическом интерфейсе. По умолчанию установлен третий столбец (индекс 2), то есть сортировка будет по максимальной прибыли.

class CFrameGenerator
  {
private:
   //--- Индекс отсортированного столбца
   uint              m_column_sort_index;
   //---
public:
   //--- Установка индекса столбца, по которому будет осуществляться сортировка таблицы
   void              ColumnSortIndex(const uint index) { m_column_sort_index=index; }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(void) : m_column_sort_index(2)
  {
  }

Если мы хотим отбирать результаты по другому критерию, то метод CFrameGenerator::ColumnSortIndex() нужно вызывать в методе CProgram::OnTesterInitEvent() в самом начале оптимизации:

//+------------------------------------------------------------------+
//| Событие начала процесса оптимизации                              |
//+------------------------------------------------------------------+
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 – Схема с параметрами для расчёта индекса массива из следующей категории.

Рис. 2. Схема с параметрами для расчёта индекса массива из следующей категории.

Для получения данных из фрейма реализован публичный метод CFrameGenerator::GetFrameData(). Рассмотрим его подробнее.

В начале метода указатель фреймов нужно перевести в начало списка. Затем запускается процесс перебора всех фреймов с результатами оптимизации. Нужно найти фрейм, номер прохода которого передан в метод в качестве аргумента. Если он найден, то далее программа работает по следующему алгоритму.

  • Получаем размер общего массива с данными фрейма. 
  • Получаем элементы строки строкового параметра и их количество. Если оказывается, что символов больше одного, то количество балансов в массиве больше на один. То есть первый диапазон — это общий баланс, а остальные относятся к балансам символов.
  • Далее нужно перенести данные в массивы балансов. Запускаем цикл для извлечения данных из общего массива (количество итераций равно количеству балансов). Для определения индекса, начиная с которого нужно копировать данные, достаточно сделать смещение на количество статистических показателей (STAT_TOTAL) и умножить индекс итерации ( ) на размер массива баланса (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, в них нужно передать указатели на элементы графического интерфейса в самом начале оптимизации в метод 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() вызывается по окончании процесса оптимизации параметров эксперта в методе OnTesterDeinit(). После этого графический интерфейс становится доступным для пользователя. Перейдя на вкладку Results, можно увидеть результаты оптимизации, выбранные по указанному критерию. В нашем примере выбор осуществлялся по показателю во втором столбце (Profit).

 Рис. 3 – Таблица результатов оптимизации в графическом интерфейсе.

Рис. 3. Таблица результатов оптимизации в графическом интерфейсе.

Теперь рассмотрим, каким образом пользователь может увидеть мультисимвольные балансы результатов из этой таблицы. Если выделить тот или иной ряд таблицы, генерируется пользовательское событие 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;
     }
...
  }

Каждый раз, когда пользователь выделяет строку в таблице, график мультисимвольных балансов обновляется на вкладке Balance:

 Рис. 4 – Демонстрация полученного результата.

Рис. 4. Демонстрация полученного результата.

Получился довольно удобный инструмент для быстрого просмотра мультисимвольных результатов тестов. 

Заключение

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

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

Наименование файла Комментарий
MacdSampleMSFrames.mq5 Модифицированный эксперт из стандартной поставки - MACD Sample
Program.mqh Файл с классом программы
CreateGUI.mqh Файл с реализацией методов из класса программы в файле Program.mqh
Strategy.mqh Файл с модифицированным классом стратегии MACD Sample (мультисимвольная версия)
FormatString.mqh Файл со вспомогательными функциями для форматирования строк
FrameGenerator.mqh Файл с классом для работы с результатами оптимизации.
Прикрепленные файлы |
Experts.zip (25.26 KB)
Andrey Khatimlianskii
Andrey Khatimlianskii | 8 апр 2018 в 01:25

Каждый раз, когда пользователь выделяет строку в таблице, график мультисимвольных балансов обновляется на вкладке Balance:

Избавиться от 2 лишних кликов переключения на вкладку графиков и обратно, поместив графики в том же окне?

И перемещаться по строкам таблицы кнопками вверх/вниз, моментально получая соответствующие кривые?

fxsaber
fxsaber | 8 апр 2018 в 06:20
Andrey Khatimlianskii:

Избавиться от 2 лишних кликов переключения на вкладку графиков и обратно, поместив графики в том же окне?

И перемещаться по строкам таблицы кнопками вверх/вниз, моментально получая соответствующие кривые?

Подобных отличных решений не хватает и в штатном Оптимизаторе.

Anatoli Kazharski
Anatoli Kazharski | 8 апр 2018 в 09:02
Andrey Khatimlianskii:

1. Избавиться от 2 лишних кликов переключения на вкладку графиков и обратно, поместив графики в том же окне?

2. И перемещаться по строкам таблицы кнопками вверх/вниз, моментально получая соответствующие кривые?

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

Andrey Khatimlianskii
Andrey Khatimlianskii | 8 апр 2018 в 23:09
Anatoli Kazharski:

второй пока нет, так как не планировал в ближайшее время возвращаться к GUI-библиотеке.

Это не обязательно встраивать в библиотеку, просто удобная дополнительная фича.

Anatoli Kazharski
Anatoli Kazharski | 9 апр 2018 в 09:07
Andrey Khatimlianskii:

Это не обязательно встраивать в библиотеку, просто удобная дополнительная фича.

Я посмотрю, что можно сделать. 

Как создать графическую панель любой сложности и как это работает Как создать графическую панель любой сложности и как это работает

В статье подробно рассматривается, как создать панель на базе класса CAppDialog и как добавить в нее элементы управления. Описывается структура панели и схема наследования объектов в ней. Продемонстрировано, что нужно для обработки событий и как события раздаются подчинённым элементам управления. Приведены примеры изменения параметров панели: размера, цвета фона.

Создание многомодульных советников Создание многомодульных советников

Язык программирования MQL позволяет реализовать концепцию модульного проектирования торговых стратегий. В статье показан пример создания многомодульного советника, состоящего из отдельно скомпилированных файловых модулей.

Строим индикатор ZigZag по осцилляторам. Пример выполнения технического задания Строим индикатор ZigZag по осцилляторам. Пример выполнения технического задания

В статье демонстрируется создание индикатора ZigZag в соответствии с одним из примеров заданий, описанным в статье "Как составить техническое задание при заказе индикатора". Индикатор строится по экстремумам, которые определяются с помощью осциллятора. В индикаторе предусмотрена возможность использования одного из пяти осцилляторов на выбор: WPR, CCI, Chaikin, RSI, Stochastic Oscillator.

Random Decision Forest в обучении с подкреплением Random Decision Forest в обучении с подкреплением

Random Forest (RF) с применением бэггинга — один из самых сильных методов машинного обучения, который немного уступает градиентному бустингу. В статье делается попытка разработки самообучающейся торговой системы, которая принимает решения на основании полученного опыта взаимодействия с рынком.