Работаем с результатами оптимизации через графический интерфейс
Содержание
- Введение
- Разработка графического интерфейса
- Сохранение результатов оптимизации
- Извлечение данных из фрейма
- Визуализация данных и взаимодействие с графическим интерфейсом
- Заключение
Введение
Продолжаем развивать тему обработки и анализа результатов оптимизации. В предыдущей статье было показано, как визуализировать результаты оптимизации через графический интерфейс 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. Последовательность расположения данных в массиве.
Для определения индексов каждого диапазона в этом массиве, как уже было сказано выше, понадобятся ключи. Количество статистических показателей постоянно и определено заранее. В данном случае будем отображать в таблице пять показателей и номер прохода, чтобы обеспечить возможность доступа к данным этого результата уже после оптимизации:
//--- Количество статистических показателей #define STAT_TOTAL 6
- Номер прохода
- Результат теста
- Прибыль (STAT_PROFIT)
- Количество трейдов (STAT_TRADES)
- Просадка (STAT_EQUITY_DDREL_PERCENT)
- Фактор восстановления (STAT_RECOVERY_FACTOR)
Количество данных баланса, общего и отдельно для каждого символа, будет одинаковым. Это значение будем отправлять в функцию 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. Схема с параметрами для расчёта индекса массива из следующей категории.
Для получения данных из фрейма реализован публичный метод CFrameGenerator::GetFrameData(). Рассмотрим его подробнее.
В начале метода указатель фреймов нужно перевести в начало списка. Затем запускается процесс перебора всех фреймов с результатами оптимизации. Нужно найти фрейм, номер прохода которого передан в метод в качестве аргумента. Если он найден, то далее программа работает по следующему алгоритму.
- Получаем размер общего массива с данными фрейма.
- Получаем элементы строки строкового параметра и их количество. Если оказывается, что символов больше одного, то количество балансов в массиве больше на один. То есть первый диапазон — это общий баланс, а остальные относятся к балансам символов.
- Далее нужно перенести данные в массивы балансов. Запускаем цикл для извлечения данных из общего массива (количество итераций равно количеству балансов). Для определения индекса, начиная с которого нужно копировать данные, достаточно сделать смещение на количество статистических показателей (STAT_TOTAL) и умножить индекс итерации ( i ) на размер массива баланса (m_value). Так на каждой итерации мы получаем в отдельные массивы данные всех балансов.
- На последней итерации получаем в отдельные массивы данные просадок. Это последние данные в массиве, поэтому нужно просто узнать оставшееся количество элементов и разделить его на 2. Далее последовательно в два шага получаем данные просадок.
- Последним действием обновляем графики новыми данными и останавливаем цикл перебора фреймов.
class CFrameGenerator { public: //--- Получает данные по указанному номеру фрейма void GetFrameData(const ulong pass_number); }; //+------------------------------------------------------------------+ //| Получает данные по указанному номеру фрейма | //+------------------------------------------------------------------+ void CFrameGenerator::GetFrameData(const ulong pass_number) { //--- Переводим указатель фреймов в начало ::FrameFirst(); //--- Извлечение данных while(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { //--- Номера проходов не совпадают, перейти к следующему if(m_pass!=pass_number) continue; //--- Размер массива с данными int data_total=::ArraySize(m_data); //--- Получим элементы строки по разделителю ushort u_sep =::StringGetCharacter(",",0); int symbols_total =::StringSplit(m_name,u_sep,m_symbols_name); int balances_total =(symbols_total>1)? symbols_total+1 : symbols_total; //--- Установим размер массиву количества балансов ::ArrayResize(m_symbols_balance,balances_total); //--- Распределим данные по массивам for(int i=0; i<balances_total; i++) { //--- Освободить массив данных ::ArrayFree(m_symbols_balance[i].m_data); //--- Определим индекс, от которого нужно копировать исходные данные int src_index=STAT_TOTAL+int(i*m_value); //--- Копируем данные в массив структуры балансов ::ArrayCopy(m_symbols_balance[i].m_data,m_data,0,src_index,(int)m_value); //--- Если это последния итерация, получим данные просадок if(i+1==balances_total) { //--- Получим количество оставшихся данных и размер для массивов по двум осям double dd_total =data_total-(src_index+(int)m_value); double array_size =dd_total/2.0; //--- Индекс, от которого начнём копирование src_index=int(data_total-dd_total); //--- Установим размер массивам просадок ::ArrayResize(m_dd_x,(int)array_size); ::ArrayResize(m_dd_y,(int)array_size); //--- Последовательно скопируем данные ::ArrayCopy(m_dd_x,m_data,0,src_index,(int)array_size); ::ArrayCopy(m_dd_y,m_data,0,src_index+(int)array_size,(int)array_size); } } //--- Обновить графики и остановить цикл UpdateMSBalanceGraph(); UpdateDrawdownGraph(); break; } }
Чтобы получить данные из ячеек массива таблицы, вызываем публичный метод CFrameGenerator::GetValue(), указав в аргументах индекс столбца и строки таблицы.
class CFrameGenerator { public: //--- Возвращает значение из указанной ячейки string GetValue(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| Возвращает значение из указанной ячейки | //+------------------------------------------------------------------+ string CFrameGenerator::GetValue(const uint column_index,const uint row_index) { //--- Проверка на выход из диапазона столбцов uint csize=::ArraySize(m_columns); if(csize<1 || column_index>=csize) return(""); //--- Проверка на выход из диапазона рядов uint rsize=::ArraySize(m_columns[column_index].m_rows); if(rsize<1 || row_index>=rsize) return(""); //--- return(m_columns[column_index].m_rows[row_index]); }
Визуализация данных и взаимодействие с графическим интефрейсом
Для обновления графиков данными балансов и просадок в класс CFrameGenerator объявлены ещё два объекта типа CGraphic. Как и в случае других объектов этого типа в классе CFrameGenerator, в них нужно передать указатели на элементы графического интерфейса в самом начале оптимизации в метод 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. Таблица результатов оптимизации в графическом интерфейсе.
Теперь рассмотрим, каким образом пользователь может увидеть мультисимвольные балансы результатов из этой таблицы. Если выделить тот или иной ряд таблицы, генерируется пользовательское событие 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. Демонстрация полученного результата.
Получился довольно удобный инструмент для быстрого просмотра мультисимвольных результатов тестов.
Заключение
Я показал еще один из возможных вариантов того, как можно работать с результатами оптимизации после ее завершения. Эта тема ещё не исчерпана, и её можно и нужно развивать дальше. С библиотекой для создания графических интерфейсов можно создать много интересных и удобных решений. Предлагайте свои идеи в комментариях к статье: возможно, в одной из следующих статей появится инструмент, необходимый именно вам для работы с результатами оптимизации.
Ниже Вы можете загрузить к себе на компьютер файлы для тестов и более подробного изучения представленного в статье кода.
Наименование файла | Комментарий |
---|---|
MacdSampleMSFrames.mq5 | Модифицированный эксперт из стандартной поставки - MACD Sample |
Program.mqh | Файл с классом программы |
CreateGUI.mqh | Файл с реализацией методов из класса программы в файле Program.mqh |
Strategy.mqh | Файл с модифицированным классом стратегии MACD Sample (мультисимвольная версия) |
FormatString.mqh | Файл со вспомогательными функциями для форматирования строк |
FrameGenerator.mqh | Файл с классом для работы с результатами оптимизации. |
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Каждый раз, когда пользователь выделяет строку в таблице, график мультисимвольных балансов обновляется на вкладке Balance:
Избавиться от 2 лишних кликов переключения на вкладку графиков и обратно, поместив графики в том же окне?
И перемещаться по строкам таблицы кнопками вверх/вниз, моментально получая соответствующие кривые?
Избавиться от 2 лишних кликов переключения на вкладку графиков и обратно, поместив графики в том же окне?
И перемещаться по строкам таблицы кнопками вверх/вниз, моментально получая соответствующие кривые?
Подобных отличных решений не хватает и в штатном Оптимизаторе.
1. Избавиться от 2 лишних кликов переключения на вкладку графиков и обратно, поместив графики в том же окне?
2. И перемещаться по строкам таблицы кнопками вверх/вниз, моментально получая соответствующие кривые?
Сейчас готовится материал для ещё одной статьи по этой теме. Первый пункт учту, а второй пока нет, так как не планировал в ближайшее время возвращаться к GUI-библиотеке.
второй пока нет, так как не планировал в ближайшее время возвращаться к GUI-библиотеке.
Это не обязательно встраивать в библиотеку, просто удобная дополнительная фича.
Это не обязательно встраивать в библиотеку, просто удобная дополнительная фича.
Я посмотрю, что можно сделать.