Визуализация стратегий в MQL5: раскладываем результаты оптимизации по графикам критериев
Содержание
- Введение
- Как это работает
- Делаем классы под свои задачи
- Элемент управления Tab Control
- Класс таблиц
- Класс Прогресс-Бара
- Класс чарта статистики
- Класс просмотровщика фреймов
- Подключаем функционал к советнику
- Заключение
Введение
Ресурс mql5.com содержит так много информации, что каждый раз, перелистывая вновь и вновь каталоги статей, либо справочную информацию, или учебник, обязательно находишь для себя что-то новое и интересное.
Так и в этот раз, попалась простенькая и на первый взгляд незамысловатая статья, где вкратце рассказано о тестере стратегий. Вроде всё просто и давно известно, но... Но последняя часть статьи заинтриговала. Там предложено просто подключить небольшой код к советнику, дописать к нему несколько стандартных обработчиков, и.., и обычный оптимизатор тестера стратегий платформы MetaTrader 5 превращается в визуальный. Хм... Интересно.
Стал изучать и разбираться. В итоге, родилась идея немного улучшить внешний вид и расширить возможности просмотра итогов оптимизации.
Сделаем так: советник будет открывать новое окно, на котором будут расположены пять вкладок. На первой будет график всех проходов, где каждый новый проход будет отображаться линией баланса. На остальных четырёх вкладках тоже будут графики, но доступны они будут по окончании оптимизации. Каждая из этих вкладок будет отображать данные о трёх лучших проходах по одному из четырёх критериев оптимизации. И на каждой вкладке будут присутствовать две таблицы: с результатами прохода оптимизации и настройками советника для этого прохода:
- Вкладка Optimization:
- таблица результатов оптимизации очередного прохода,
- таблица входных параметров советника для этого прохода,
- график баланса текущего завершённого прохода оптимизации,
- кнопка Replay для повторного воспроизведения проведённой оптимизации.
- Вкладка Sharpe Ratio:
- таблица результатов оптимизации выбранного прохода (одного из трёх лучших по Коэффициенту Шарпа),
- таблица входных параметров советника для выбранного прохода (одного из трёх лучших по Коэффициенту Шарпа),
- графики баланса трёх лучших проходов оптимизации по Коэффициенту Шарпа.
- кнопка-переключатель (трёхпозиционный) для выбора одного из трёх лучших результатов оптимизации по Коэффициенту Шарпа.
- Вкладка Net Profit:
- таблица результатов оптимизации выбранного прохода (одного из трёх лучших по Общей Прибыли),
- таблица входных параметров советника для выбранного прохода (одного из трёх лучших по Общей Прибыли),
- графики баланса трёх лучших проходов оптимизации по Общей Прибыли,
- кнопка-переключатель (трёхпозиционный) для выбора одного из трёх лучших результатов оптимизации по Общей Прибыли.
- Вкладка Profit Factor:
- таблица результатов оптимизации выбранного прохода (одного из трёх лучших по Прибыльности),
- таблица входных параметров советника для выбранного прохода (одного из трёх лучших по Прибыльности),
- графики баланса трёх лучших проходов оптимизации по Прибыльности,
- кнопка-переключатель (трёхпозиционный) для выбора одного из трёх лучших результатов оптимизации по Прибыльности.
- Вкладка Recovery Factor:
- таблица результатов оптимизации выбранного прохода (одного из трёх лучших по Фактору Восстановления),
- таблица входных параметров советника для выбранного прохода (одного из трёх лучших по Фактору Восстановления),
- графики баланса трёх лучших проходов оптимизации по Фактору Восстановления,
- кнопка-переключатель (трёхпозиционный) для выбора одного из трёх лучших результатов оптимизации по Фактору Восстановления.
Для реализации набора вкладок, сделаем классы элементов управления, из которых составим элемент управления Tab Control. Процесс создания контроллов в этой статье пропустим, просто предложив готовый файл классов. В последующих же статьях вернёмся к описанию таких классов для создания некоторых элементов управления, которые в дальнейшем могут пригодиться.
Для вывода информации о параметрах проходов, нам потребуются классы таблиц, которые возьмём в готовом виде из статьи "Возможности SQLite в MQL5: Пример интерактивной панели с торговой статистикой в разрезе символов и магиков", и немного доработаем классы таблиц для более удобного создания таблиц и вывода текста в их ячейки.
Для реализации идеи возьмём коды для работы с фреймами оптимизации, прилагаемые к упомянутой выше статье, и на их основе сделаем собственные классы, стараясь максимально сохранить концепцию. Так как в статье не описан сам процесс работы с фреймами и советником, работающем во frame-режиме, попробуем здесь разобраться в этой системе.
Как это работает
Обратимся к Учебнику по MQL5, что там указано относительно работы тестера стратегий и его оптимизатора:
... Особенно важная функция тестера — многопоточная оптимизация, которую можно выполнять с привлечением локальных и распределенных (сетевых) программ-агентов, в том числе и в MQL5 Cloud Network. Единичный прогон тестирования (с конкретными входными параметрами эксперта), запущенный пользователем вручную, или один из множества прогонов, вызванных оптимизацией (когда делается перебор значений параметров в заданных диапазонах) производится в отдельной программе — агенте. Технически — это файл metatester64.exe, и копии его процессов можно увидеть в диспетчере задач Windows во время тестирования и оптимизации. Именно за счет этого тестер является многопоточным.
Терминал является диспетчером, который раздает задачи локальным и удаленным агентам. Локальные агенты он запускает при необходимости сам. При оптимизации, по умолчанию запускается несколько агентов — их количество соответствует количеству ядер процессора. После выполнения очередного задания по тестированию советника с заданными параметрами, агент возвращает терминалу результаты.
В каждом агенте создается свое собственное торговое и программное окружение. Все агенты изолированы друг от друга и от клиентского терминала.
Что можно понять из описания: каждый экземпляр тестируемого эксперта запускается на своём агенте тестирования, и каждый проход — его итоговые данные — отправляются из агента в терминал.
Для обмена данными между терминалом и агентами существует набор обработчиков:
- OnTesterInit() — вызывается в экспертах при наступлении события TesterInit для выполнения необходимых действий перед началом оптимизации в тестере стратегий.
- OnTester() — вызывается в экспертах при наступлении события Tester для выполнения необходимых действий по окончании тестирования.
- OnTesterPass() — Вызывается в экспертах при наступлении события TesterPass для обработки нового фрейма данных во время оптимизации эксперта.
- OnTesterDeinit() — Вызывается в экспертах при наступлении события TesterDeinit для выполнения необходимых действий по окончании оптимизации эксперта.
Если в эксперте есть какой-либо из обработчиков OnTesterInit(), OnTesterDeinit() (эти два обработчика всегда работают в паре — нельзя иметь только один из них), OnTesterPass(), то эксперт будет запущен в отдельном окне терминала в особом frame-режиме:
Для управления ходом оптимизации и передачи с агентов на терминал произвольных прикладных результатов (помимо показателей торговли) в MQL5 существует 3 особых события: OnTesterInit, OnTesterDeinit, OnTesterPass. Описав для них обработчики в коде, программист получит возможность выполнять нужные ему действия перед запуском оптимизации, после завершения оптимизации и при завершении каждого из отдельных проходов оптимизации.
Все обработчики являются опциональными. Оптимизация работает и без них. Также следует понять, что все 3 события работают только в ходе оптимизации, но не одиночного теста.
Эксперт с данными обработчиками автоматически загружается на отдельном графике терминала с указанными в тестере символом и периодом. Эта копия эксперта не торгует, а выполняет исключительно сервисные действия. В ней не работают все прочие обработчики событий, в частности, OnInit, OnDeinit, OnTick.
В ходе оптимизации только один экземпляр эксперта работает в терминале и, при необходимости, принимает поступающие фреймы. Но еще раз уточним, что такой экземпляр эксперта запускается только при наличии в его коде одного из трех описываемых обработчиков событий.
После завершения каждого отдельного прохода оптимизатора, в экземпляре эксперта, работающего на агенте, генерируется событие OnTester(). Из обработчика этого события можно отправить данные о проходе в эксперт, работающий на отдельном графике в специальном frame-режиме. Пакет данных о завершённом проходе, отправляемых в эксперт на графике, называется фреймом, и в нём содержится информация о номере прохода, значениях входных переменных эксперта, с которыми был запущен проход, и результаты этого прохода.
Все эти данные поступают в эксперт, и в нём генерируется событие TesterPass, обрабатываемое в обработчике OnTesterPass(), где мы можем считать данные прохода и выполнить какие-либо действия (в данном случае, например, нарисовать график баланса этого прохода и выполнить другие сервисные действия).
Чтобы отправить данные о проходе из агента в эксперт на графике в терминале, нужно использовать функцию FrameAdd(). Текущий фрейм (завершённый проход) будет отправлен из агента в эксперт и там уже обработан в обработчике OnTesterPass().
Как видим, некоторые функции работают на агенте в запущенном в нём экземпляре эксперта, а некоторые — в советнике на графике терминала, работающего во frame-режиме. Но все они, естественно, должны быть описаны внутри кода эксперта.
В итоге, последовательность работы советника и наших действий при передаче данных между агентом и терминалом такова:
- В обработчике OnTesterInit (экземпляр советника на графике в терминале) необходимо подготовить все графические построения — отдельный график, на котором запущен эксперт во frame-режиме, и наполнение этого графика: чарт графика баланса, таблицы с параметрами и результатами, объект управления вкладками и кнопки выбора действий на вкладках;
- В обработчике OnTester (экземпляр советника на агенте) необходимо собрать всю информацию о завершённом проходе — балансовый результат каждой сделки закрытия записать в массив, получить и записать в массив полученные результаты этого прохода и отправить все эти данные в эксперт при помощи FrameAdd();
- В обработчике OnTesterPass (экземпляр советника на графике в терминале) получаем очередной фрейм, отправленный из агента при помощи FrameAdd(), считываем его данные и рисуем график баланса на чарте, создаём объект-фрейм и сохраняем его в массив для последующей сортировки и выбора по критериям оптимизации;
- В обработчиках OnTesterDeinit и OnChartEvent (экземпляр советника на графике в терминале) осуществляется работа с данными оптимизации после её завершения — повторное воспроизведение процесса оптимизации, показ лучших результатов по некоторым критериям оптимизации.
Делаем классы под свои задачи
Для создания элемента управления вкладками Tab Control был создан файл с набором элементов управления Controls.mqh. Файл прикреплён в конце статьи, и его нужно разместить прямо в папке, в которой будем писать тестовый советник, например, в каталоге терминала \MQL5\Experts\FrameViewer\Controls.mqh.
Здесь не будем рассматривать каждый созданный класс каждого элемента управления. Сделаем лишь краткий обзор.
Всего сделано было десять классов для восьми самостоятельных элементов управления:
| # | Класс | Родительский класс | Описание | Назначение |
|---|---|---|---|---|
| 1 | CBaseCanvas | CObject | Базовый класс рисования | Базовый холст. Содержит методы для установки и изменения размеров и положения, скрытия и отображения |
| 2 | CPanel | CBaseCanvas | Класс панели | Содержит методы установки и изменения цвета и обработчики событий мышки. Позволяет прикреплять дочерние элементы управления |
| 3 | CLabel | CPanel | Класс текстовой метки | Выводит текст на холст в установленных координатах |
| 4 | CButton | CLabel | Класс простой кнопки | Обычная кнопка с нефиксированным состоянием. Реагирует на наведение курсора и щелчки мышкой изменением цвета |
| 5 | CButtonTriggered | CButton | Класс двухпозиционной кнопки | Кнопка, имеющая два состояния: Вкл/Выкл. Реагирует на наведение курсора, щелчки мышкой и смену состояния изменением цвета |
| 6 | CTabButton | CButtonTriggered | Класс кнопки для вкладки | Двухпозиционная кнопка с отсутствующей рамкой в месте соединения с полем вкладки |
| 7 | CButtonSwitch | CPanel | Класс кнопки-переключателя | Панель с двумя и более двухпозиционными кнопками, где только одна может иметь состояние Вкл. Позволяет добавлять новые кнопки к уже имеющимся |
| 8 | CTabWorkArea | CObject | Класс рабочей области вкладки | Объект, имеющий в составе два базовых класса рисования — для фона и переднего плана |
| 9 | CTab | CPanel | Класс объекта вкладки | Панель, имеющая в составе кнопку и поле. На поле вкладки расположена рабочая область, где и происходит рисование |
| 10 | CTabControl | CPanel | Класс объекта управления вкладками | Панель, позволяющая добавить в свой состав объекты вкладки и управлять ими |
После успешного создания объекта управления, для каждого из объектов должен быть вызван его метод Create() с указанием его координат и размеров. После этого элемент готов к работе с ним.
Элемент управления, имеющий в своём составе реализованные обработчики событий, отправляет на график управляющей программы пользовательские события, по которым можно определить что было сделано в объекте:
| # | Класс | Событие | Идентификатор | lparam | dparam | sparam |
|---|---|---|---|---|---|---|
| 1 | CButton | Щелчок на объекте | (ushort)CHARTEVENT_CLICK | X-координата курсора | Y-координата курсора | Имя объекта кнопки |
| 2 | CButtonTriggered | Щелчок на объекте | (ushort)CHARTEVENT_CLICK | X-координата курсора | Y-координата курсора | Имя объекта кнопки |
| 3 | CTabButton | Щелчок на объекте | (ushort)CHARTEVENT_CLICK | X-координата курсора | Y-координата курсора | Имя объекта кнопки |
| 4 | CButtonSwitch | Щелчок на кнопке объекта | (ushort)CHARTEVENT_CLICK | Идентификатор кнопки | 0 | Имя объекта кнопки-переключателя |
Из таблицы видно, что для упрощения кода, из элемента управления Tab Control нет отсылки на график программы пользовательских событий. Если в программе требуется реакция на переключение вкладки, то определить событие можно по событию щелчка по кнопке вкладки TabButton. По имени кнопки можно узнать номер вкладки, либо запросить из объекта TabControl индекс выбранной вкладки, и т.п.
В любом случае, в последующем мы подробно разберём подобные классы при создании различных полезных для применения в своих программах контроллов.
Теперь нам необходимо немного доработать класс таблиц, представленный в статье, и который нужно скачать (файл Dashboard.mqh), скопировать из файла только код класса таблиц (строки 12 - 285) и сохранить скопированный код в папке \MQL5\Experts\FrameViewer\ в файле Table.mqh.
Допишем класс так, чтобы работа с таблицами и табличными данными была немного удобнее.
Подключим к файлу файл класса динамического массива указателей на экземпляры класса CObject и его наследников CArrayObj и файл класса для упрощенного создания пользовательских рисунков CCanvas:
//+------------------------------------------------------------------+ //| Table.mqh | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #include <Arrays\ArrayObj.mqh> #include <Canvas\Canvas.mqh>
В приватную секцию класса ячейки таблицы добавим новые переменные для хранения ширины, высоты и цвета текста в ячейке:
//+------------------------------------------------------------------+ //| Класс ячейки таблицы | //+------------------------------------------------------------------+ class CTableCell : public CObject { private: int m_row; // Строка int m_col; // Столбец int m_x; // Координата X int m_y; // Координата Y int m_w; // Ширина int m_h; // Высота string m_text; // Текст в ячейке color m_fore_color; // Цвет текста в ячейке public:
В публичной секции добавим методы чтения и установки новых свойств и метод, выводящий текст, записанный в ячейке, на указанный объект канваса:
public: //--- Методы установки значений void SetRow(const uint row) { this.m_row=(int)row; } void SetColumn(const uint col) { this.m_col=(int)col; } void SetX(const uint x) { this.m_x=(int)x; } void SetY(const uint y) { this.m_y=(int)y; } void SetXY(const uint x,const uint y) { this.m_x=(int)x; this.m_y=(int)y; } void SetWidth(const uint w) { this.m_w=(int)w; } void SetHeight(const uint h) { this.m_h=(int)h; } void SetSize(const uint w,const uint h) { this.m_w=(int)w; this.m_h=(int)h; } void SetText(const string text) { this.m_text=text; } //--- Методы получения значений int Row(void) const { return this.m_row; } int Column(void) const { return this.m_col; } int X(void) const { return this.m_x; } int Y(void) const { return this.m_y; } int Width(void) const { return this.m_w; } int Height(void) const { return this.m_h; } string Text(void) const { return this.m_text; } //--- Выводит текст, записанный в свойствах ячейки на канвас, указатель на который передан в метод void TextOut(CCanvas *canvas, const int x_shift, const int y_shift, const color bg_color=clrNONE, const uint flags=0, const uint alignment=0) { if(canvas==NULL) return; //--- Запомним текущие флаги шрифта uint flags_prev=canvas.FontFlagsGet(); //--- Установим цвет фона uint clr=(bg_color==clrNONE ? 0x00FFFFFF : ::ColorToARGB(bg_color)); //--- Зальём установленным цветом фона ячейку (сотрём прошлую надпись) canvas.FillRectangle(this.m_x+1, this.m_y+1, this.m_x+this.m_w-1, this.m_y+this.m_h-1, clr); //--- Установим флаги шрифта canvas.FontFlagsSet(flags); //--- Выведем текст в ячейку canvas.TextOut(this.m_x+x_shift, this.m_y+y_shift, this.m_text, ::ColorToARGB(this.m_fore_color), alignment); //--- Возвращаем ранее запомненные флаги шрифта и обновляем канвас canvas.FontFlagsSet(flags_prev); canvas.Update(false); } //--- Виртуальный метод сравнения двух объектов
В конце листинга класса напишем новый класс управления таблицами:
//+------------------------------------------------------------------+ //| Класс управления таблицами | //+------------------------------------------------------------------+ class CTableDataControl : public CTableData { protected: uchar m_alpha; color m_fore_color; //--- Преобразует RGB в color color RGBToColor(const double r,const double g,const double b) const; //--- Записывает в переменные значения компонентов RGB void ColorToRGB(const color clr,double &r,double &g,double &b); //--- Возвращает составляющую цвета (1) Red, (2) Green, (3) Blue double GetR(const color clr) { return clr&0xff ; } double GetG(const color clr) { return(clr>>8)&0xff; } double GetB(const color clr) { return(clr>>16)&0xff; } //--- Возвращает новый цвет color NewColor(color base_color, int shift_red, int shift_green, int shift_blue); public: //--- Возвращает указатель на себя CTableDataControl*Get(void) { return &this; } //--- (1) Устанавливает, (2) возвращает прозрачность void SetAlpha(const uchar alpha) { this.m_alpha=alpha; } uchar Alpha(void) const { return this.m_alpha; } //--- Рисует (1) фоновую сетку, (2) с автоматическим размером ячеек void DrawGrid(CCanvas *canvas,const int x,const int y,const uint header_h,const uint rows,const uint columns,const uint row_size,const uint col_size, const color line_color=clrNONE,bool alternating_color=true); void DrawGridAutoFill(CCanvas *canvas,const uint border,const uint header_h,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true); //--- Выводит (1) текстовое сообщение, (2) закрашенный прямоугольник в указанные координаты void DrawText(CCanvas *canvas,const string text,const int x,const int y,const color clr=clrNONE,const uint align=0,const int width=WRONG_VALUE,const int height=WRONG_VALUE); void DrawRectangleFill(CCanvas *canvas,const int x,const int y,const int width,const int height,const color clr,const uchar alpha); //--- Конструкторы/Деструктор CTableDataControl (const uint id) : CTableData(id), m_fore_color(clrDimGray), m_alpha(255) {} CTableDataControl (void) : m_alpha(255) {} ~CTableDataControl (void) {} }; //+------------------------------------------------------------------+ //| Рисует фоновую сетку | //+------------------------------------------------------------------+ void CTableDataControl::DrawGrid(CCanvas *canvas,const int x,const int y,const uint header_h,const uint rows,const uint columns,const uint row_size,const uint col_size, const color line_color=clrNONE,bool alternating_color=true) { //--- Очищаем все списки объекта табличных данных (удаляем ячейки из строк и все строки) this.Clear(); //--- Высота строки не может быть меньше 2 int row_h=int(row_size<2 ? 2 : row_size); //--- Ширина столбца не может быть меньше 2 int col_w=int(col_size<2 ? 2 : col_size); //--- Левая координата (X1) таблицы int x1=x; //--- Рассчитываем координату X2 (справа) в зависимости от количества столбцов и их ширины int x2=x1+col_w*int(columns>0 ? columns : 1); //--- Координата Y1 находится под областью заголовка панели int y1=(int)header_h+y; //--- Рассчитываем координату Y2 (снизу) в зависимости от количества строк и их высоты int y2=y1+row_h*int(rows>0 ? rows : 1); //--- Устанавливаем координаты таблицы this.SetCoords(x1,y1-header_h,x2,y2-header_h); //--- Получаем цвет линий сетки таблицы, либо по умолчанию, либо переданный в метод color clr=(line_color==clrNONE ? C'200,200,200' : line_color); //--- Рисуем рамку таблицы canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha)); //--- В цикле по строкам таблицы for(int i=0;i<(int)rows;i++) { //--- рассчитываем координату Y очередной горизонтальной линии сетки (координата Y очередной строки таблицы) int row_y=y1+row_h*i; //--- если передан флаг "чередующихся" цветов строк и строка чётная if(alternating_color && i%2==0) { //--- осветляем цвет фона таблицы и рисуем фоновый прямоугольник color new_color=this.NewColor(clr,45,45,45); canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha)); } //--- Рисуем горизонтальную линию сетки таблицы canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha)); //--- Создаём новый объект строки таблицы CTableRow *row_obj=new CTableRow(i); if(row_obj==NULL) { ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i); continue; } //--- Добавляем его в список строк объекта табличных данных //--- (если добавить объект не удалось - удаляем созданный объект) if(!this.AddRow(row_obj)) delete row_obj; //--- Устанавливаем в созданном объекте-строке его координату Y с учётом смещения от заголовка панели row_obj.SetY(row_y-header_h); } //--- В цикле по столбцам таблицы for(int i=0;i<(int)columns;i++) { //--- рассчитываем координату X очередной вертикальной линии сетки (координата X очередного столбца таблицы) int col_x=x1+col_w*i; //--- Если линия сетки вышла за пределы панели - прерываем цикл if(x1==1 && col_x>=x1+canvas.Width()-2) break; //--- Рисуем вертикальную линию сетки таблицы canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha)); //--- Получаем из объекта табличных данных количество созданных строк int total=this.RowsTotal(); //--- В цикле по строкам таблицы for(int j=0;j<total;j++) { //--- получаем очередную строку CTableRow *row=this.GetRow(j); if(row==NULL) continue; //--- Создаём новую ячейку таблицы CTableCell *cell=new CTableCell(row.Row(),i); if(cell==NULL) { ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i); continue; } //--- Добавляем созданную ячейку в строку //--- (если добавить объект не удалось - удаляем созданный объект) if(!row.AddCell(cell)) { delete cell; continue; } //--- Устанавливаем в созданном объекте-ячейке его координату X и координату Y из объекта-строки cell.SetXY(col_x,row.Y()); cell.SetSize(col_w, row_h); } } //--- Обновляем канвас без перерисовки графика canvas.Update(false); } //+------------------------------------------------------------------+ //| Рисует фоновую сетку с автоматическим размером ячеек | //+------------------------------------------------------------------+ void CTableDataControl::DrawGridAutoFill(CCanvas *canvas,const uint border,const uint header_h,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true) { //--- Координата X1 (левая) таблицы int x1=(int)border; //--- Координата X2 (правая) таблицы int x2=canvas.Width()-(int)border-1; //--- Координата Y1 (верхняя) таблицы int y1=int(header_h+border-1); //--- Координата Y2 (нижняя) таблицы int y2=canvas.Height()-(int)border-1; //--- Устанавливаем координаты таблицы this.SetCoords(x1,y1,x2,y2); //--- Получаем цвет линий сетки таблицы, либо по умолчанию, либо переданный в метод color clr=(line_color==clrNONE ? C'200,200,200' : line_color); //--- Если отступ от края панели больше нуля - рисуем рамку таблицы //--- иначе - рамкой таблицы выступает рамка панели if(border>0) canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha)); //--- Высота всей сетки таблицы int greed_h=y2-y1; //--- Рассчитываем высоту строки в зависимости от высоты таблицы и количества строк int row_h=(int)::round((double)greed_h/(double)rows); //--- В цикле по количеству строк for(int i=0;i<(int)rows;i++) { //--- рассчитываем координату Y очередной горизонтальной линии сетки (координата Y очередной строки таблицы) int row_y=y1+row_h*i; //--- если передан флаг "чередующихся" цветов строк и строка чётная if(alternating_color && i%2==0) { //--- осветляем цвет фона таблицы и рисуем фоновый прямоугольник color new_color=this.NewColor(clr,45,45,45); canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha)); } //--- Рисуем горизонтальную линию сетки таблицы canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha)); //--- Создаём новый объект строки таблицы CTableRow *row_obj=new CTableRow(i); if(row_obj==NULL) { ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i); continue; } //--- Добавляем его в список строк объекта табличных данных //--- (если добавить объект не удалось - удаляем созданный объект) if(!this.AddRow(row_obj)) delete row_obj; //--- Устанавливаем в созданном объекте-строке его координату Y с учётом смещения от заголовка панели row_obj.SetY(row_y-header_h); } //--- Ширина сетки таблицы int greed_w=x2-x1; //--- Рассчитываем ширину столбца в зависимости от ширины таблицы и количества столбцов int col_w=(int)::round((double)greed_w/(double)columns); //--- В цикле по столбцам таблицы for(int i=0;i<(int)columns;i++) { //--- рассчитываем координату X очередной вертикальной линии сетки (координата X очередного столбца таблицы) int col_x=x1+col_w*i; //--- Если это не самая первая вертикальная линия - рисуем её //--- (первой вертикальной линией выступает либо рамка таблицы, либо рамка панели) if(i>0) canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha)); //--- Получаем из объекта табличных данных количество созданных строк int total=this.RowsTotal(); //--- В цикле по строкам таблицы for(int j=0;j<total;j++) { //--- получаем очередную строку CTableRow *row=this.GetRow(j); if(row==NULL) continue; //--- Создаём новую ячейку таблицы CTableCell *cell=new CTableCell(row.Row(),i); if(cell==NULL) { ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i); continue; } //--- Добавляем созданную ячейку в строку //--- (если добавить объект не удалось - удаляем созданный объект) if(!row.AddCell(cell)) { delete cell; continue; } //--- Устанавливаем в созданном объекте-ячейке его координату X и координату Y из объекта-строки cell.SetXY(col_x,row.Y()); cell.SetSize(col_w, row_h); } } //--- Обновляем канвас без перерисовки графика canvas.Update(false); } //+------------------------------------------------------------------+ //| Возвращает цвет с новой цветовой составляющей | //+------------------------------------------------------------------+ color CTableDataControl::NewColor(color base_color, int shift_red, int shift_green, int shift_blue) { double clR=0, clG=0, clB=0; this.ColorToRGB(base_color,clR,clG,clB); double clRn=(clR+shift_red < 0 ? 0 : clR+shift_red > 255 ? 255 : clR+shift_red); double clGn=(clG+shift_green< 0 ? 0 : clG+shift_green> 255 ? 255 : clG+shift_green); double clBn=(clB+shift_blue < 0 ? 0 : clB+shift_blue > 255 ? 255 : clB+shift_blue); return this.RGBToColor(clRn,clGn,clBn); } //+------------------------------------------------------------------+ //| Преобразует RGB в color | //+------------------------------------------------------------------+ color CTableDataControl::RGBToColor(const double r,const double g,const double b) const { int int_r=(int)::round(r); int int_g=(int)::round(g); int int_b=(int)::round(b); int clr=0; clr=int_b; clr<<=8; clr|=int_g; clr<<=8; clr|=int_r; //--- return (color)clr; } //+------------------------------------------------------------------+ //| Получение значений компонентов RGB | //+------------------------------------------------------------------+ void CTableDataControl::ColorToRGB(const color clr,double &r,double &g,double &b) { r=GetR(clr); g=GetG(clr); b=GetB(clr); } //+------------------------------------------------------------------+ //| Выводит текстовое сообщение в указанные координаты | //+------------------------------------------------------------------+ void CTableDataControl::DrawText(CCanvas *canvas,const string text,const int x,const int y,const color clr=clrNONE,const uint align=0,const int width=WRONG_VALUE,const int height=WRONG_VALUE) { //--- Объявим переменные для записи в них ширины и высоты текста int w=width; int h=height; //--- Если ширина и высота текста, переданные в метод, имеют нулевые значения, //--- то полностью очищается всё пространство канваса прозрачным цветом if(width==0 && height==0) canvas.Erase(0x00FFFFFF); //--- Иначе else { //--- Если переданные ширина и высота имеют значения по умолчанию (-1) - получаем из текста его ширину и высоту if(width==WRONG_VALUE && height==WRONG_VALUE) canvas.TextSize(text,w,h); //--- иначе, else { //--- если ширина, переданная в метод, имеет значение по умолчанию (-1) - получаем ширину из текста, либо //--- если ширина, переданная в метод, имеет значение больше нуля - используем переданную в метод ширину, либо //--- если ширина, переданная в метод, имеет нулевое значение, используем значение 1 для ширины w=(width ==WRONG_VALUE ? canvas.TextWidth(text) : width>0 ? width : 1); //--- если высота, переданная в метод, имеет значение по умолчанию (-1) - получаем высоту из текста, либо //--- если высота, переданная в метод, имеет значение больше нуля - используем переданную в метод высоту, либо //--- если высота, переданная в метод, имеет нулевое значение, используем значение 1 для высоты h=(height==WRONG_VALUE ? canvas.TextHeight(text) : height>0 ? height : 1); } //--- Заполняем пространство по указанным координатам и полученной шириной и высотой прозрачным цветом (стираем прошлую запись) canvas.FillRectangle(x,y,x+w,y+h,0x00FFFFFF); } //--- Выводим текст на очищенное от прошлого текста место и обновляем рабочую область без перерисовки экрана canvas.TextOut(x,y,text,::ColorToARGB(clr==clrNONE ? this.m_fore_color : clr),align); canvas.Update(false); } //+------------------------------------------------------------------+ //| Выводит закрашенный прямоугольник в указанные координаты | //+------------------------------------------------------------------+ void CTableDataControl::DrawRectangleFill(CCanvas *canvas,const int x,const int y,const int width,const int height,const color clr,const uchar alpha) { canvas.FillRectangle(x,y,x+width,y+height,::ColorToARGB(clr,alpha)); canvas.Update(); } //+------------------------------------------------------------------+
В этом классе расположены методы, принцип которых был описан в статье "Делаем информационную панель для отображения данных в индикаторах и советниках" в разделе описания информационной панели. В указанной статье методы принадлежали объекту панели. Здесь же они вынесены в отдельный класс, унаследованный от класса таблицы.
Все объекты табличных данных здесь будут иметь тип класса CTableDataControl — объекта управления таблицами, что даст возможность оперативно управлять таблицами.
Давайте посмотрим, что в той давней статье нам предлагалось загрузить и подключить к советнику:
И последний "гвоздь" нашей программы — работа с результатами оптимизации! Если раньше трейдеру для обработки результатов необходимо было готовить данные, куда-то выгружать и обрабатывать их на стороне, то теперь это можно сделать "не отходя от кассы" — во время самой оптимизации. Для демонстрации такой возможности нам понадобятся несколько подключаемых файлов, в которых реализованы простейшие примеры такой обработки.
Заливаем прикрепленные к статье файлы с расширением MQH в папку MQL5\Include. Берем любого эксперта и вставляем в конце вот такой блок:
//--- подключим код для работы с результатами оптимизации #include <FrameGenerator.mqh> //--- генератор фреймов CFrameGenerator fg; //+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester() { //--- тут нужно вставить свою функцию для вычисления критерия оптимизации double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT)); TesterCritetia=TesterStatistics(STAT_PROFIT)>0?TesterCritetia:(-TesterCritetia); //--- вызываем на каждом окончании тестирования и передаем в качестве параметра критерий оптимизации fg.OnTester(TesterCritetia); //--- return(TesterCritetia); } //+------------------------------------------------------------------+ //| TesterInit function | //+------------------------------------------------------------------+ void OnTesterInit() { //--- подготавливаем график для отображения графиков баланса fg.OnTesterInit(3); //параметр задает количество линий баланса на графике } //+------------------------------------------------------------------+ //| TesterPass function | //+------------------------------------------------------------------+ void OnTesterPass() { //--- обрабатываем полученные результаты тестирования и выводим графику fg.OnTesterPass(); } //+------------------------------------------------------------------+ //| TesterDeinit function | //+------------------------------------------------------------------+ void OnTesterDeinit() { //--- завершение оптимизации fg.OnTesterDeinit(); } //+------------------------------------------------------------------+ //| Обработка событий на графике | //+------------------------------------------------------------------+ void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- запускает воспроизведение фреймов по окончании оптимизации при нажатии на шапке fg.OnChartEvent(id,lparam,dparam,sparam,100); // 100 - это пауза в ms между кадрами } //+------------------------------------------------------------------+
Для примера был взят идущий в стандартной поставке советник Moving Averages.mq5. Вставляем код и сохраняем советник с именем Moving Averages With Frames.mq5. Компилируем и запускаем оптимизацию.
Идём в конец статьи и смотрим прикреплённые файлы. Там есть четыре файла с расширением *.mqh. Заливаем их себе и разбираемся:
- specialchart.mqh (7.61 KB) — класс специального чарта, на котором рисуются линии баланса каждого прохода тестера и линии баланса при воспроизведении процесса завершённой оптимизации;
- colorprogressbar.mqh (4.86 KB) — класс прогресс-бара, который отображает процесс оптимизации, заполняясь цветными столбцами при прохождении оптимизации. Зелёный цвет — для прибыльной серии, красный — для убыточной, располагается внизу специального чарта;
- simpletable.mqh (10.74 KB) — класс простой таблицы, на которой отображаются данные каждого прохода оптимизации — полученный результат и значения настроечных параметров советника, с которыми на этом проходе был запущен эксперт. Две таблицы располагаются слева от графиков специального чарта;
- framegenerator.mqh (14.88 KB) — класс для обмена данными между агентом тестирования и терминалом и вывода информации на специальный чарт. Является основным классом для реализации визуальной оптимизации.
Исходя из полученных знаний, решаем сделать (1) класс прогресс-бара, (2) класс специального чарта и (3) класс просмотровщика фреймов. Класс таблиц (4) у нас уже есть, загружен в папку будущего советника и немного доработан.
Нам потребуется сделать ещё один небольшой класс — класс фрейма (5). Зачем? Мы будем выбирать и отображать графики трёх лучших проходов для каждого из четырёх критериев оптимизации — Коэффициент Шарпа, Общая прибыль, Прибыльность и Фактор восстановления. Сделать это будет удобно, если у нас есть список объектов, созданный на основе класса динамического массива указателей на экземпляры класса CObject и его наследников Стандартной Библиотеки. Достаточно отсортировать список по нужному критерию, и все объекты списка будут отсортированы по значению свойства выбранного критерия. Объект с максимальным значением параметра будет находиться в конце списка. Останется найти два, у которых значение свойства меньше, чем у предыдущего найденного объекта. И методы для такого поиска уже все реализованы в упомянутом классе.
Класс прогресс-бара, класс специального чарта и класс просмотровщика фреймов создаются на основе кодов, загруженных из статьи — просто смотрим, как сделано там, и на этой базе делаем своё, корректируя, убирая ненужное и добавляя что-то необходимое. Давайте посмотрим на получившиеся коды, и при желании их можно будет сравнить со взятыми из старой статьи — архив со старыми файлами будет прикреплён в конце статьи.
Все классы будем писать в одном файле. Создадим его (если ещё не создавали) в папке \MQL5\Experts\FrameViewer\FrameViewer.mqh и начнём наполнять.
Подключим к созданному файлу файлы требуемых классов и библиотек и определим некоторые макроподстановки:
//+------------------------------------------------------------------+ //| FrameViewer.mqh | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #include "Controls.mqh" // Классы контроллов #include "Table.mqh" // Класс таблиц #include <Arrays\ArrayDouble.mqh> // Массив вещественных данных #define CELL_W 128 // Ширина ячеек таблицы #define CELL_H 19 // Высота ячейки таблицы #define BUTT_RES_W CELL_W+30 // Ширина кнопки выбора результата оптимизации #define DATA_COUNT 8 // Количество данных #define FRAME_ID 1 // Идентификатор фреймов #define TABLE_OPT_STAT_ID 1 // Идентификатор таблицы статистики на вкладке оптимизации #define TABLE_OPT_INP_ID 2 // Идентификатор таблицы входных параметров на вкладке оптимизации
Почти у каждого графического объекта, на котором будет выполняться рисование, есть несколько объектов типа CCanvas. Один может служить подложкой, на которой располагаются ещё два: на первом рисуется фоновое изображение, а на втором — то, что должно быть нарисовано поверх фона. Для объектов, чьи методы предназначены для рисования, в эти методы передаётся указатель на нужный объект канваса, на котором метод и будет рисовать.
Так как кода классов достаточно много, и каждый класс и его методы вполне подробно прокомментированы, то не будем здесь расписывать всё подробно и пошагово. Просто посмотрим коды классов и методов, для которых сделаем краткий обзор по представленному коду.
Итак, класс прогресс-бара:
//+------------------------------------------------------------------+ //| Класс прогресс-бара, рисующий двумя цветами | //+------------------------------------------------------------------+ class CColorProgressBar :public CObject { private: CCanvas *m_background; // Указатель на объект класса CCanvas для рисования на фоне CCanvas *m_foreground; // Указатель на объект класса CCanvas для рисования на переднем плане CRect m_bound; // Координаты и размеры рабочей области color m_good_color, m_bad_color; // Цвета прибыльной и убыточной серий color m_back_color, m_fore_color; // Цвета фона и рамки bool m_passes[]; // Количество обработанных проходов int m_last_index; // Номер последнего прохода public: //--- Конструктор/деструктор CColorProgressBar(void); ~CColorProgressBar(void){}; //--- Устанавливает указатель на канвас void SetCanvas(CCanvas *background, CCanvas *foreground) { if(background==NULL) { ::Print(__FUNCTION__, ": Error. Background is NULL"); return; } if(foreground==NULL) { ::Print(__FUNCTION__, ": Error. Foreground is NULL"); return; } this.m_background=background; this.m_foreground=foreground; } //--- Устанавливает координаты и размеры рабочей области на канвасе void SetBound(const int x1, const int y1, const int x2, const int y2) { this.m_bound.SetBound(x1, y1, x2, y2); } //--- Возврат координат границ прямоугольной области int X1(void) const { return this.m_bound.left; } int Y1(void) const { return this.m_bound.top; } int X2(void) const { return this.m_bound.right; } int Y2(void) const { return this.m_bound.bottom; } //--- Установка цвета фона и рамки void SetBackColor(const color clr) { this.m_back_color=clr; } void SetForeColor(const color clr) { this.m_fore_color=clr; } //--- Возврат цвета фона и рамки color BackColor(void) const { return this.m_back_color; } color ForeColor(void) const { return this.m_fore_color; } //--- Сбрасывает счетчик в ноль void Reset(void) { this.m_last_index=0; } //--- Добавляет результат для отрисовки полоски в прогресс-баре void AddResult(bool good, const bool chart_redraw); //--- Обновляет прогресс-бар на графике void Update(const bool chart_redraw); }; //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CColorProgressBar::CColorProgressBar() : m_last_index(0), m_good_color(clrSeaGreen), m_bad_color(clrLightPink) { //--- Зададим размер массива проходов с запасом ::ArrayResize(this.m_passes, 5000, 1000); ::ArrayInitialize(this.m_passes, 0); } //+------------------------------------------------------------------+ //| Добавление результата | //+------------------------------------------------------------------+ void CColorProgressBar::AddResult(bool good, const bool chart_redraw) { this.m_passes[this.m_last_index]=good; //--- Добавим еще одну вертикальную черту нужного цвета в прогресс-бар this.m_foreground.LineVertical(this.X1()+1+this.m_last_index, this.Y1()+1, this.Y2()-1, ::ColorToARGB(good ? this.m_good_color : this.m_bad_color)); //--- Обновление на графике this.m_foreground.Update(chart_redraw); //--- Обновление индекса this.m_last_index++; if(this.m_last_index>=this.m_bound.Width()-1) this.m_last_index=0; } //+------------------------------------------------------------------+ //| Обновление прогресс-бара на графике | //+------------------------------------------------------------------+ void CColorProgressBar::Update(const bool chart_redraw) { //--- Зальем фон цветом фона this.m_background.FillRectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_back_color)); //--- Нарисуем рамку this.m_background.Rectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_fore_color)); //--- Обновим чарт this.m_background.Update(chart_redraw); }
В классе нет собственных объектов канваса для рисования. Для указания объекта канваса, на котором производится рисование, есть метод, в который передаётся указатель на существующий канвас, и он назначается переменным класса. На этом канвасе методы класса и будут рисовать. Здесь два объекта — для рисования фона прогресс-бара, и рисования на переднем плане поверх нарисованного фона. Канвасом будут выступать объекты CCanvas класса специального чарта, на котором будет рисоваться этот прогресс-бар.
Класс для отрисовки графиков статистики и таблиц результатов оптимизации и параметров настройки советника:
//+------------------------------------------------------------------+ //| Класс для отрисовки графиков статистики и таблиц | //| результатов оптимизации и параметров настройки советника | //+------------------------------------------------------------------+ class CStatChart: public CObject { private: color m_back_color; // Цвет фона color m_fore_color; // Цвет рамки int m_line_width; // Толщина линии в пискелях int m_lines; // Количество линий на графике CArrayDouble m_seria[]; // Массивы для хранения значений графика bool m_profitseria[]; // Прибыльная серия или нет int m_lastseria_index; // Индекс свежей линии на графике color m_profit_color; // Цвет прибыльной серии color m_loss_color; // Цвет убыточной серии color m_selected_color; // Цвет выбранной лучшей серии protected: CCanvas *m_background; // Указатель на объект класса CCanvas для рисования на фоне CCanvas *m_foreground; // Указатель на объект класса CCanvas для рисования на переднем плане CRect m_bound_chart; // Рабочая область графика CRect m_bound_head; // Рабочая область заголовка чарта CColorProgressBar m_progress_bar; // Прогресс-бар CButton m_button_replay; // Кнопка воспроизведения CButtonSwitch m_button_res; // Кнопка выбора одного из трёх лучших результатов int m_tab_id; // Идентификатор вкладки public: //--- Конструктор/деструктор CStatChart() : m_lastseria_index(0), m_profit_color(clrForestGreen), m_loss_color(clrOrangeRed), m_selected_color(clrDodgerBlue), m_tab_id(0) {}; ~CStatChart() { this.m_background=NULL; this.m_foreground=NULL; } //--- Устанавливает указатель на канвас void SetCanvas(CCanvas *background, CCanvas *foreground) { if(background==NULL) { ::Print(__FUNCTION__, ": Error. Background is NULL"); return; } if(foreground==NULL) { ::Print(__FUNCTION__, ": Error. Foreground is NULL"); return; } this.m_background=background; this.m_foreground=foreground; this.m_progress_bar.SetCanvas(background, foreground); } //--- Устанавливает координаты и размеры рабочей области чарта и прогресс-бара на канвасе void SetChartBounds(const int x1, const int y1, const int x2, const int y2) { this.m_bound_chart.SetBound(x1, y1, x2, y2); this.SetBoundHeader(x1, y1-CELL_H, x2, y1); this.m_progress_bar.SetBound(x1, y2-CELL_H, x2, y2); } //--- Устанавливает координаты и размеры заголовка чарта на канвасе void SetBoundHeader(const int x1, const int y1, const int x2, const int y2) { this.m_bound_head.SetBound(x1, y1, x2, y2); } //--- Возвращает указатель на (1) себя, (2) прогресс-бар CStatChart *Get(void) { return &this; } CColorProgressBar*GetProgressBar(void) { return(&this.m_progress_bar); } //--- Установка/возврат идентификатора вкладки void SetTabID(const int id) { this.m_tab_id=id; } int TabID(void) const { return this.m_tab_id; } //--- Возврат координат границ прямоугольной области чарта int X1(void) const { return this.m_bound_chart.left; } int Y1(void) const { return this.m_bound_chart.top; } int X2(void) const { return this.m_bound_chart.right; } int Y2(void) const { return this.m_bound_chart.bottom; } //--- Возврат координат границ прямоугольной области заголовка int HeaderX1(void) const { return this.m_bound_head.left; } int HeaderY1(void) const { return this.m_bound_head.top; } int HeaderX2(void) const { return this.m_bound_head.right; } int HeaderY2(void) const { return this.m_bound_head.bottom; } //--- Возврат координат границ прямоугольной области прогресс-бара int ProgressBarX1(void) const { return this.m_progress_bar.X1(); } int ProgressBarY1(void) const { return this.m_progress_bar.Y1(); } int ProgressBarX2(void) const { return this.m_progress_bar.X2(); } int ProgressBarY2(void) const { return this.m_progress_bar.Y2(); } //--- Возвращает указатель на кнопку (1) воспроизведения, (2) выбора результата (3) худшего, (4) среднего, (5) лучшего результата CButton *ButtonReplay(void) { return(&this.m_button_replay); } CButtonSwitch *ButtonResult(void) { return(&this.m_button_res); } CButtonTriggered *ButtonResultMin(void) { return(this.m_button_res.GetButton(0)); } CButtonTriggered *ButtonResultMid(void) { return(this.m_button_res.GetButton(1)); } CButtonTriggered *ButtonResultMax(void) { return(this.m_button_res.GetButton(2)); } //--- (1) Скрывает, (2) показывает, (3) переносит на передний план кнопку вывбора результатов bool ButtonsResultHide(void) { return(this.m_button_res.Hide()); } bool ButtonsResultShow(void) { return(this.m_button_res.Show()); } bool ButtonsResultBringToTop(void) { return(this.m_button_res.BringToTop()); } //--- Создаёт кнопку воспроизведения bool CreateButtonReplay(void) { if(this.m_background==NULL) { ::PrintFormat("%s: Background is not assigned (use SetCanvas() function first)"); return false; } string text="Optimization Completed: Click to Replay"; int w=this.m_background.TextWidth(text); //--- Левая верхняя координата кнопки CPoint cp=this.m_bound_head.CenterPoint(); int x=cp.x-w/2; int y=this.Y1()+this.m_bound_head.top-2; //--- Создаём кнопу и устанавливаем для неё новые цвета, скрываем созданную кнопку if(!this.m_button_replay.Create(::StringFormat("Tab%d_ButtonReplay", this.m_tab_id), text, x, y, w, CELL_H-1)) return false; this.m_button_replay.SetDefaultColors(COLOR_BACKGROUND, STATE_OFF, C'144,238,144', C'144,228,144', C'144,218,144', clrSilver); this.m_button_replay.SetDefaultColors(COLOR_BORDER, STATE_OFF, C'144,238,144', C'144,228,144', C'144,218,144', clrSilver); this.m_button_replay.SetDefaultColors(COLOR_FOREGROUND, STATE_OFF, clrBlack, clrBlack, clrBlack, clrGray); this.m_button_replay.ResetUsedColors(STATE_OFF); this.m_button_replay.Draw(false); this.m_button_replay.Hide(); return true; } //--- Создаёт кнопку выбора результатов bool CreateButtonResults(void) { if(this.m_background==NULL) { ::PrintFormat("%s: Background is not assigned (use SetCanvas() function first)"); return false; } //--- Левая верхняя координата кнопки int x=this.m_bound_head.left+1; int y=this.m_progress_bar.Y1()+CELL_H+2; int w=BUTT_RES_W; //--- Создаём кнопу и устанавливаем для неё новые цвета, скрываем созданную кнопку if(!this.m_button_res.Create(::StringFormat("Tab%u_ButtonRes",this.m_tab_id), "", x, y, w, CELL_H-1)) return false; string text[3]={"Worst result of the top 3", "Average result of the top 3", "Best result of the top 3"}; if(!this.m_button_res.AddNewButton(text, w)) return false; this.m_button_res.GetButton(0).SetDefaultColors(COLOR_BORDER, STATE_OFF, C'228,228,228', C'228,228,228', C'228,228,228', clrSilver); this.m_button_res.GetButton(0).ResetUsedColors(STATE_OFF); this.m_button_res.GetButton(1).SetDefaultColors(COLOR_BORDER, STATE_OFF, C'228,228,228', C'228,228,228', C'228,228,228', clrSilver); this.m_button_res.GetButton(1).ResetUsedColors(STATE_OFF); this.m_button_res.GetButton(2).SetDefaultColors(COLOR_BORDER, STATE_OFF, C'228,228,228', C'228,228,228', C'228,228,228', clrSilver); this.m_button_res.GetButton(2).ResetUsedColors(STATE_OFF); this.m_button_res.Draw(false); this.m_button_res.Hide(); return true; } //--- Устанавливает цвет фона void SetBackColor(const color clr) { this.m_back_color=clr; this.m_progress_bar.SetBackColor(clr); } //--- Устанавливает цвет рамки void SetForeColor(const color clr) { this.m_fore_color=clr; this.m_progress_bar.SetForeColor(clr); } //--- Задаёт количество линий на графике void SetLines(const int num) { this.m_lines=num; ::ArrayResize(this.m_seria, num); ::ArrayResize(this.m_profitseria, num); } //--- Установка цвета (1) прибыльной, (2) убыточной, (3) выбранной серии void SetProfitColorLine(const color clr) { this.m_profit_color=clr; } void SetLossColorLine(const color clr) { this.m_loss_color=clr; } void SetSelectedLineColor(const color clr) { this.m_selected_color=clr; } //--- Обновление объекта на экране void Update(color clr, const int line_width, const bool chart_redraw); //--- Добавление данных из массива void AddSeria(const double &array[], bool profit); //--- Рисует график void Draw(const int seria_index, color clr, const int line_width, const bool chart_redraw); //--- Рисует линию в привычных координатах (слева-направо, снизу-вверх) void Line(int x1, int y1, int x2, int y2, uint col, int size); //--- Получение макс. и мин. значения в серии double MaxValue(const int seria_index); double MinValue(const int seria_index); //--- Обработчик событий void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam) { //--- Если кнопка воспроизведения не скрыта - вызываем её обработчик событий if(!this.m_button_replay.IsHidden()) this.m_button_replay.OnChartEvent(id, lparam, dparam, sparam); //--- Если кнопка выбора результата не скрыта - вызываем её обработчик событий if(!this.m_button_res.IsHidden()) this.m_button_res.OnChartEvent(id, lparam, dparam, sparam); } };
Класс рисует на указанном канвасе (фон и передний план) таблицы параметров и результатов тестирования, графики проходов, прогресс-бар и кнопки для запуска воспроизведения завершённого процесса оптимизации и выбора лучших результатов по некоторым критериям оптимизации.
Следует заметить, что для указания границ прямоугольной области канваса, внутри которой находится отслеживаемый объект или область, в классах, рассматриваемых здесь, используется структура CRect.
Структура описана в файле \MQL5\Include\Controls\Rect.mqh, и служит удобным инструментом для указания границ прямоугольной области, внутри которой есть важные элементы. Например, мы можем на канвасе ограничить область, внутри которой нужно отслеживать курсор мышки, либо указать размеры ограничивающего прямоугольника на весь размер канваса. В этом случае, вся область всего объекта будет доступна для взаимодействия с курсором. В структуре созданы методы, возвращающие координаты границ прямоугольной области. Причём установить границы и получить их значения возможно несколькими способами — всё зависит от потребностей и структуры объектов. Также реализованы методы для перемещения и смещения прямоугольной области. В общем, это удобный инструмент для указания границ какой-либо области, которые каким-либо образом необходимо отслеживать.
В рассматриваемых классах эти области нужны для взаимодействия с курсором мышки и указания, где расположены на канвасе объекты.
Метод для обновления чарта:
//+------------------------------------------------------------------+ //| Обновление чарта | //+------------------------------------------------------------------+ void CStatChart::Update(color clr, const int line_width, const bool chart_redraw) { //--- Если канвас для фона или переднего плана не установлен - уходим if(this.m_background==NULL || this.m_foreground==NULL) return; //--- StatChart зальем фон this.m_background.FillRectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_back_color)); //--- StatChart нарисуем рамку this.m_background.Rectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_fore_color)); //--- ProgressBar зальем фон и нарисуем рамку this.m_progress_bar.Update(false); //--- Отрисуем каждую серию на 80% доступной площади чарта по вертикали и горизонтали for(int i=0; i<this.m_lines; i++) { //--- Если цвет задан отсутствующим - используем цвета прибыльной и убыточной серий if(clr==clrNONE) { clr=this.m_loss_color; if(this.m_profitseria[i]) clr=this.m_profit_color; } //--- иначе - используем цвет, заданный для выбранной линии else clr=this.m_selected_color; //--- Рисуем график результатов оптимизации this.Draw(i, clr, line_width, false); } //--- Обновим оба канваса this.m_background.Update(false); this.m_foreground.Update(chart_redraw); }
Прямоугольная область канваса, предназначенная для рисования графиков прохода, стирается, на ней рисуется линия баланса и прогресс-бар.
Метод, добавляющий новую серию данных для отрисовки на графике:
//+------------------------------------------------------------------+ //| Добавляет новую серию данных для отрисовки на графике | //+------------------------------------------------------------------+ void CStatChart::AddSeria(const double &array[], bool profit) { //--- Добавляем массив в серию номер m_lastseria_index this.m_seria[this.m_lastseria_index].Resize(0); this.m_seria[this.m_lastseria_index].AddArray(array); this.m_profitseria[this.m_lastseria_index]=profit; //--- Отслеживаем индекс последней линии (не используется в данный момент) this.m_lastseria_index++; if(this.m_lastseria_index>=this.m_lines) this.m_lastseria_index=0; }
Каждый новый проход оптимизатора, его массив данных, должен заноситься в массив серий, что и выполняет данный метод.
Методы для получения максимального и минимального значения указанной серии в массиве проходов оптимизатора:
//+------------------------------------------------------------------+ //| Получение максимального значения указанной серии | //+------------------------------------------------------------------+ double CStatChart::MaxValue(const int seria_index) { double res=this.m_seria[seria_index].At(0); int total=this.m_seria[seria_index].Total(); //--- Переберем массив и сравним каждые две соседние серии for(int i=1; i<total; i++) { if(this.m_seria[seria_index].At(i)>res) res=this.m_seria[seria_index].At(i); } //--- результат return res; } //+------------------------------------------------------------------+ //| Получение минимального значения указанной серии | //+------------------------------------------------------------------+ double CStatChart::MinValue(const int seria_index) { double res=this.m_seria[seria_index].At(0);; int total=this.m_seria[seria_index].Total(); //--- Переберем массив и сравним каждые две соседние серии for(int i=1; i<total; i++) { if(this.m_seria[seria_index].At(i)<res) res=this.m_seria[seria_index].At(i); } //--- результат return res; }
Чтобы графики проходов оптимизатора расположить относительно по центру специального чарта, нужно знать максимальное и минимальное значения в серии прохода. Потом по этим значениям можно рассчитать относительные координаты линии на чарте так, чтобы линия вписалась в 80% пространства чарта, отведённое для рисования графиков баланса проходов оптимизатора.
Метод для рисования линии баланса на чарте:
//+------------------------------------------------------------------+ //| Перегрузка базовой функции рисования | //+------------------------------------------------------------------+ void CStatChart::Line(int x1, int y1, int x2, int y2, uint col, int size) { //--- Если канвас не задан - уходим if(this.m_foreground==NULL) return; //--- Так как ось Y перевернута, то нужно перевернуть y1 и y2 int y1_adj=this.m_bound_chart.Height()-CELL_H-y1; int y2_adj=this.m_bound_chart.Height()-CELL_H-y2; //--- Рисуем сглаженную линию //--- Если толщина линии меньше 3, то рисуем линию с использованием алгоритма сглаживания Ву //--- (при толщине 1 и 2 в методе LineThick() вызывается метод LineWu()), //--- иначе - рисуем сглаженную линию заданной толщины при помощи LineThick this.m_foreground.LineThick(x1, y1_adj, x2, y2_adj,::ColorToARGB(col), (size<1 ? 1 : size), STYLE_SOLID, LINE_END_ROUND); }
Это перегруженный метод одноимённого метода класса CCanvas. Координаты на графике начинаются от верхнего левого угла. А привычные координаты графиков баланса начинаются от левого нижнего.
В этом методе переворачиваются экранные координаты Y для отрисовки не перевёрнутой линии баланса по значениям точек баланса из массива.
Метод, рисующий линии баланса на чарте:
//+------------------------------------------------------------------+ //| Отрисовка линии баланса на графике | //+------------------------------------------------------------------+ void CStatChart::Draw(const int seria_index, color clr, const int line_width, const bool chart_redraw) { //--- Если канвас не задан - уходим if(this.m_foreground==NULL) return; //--- Готовим коэффициенты для перевода значений в пиксели double min=this.MaxValue(seria_index); double max=this.MinValue(seria_index); double size=this.m_seria[seria_index].Total(); //--- Отступы от края графика double x_indent=this.m_bound_chart.Width()*0.05; double y_indent=this.m_bound_chart.Height()*0.05; //--- Вычислим коэффициенты double k_y=(max-min)/(this.m_bound_chart.Height()-2*CELL_H-2*y_indent); double k_x=(size)/(this.m_bound_chart.Width()-2*x_indent); //--- Постоянные double start_x=this.m_bound_chart.left+x_indent; double start_y=this.m_bound_chart.bottom-2*CELL_H*2-y_indent; //--- Теперь рисуем ломанную линию проходя по всем точкам серии for(int i=1; i<size; i++) { //--- переводим значения в пиксели int x1=(int)((i-0)/k_x+start_x); // номер значения откладываем на горизонтали int y1=(int)(start_y-(m_seria[seria_index].At(i)-min)/k_y); // по вертикали int x2=(int)((i-1-0)/k_x+start_x);// номер значения откладываем на горизонтали int y2=(int)(start_y-(m_seria[seria_index].At(i-1)-min)/k_y); // по вертикали //--- Выводим линию от предыдущей точки к текущей this.Line(x1, y1, x2, y2, clr, line_width); } //--- Обновление канваса с перерисовкой графика (если флаг установлен) this.m_foreground.Update(chart_redraw); }
Здесь рассчитываются нужные координаты линии баланса на графике (внутри области чарта, предназначенной для рисования графиков баланса), и в цикле по массиву указанной серии рисуем линии между всеми точками баланса, записанными в массиве.
Класс данных фрейма:
//+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum ENUM_FRAME_PROP // Свойства фрейма { FRAME_PROP_PASS_NUM, // Номер прохода FRAME_PROP_SHARPE_RATIO, // Результат Sharpe Ratio FRAME_PROP_NET_PROFIT, // Результат Net Profit FRAME_PROP_PROFIT_FACTOR, // Результат Profit Factor FRAME_PROP_RECOVERY_FACTOR, // Результат Recovery Factor }; //+------------------------------------------------------------------+ //| Класс данных фрейма | //+------------------------------------------------------------------+ class CFrameData : public CObject { protected: ulong m_pass; // Номер прохода double m_sharpe_ratio; // Коэффициент Шарпа double m_net_profit; // Общая прибыль double m_profit_factor; // Доходность double m_recovery_factor; // Фактор восстановления public: //--- Установка свойств фрейма (результатов прохода) void SetPass(const ulong pass) { this.m_pass=pass; } void SetSharpeRatio(const double value) { this.m_sharpe_ratio=value; } void SetNetProfit(const double value) { this.m_net_profit=value; } void SetProfitFactor(const double value) { this.m_profit_factor=value; } void SetRecoveryFactor(const double value) { this.m_recovery_factor=value; } //--- Возврат свойств фрейма (результатов прохода) ulong Pass(void) const { return this.m_pass; } double SharpeRatio(void) const { return this.m_sharpe_ratio; } double NetProfit(void) const { return this.m_net_profit; } double ProfitFactor(void) const { return this.m_profit_factor; } double RecoveryFactor(void) const { return this.m_recovery_factor; } //--- Описание свойств string PassDescription(void) const { return ::StringFormat("Pass: %I64u", this.m_pass); } string SharpeRatioDescription(void) const { return ::StringFormat("Sharpe Ratio: %.2f", this.m_sharpe_ratio); } string NetProfitDescription(void) const { return ::StringFormat("Net Profit: %.2f", this.m_net_profit); } string ProfitFactorDescription(void) const { return ::StringFormat("Profit Factor: %.2f", this.m_profit_factor); } string RecoveryFactorDescription(void) const { return ::StringFormat("Recovery Factor: %.2f", this.m_recovery_factor); } //--- Вывод в журнал свойств фрейма void Print(void) { ::PrintFormat("Frame %s:", this.PassDescription()); ::PrintFormat(" - %s", this.SharpeRatioDescription()); ::PrintFormat(" - %s", this.NetProfitDescription()); ::PrintFormat(" - %s", this.ProfitFactorDescription()); ::PrintFormat(" - %s", this.RecoveryFactorDescription()); } //--- Метод сравнения двух объектов virtual int Compare(const CObject *node,const int mode=0) const { //--- Вещественные значения сравниваем как двухзначные const CFrameData *obj=node; switch(mode) { case FRAME_PROP_SHARPE_RATIO : return(::NormalizeDouble(this.SharpeRatio(),2) > ::NormalizeDouble(obj.SharpeRatio(),2) ? 1 : ::NormalizeDouble(this.SharpeRatio(),2) < ::NormalizeDouble(obj.SharpeRatio(),2) ? -1 : 0); case FRAME_PROP_NET_PROFIT : return(::NormalizeDouble(this.NetProfit(),2) > ::NormalizeDouble(obj.NetProfit(),2) ? 1 : ::NormalizeDouble(this.NetProfit(),2) < ::NormalizeDouble(obj.NetProfit(),2) ? -1 : 0); case FRAME_PROP_PROFIT_FACTOR : return(::NormalizeDouble(this.ProfitFactor(),2) > ::NormalizeDouble(obj.ProfitFactor(),2) ? 1 : ::NormalizeDouble(this.ProfitFactor(),2) < ::NormalizeDouble(obj.ProfitFactor(),2) ? -1 : 0); case FRAME_PROP_RECOVERY_FACTOR : return(::NormalizeDouble(this.RecoveryFactor(),2)> ::NormalizeDouble(obj.RecoveryFactor(),2) ? 1 : ::NormalizeDouble(this.RecoveryFactor(),2)< ::NormalizeDouble(obj.RecoveryFactor(),2) ? -1 : 0); //---FRAME_PROP_PASS_NUM default : return(this.Pass()>obj.Pass() ? 1 : this.Pass()<obj.Pass() ? -1 : 0); } } //--- Конструкторы/деструктор CFrameData (const ulong pass, const double sharpe_ratio, const double net_profit, const double profit_factor, const double recovery_factor) : m_pass(pass), m_sharpe_ratio(sharpe_ratio), m_net_profit(net_profit), m_profit_factor(profit_factor), m_recovery_factor(recovery_factor) {} CFrameData (void) : m_pass(0), m_sharpe_ratio(0), m_net_profit(0), m_profit_factor(0), m_recovery_factor(0) {} ~CFrameData (void) {} };
После завершения каждого прохода оптимизатора, в терминал отправляется фрейм. В нём содержатся все данные, которые были получены при завершении этого прохода. Чтобы получить доступ к данным какого-либо прохода, нужно в цикле по всем полученным фреймам искать фрейм с нужным номером и получать его данные. Это совсем не оптимально. Нам нужно иметь возможность быстрого доступа к данным нужного прохода, и иметь возможность сортировать все проходы по указанному свойству, так как нам нужно будет выбирать три лучших прохода по одному из четырёх критериев оптимизации.
Выход — кэшировать проходы. Для этого нам нужен класс объекта фрейма. После завершения каждого прохода и отсылки фрейма в терминал, нужно создавать объект фрейм, заполнять его свойства данными полученного фрейма тестирования и размещать объект-фрейм в списке. Далее, после завершения процесса оптимизации и получения всех фреймов, у нас в списке фреймов будут находиться копии всех фреймов. И теперь можно будет этот список фреймов сортировать по нужным свойствам и быстро получать из него данные нужного фрейма.
Стоит отметить, что в методе Compare() пришлось делать сравнение вещественных чисел не сравнением нормализованной разности с нулём, а сравнение двух нормализованных чисел между собой. Почему?
Для сравнения двух вещественных чисел можно пойти разными путями. Первый — сравнивать ненормализованные числа. Сначала сравнивать тернарным оператором на "больше", затем на "меньше", а в конце — что осталось, то значит "равно". Либо можно сравнивать нормализованную разницу двух чисел с нулём. Но здесь пришлось нормализовать оба числа до двух знаков, и уже эти значения сравнивать.
Дело в том, что в терминале в таблице результатов показываются двухзначные числа в результатах оптимизации. Внутри же, эти числа не нормализованы до двух знаков. Т.е. двузначное представление результатов отражено только в таблице результатов. И если в таблице есть значения, например, 1.09 и 1.08, то на самом деле это может быть и не так. Там могут быть числа 1.085686399864 и 1.081254322375. Оба числа округляются в таблице до 1.09 и 1.08. А вот при сравнении можно натолкнуться на то, что оба числа округлены нормализацией до одинаковой величины. А если не нормализовать, то значения 1.09 может и не быть. А это приведёт к некорректному поиску лучших проходов.
Выход — нормализовать до двух знаков оба числа, и уже потом сравнить их округлённые значения.
Класс просмотровщика фреймов:
//+------------------------------------------------------------------+ //| Класс просмотровщика фреймов | //+------------------------------------------------------------------+ class CFrameViewer : public CObject { private: int m_w; // Ширина графика int m_h; // Высота графика color m_selected_color; // Цвет выбранной серии из трёх лучших uint m_line_width; // Толщина линии выбранной серии из трёх лучших bool m_completed; // Флаг завершения оптимизации CFrameData m_frame_tmp; // Объект фрейм для поиска по свойству CArrayObj m_list_frames; // Список фреймов CTabControl m_tab_control; // Элемент управления Tab Control //--- Объявляем объекты вкладок на элементе управления Tab Control //--- Вкладка 0 (Optimization) элемента управления Tab Control CTableDataControl m_table_inp_0; // Таблица параметров оптимизации на вкладке 0 CTableDataControl m_table_stat_0; // Таблица результатов оптимизации на вкладке 0 CStatChart m_chart_stat_0; // График оптимизации на вкладке 0 CColorProgressBar*m_progress_bar; // Прогресс-бар на графике оптимизации на вкладке 0 //--- Вкладка 1 (Sharpe Ratio) элемента управления Tab Control CTableDataControl m_table_inp_1; // Таблица параметров оптимизации на вкладке 1 CTableDataControl m_table_stat_1; // Таблица результатов оптимизации на вкладке 1 CStatChart m_chart_stat_1; // График результатов оптимизации на вкладке 1 //--- Вкладка 2 (Net Profit) элемента управления Tab Control CTableDataControl m_table_inp_2; // Таблица параметров оптимизации на вкладке 2 CTableDataControl m_table_stat_2; // Таблица результатов оптимизации на вкладке 2 CStatChart m_chart_stat_2; // График результатов оптимизации на вкладке 2 //--- Вкладка 3 (Profit Factor) элемента управления Tab Control CTableDataControl m_table_inp_3; // Таблица параметров оптимизации на вкладке 3 CTableDataControl m_table_stat_3; // Таблица результатов оптимизации на вкладке 3 CStatChart m_chart_stat_3; // График результатов оптимизации на вкладке 3 //--- Вкладка 4 (Recovery Factor) элемента управления Tab Control CTableDataControl m_table_inp_4; // Таблица параметров оптимизации на вкладке 4 CTableDataControl m_table_stat_4; // Таблица результатов оптимизации на вкладке 4 CStatChart m_chart_stat_4; // График результатов оптимизации на вкладке 4 protected: //--- Возвращает указатель на таблицу параметров оптимизации по индексу вкладки CTableDataControl*GetTableInputs(const uint tab_id) { switch(tab_id) { case 0 : return this.m_table_inp_0.Get(); case 1 : return this.m_table_inp_1.Get(); case 2 : return this.m_table_inp_2.Get(); case 3 : return this.m_table_inp_3.Get(); case 4 : return this.m_table_inp_4.Get(); default: return NULL; } } //--- Возвращает указатель на таблицу результатов оптимизации по индексу вкладки CTableDataControl*GetTableStats(const uint tab_id) { switch(tab_id) { case 0 : return this.m_table_stat_0.Get(); case 1 : return this.m_table_stat_1.Get(); case 2 : return this.m_table_stat_2.Get(); case 3 : return this.m_table_stat_3.Get(); case 4 : return this.m_table_stat_4.Get(); default: return NULL; } } //--- Возвращает указатель на график результатов оптимизации по индексу вкладки CStatChart *GetChartStats(const uint tab_id) { switch(tab_id) { case 0 : return this.m_chart_stat_0.Get(); case 1 : return this.m_chart_stat_1.Get(); case 2 : return this.m_chart_stat_2.Get(); case 3 : return this.m_chart_stat_3.Get(); case 4 : return this.m_chart_stat_4.Get(); default: return NULL; } } //--- Добавляет объект фрейм в список bool AddFrame(CFrameData *frame) { if(frame==NULL) { ::PrintFormat("%s: Error: Empty object passed",__FUNCTION__); return false; } this.m_frame_tmp.SetPass(frame.Pass()); this.m_list_frames.Sort(FRAME_PROP_PASS_NUM); int index=this.m_list_frames.Search(frame); if(index>WRONG_VALUE) return false; return this.m_list_frames.Add(frame); } //--- Рисует таблицу статистики оптимизации на указанной вкладке void TableStatDraw(const uint tab_id, const int x, const int y, const int w, const int h, const bool chart_redraw); //--- Рисует таблицу входных параметров оптимизации на указанной вкладке void TableInpDraw(const uint tab_id, const int x, const int y, const int w, const int h, const uint rows, const bool chart_redraw); //--- Рисует график оптимизации на указанной вкладке void ChartOptDraw(const uint tab_id, const bool opt_completed, const bool chart_redraw); //--- Рисует таблицы данных и график оптимизации void DrawDataChart(const uint tab_id); //--- Рисует графики трёх лучших проходов по критерию оптимизации void DrawBestFrameData(const uint tab_id, const int res_index); //--- Управляет отображением управляющих объектов на графиках оптимизации void ControlObjectsView(const uint tab_id); //--- Повторное проигрывание фреймов после окончания оптимизации void ReplayFrames(const int delay_ms); //--- Получение данных текущего фрейма и вывод их на указанной вкладке в таблицу и на график результатов оптимизации bool DrawFrameData(const uint tab_id, const string text, color clr, const uint line_width, ulong &pass, string ¶ms[], uint &par_count, double &data[]); //--- Выводит данные указанного фрейма на график оптимизации bool DrawFrameDataByPass(const uint tab_id, const ulong pass_num, const string text, color clr, const uint line_width, double &data[]); //--- Заполняет массив индексами фреймов трёх лучших проходов для указанного критерия оптимизации (по индексу вкладки) bool FillArrayBestFrames(const uint tab_id, ulong &array_passes[]); //--- Выводит на графики результатов оптимизации на каждой вкладке по три лучших прохода void DrawBestFrameDataAll(void); //--- Ищет и возвращает указатель на объект фрейма, со значением свойства меньше образца CFrameData *FrameSearchLess(CFrameData *frame, const int mode); public: //--- Установка толщины выбранной линии void SetSelectedLineWidth(const uint width) { this.m_line_width=width; } //--- Установка цвета прибыльной серии void SetProfitColorLine(const color clr) { int total=this.m_tab_control.TabsTotal(); for(int i=1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetProfitColorLine(clr); } } //--- Установка цвета убыточной серии void SetLossColorLine(const color clr) { int total=this.m_tab_control.TabsTotal(); for(int i=1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetLossColorLine(clr); } } //--- Установка цвета выбранной серии void SetSelectedLineColor(const color clr) { int total=this.m_tab_control.TabsTotal(); for(int i=1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetSelectedLineColor(clr); } } //--- Обработчики событий тестера стратегий void OnTester(const double OnTesterValue); int OnTesterInit(const int lines, const int selected_line_width, const color selected_line_color); void OnTesterPass(void); void OnTesterDeinit(void); //--- Обработчики событий графика void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam,const int delay_ms); protected: //--- Обработчик (1) смены вкладки элемента Tab Control, (2) выбора кнопки переключателя Button Switch void OnTabSwitchEvent(const int tab_id); void OnButtonSwitchEvent(const int tab_id, const uint butt_id); public: //--- Конструктор/деструктор CFrameViewer(void); ~CFrameViewer(void){ this.m_list_frames.Clear(); } };
Нам точно известно, сколько вкладок будет, и какие элементы будут расположены на каждой вкладке. Поэтому здесь нет создания новых объектов, а просто объявлены экземпляры нужных объектов для каждой вкладки, методы доступа к ним и методы для работы класса.
Конструктор:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CFrameViewer::CFrameViewer(void) : m_completed(false), m_progress_bar(NULL), m_selected_color(clrDodgerBlue), m_line_width(1) { //--- Размеры окна графика this.m_w=(int)::ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); this.m_h=(int)::ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Получаем указатель на прогресс-бар из объекта чарта статистики this.m_progress_bar=this.m_chart_stat_0.GetProgressBar(); this.m_list_frames.Clear(); }
В конструкторе получаем и запоминаем ширину и высоту графика, на котором запущен эксперт, находим и записываем указатель на прогресс-бар и очищаем список фреймов.
При запуске оптимизации, перед её началом необходимо подготовить график, на котором в клиентском терминале будет запущена копия советника во frame-режиме. График будет откреплён от терминала, на нём — во весь его размер — будет размещён элемент управления Tab Control, а на его вкладках расположены остальные элементы, на которые будут выводиться графики баланса проходов и кнопки управления.
Всё это необходимо сделать в обработчике OnTesterInit(). Для этого в классе предусмотрены одноимённые обработчики, которые в советнике запускаются из экземпляра класса CFrameViewer.
Обработчик OnTesterInit:
//+------------------------------------------------------------------+ //| Должна вызываться в обработчике эксперта OnTesterInit() | //+------------------------------------------------------------------+ int CFrameViewer::OnTesterInit(const int lines, const int selected_line_width, const color selected_line_color) { //--- Идентификатор графика с экспертом, работающем во Frame-режиме long chart_id=::ChartID(); //--- Готовим плавающий график для рисования таблиц статистики и линий баланса ::ResetLastError(); if(!::ChartSetInteger(chart_id, CHART_SHOW, false)) { ::PrintFormat("%s: ChartSetInteger() failed. Error %d",__FUNCTION__, GetLastError()); return INIT_FAILED; } if(!::ChartSetInteger(chart_id, CHART_IS_DOCKED, false)) { ::PrintFormat("%s: ChartSetInteger() failed. Error %d",__FUNCTION__, GetLastError()); return INIT_FAILED; } //--- Очищаем график полностью от всех графических объектов ::ObjectsDeleteAll(chart_id); //--- По размерам графика создаём элемент управления Tab Control с пятью вкладками int w=(int)::ChartGetInteger(chart_id, CHART_WIDTH_IN_PIXELS); int h=(int)::ChartGetInteger(chart_id, CHART_HEIGHT_IN_PIXELS); if(this.m_tab_control.Create("TabControl", "", 0, 0, w, h)) { //--- Если элемент управления создан успешно - добавляем к нему пять вкладок bool res=true; for(int i=0; i<5; i++) { string tab_text=(i==1 ? "Sharpe Ratio" : i==2 ? "Net Profit" : i==3 ? "Profit Factor" : i==4 ? "Recovery Factor" : "Optimization"); res &=this.m_tab_control.AddTab(i, tab_text); } if(!res) { ::PrintFormat("%s: Errors occurred while adding tabs to the Tab Control",__FUNCTION__); return INIT_FAILED; } } else { Print("Tab Control creation failed"); return INIT_FAILED; } //--- Объекты CCanvas рабочей области вкладки 0 (Optimization) для рисования фоновых изображений и текста CCanvas *tab0_background=this.m_tab_control.GetTabBackground(0); CCanvas *tab0_foreground=this.m_tab_control.GetTabForeground(0); //--- Объекты CCanvas рабочей области вкладки 1 (Sharpe Ratio) для рисования фоновых изображений и текста CCanvas *tab1_background=this.m_tab_control.GetTabBackground(1); CCanvas *tab1_foreground=this.m_tab_control.GetTabForeground(1); //--- Объекты CCanvas рабочей области вкладки 2 (Net Profit) для рисования фоновых изображений и текста CCanvas *tab2_background=this.m_tab_control.GetTabBackground(2); CCanvas *tab2_foreground=this.m_tab_control.GetTabForeground(2); //--- Объекты CCanvas рабочей области вкладки 3 (Profit Factor) для рисования фоновых изображений и текста CCanvas *tab3_background=this.m_tab_control.GetTabBackground(3); CCanvas *tab3_foreground=this.m_tab_control.GetTabForeground(3); //--- Объекты CCanvas рабочей области вкладки 4 (Recovery Factor) для рисования фоновых изображений и текста CCanvas *tab4_background=this.m_tab_control.GetTabBackground(4); CCanvas *tab4_foreground=this.m_tab_control.GetTabForeground(4); //--- Устанавливаем объектам графиков статистики оптимизации идентификаторы вкладок this.m_chart_stat_0.SetTabID(0); this.m_chart_stat_1.SetTabID(1); this.m_chart_stat_2.SetTabID(2); this.m_chart_stat_3.SetTabID(3); this.m_chart_stat_4.SetTabID(4); //--- Указываем для объектов чартов статистики, что рисуем на вкладке с соответствующим индексом this.m_chart_stat_0.SetCanvas(tab0_background, tab0_foreground); this.m_chart_stat_1.SetCanvas(tab1_background, tab1_foreground); this.m_chart_stat_2.SetCanvas(tab2_background, tab2_foreground); this.m_chart_stat_3.SetCanvas(tab3_background, tab3_foreground); this.m_chart_stat_4.SetCanvas(tab4_background, tab4_foreground); //--- Устанавливаем количество серий на графиках статистики оптимизации this.m_chart_stat_0.SetLines(lines); this.m_chart_stat_1.SetLines(lines); this.m_chart_stat_2.SetLines(lines); this.m_chart_stat_3.SetLines(lines); this.m_chart_stat_4.SetLines(lines); //--- Задаём цвета фона и переднего плана графиков статистики оптимизации this.m_chart_stat_0.SetBackColor(clrIvory); this.m_chart_stat_0.SetForeColor(C'200,200,200'); this.m_chart_stat_1.SetBackColor(clrIvory); this.m_chart_stat_1.SetForeColor(C'200,200,200'); this.m_chart_stat_2.SetBackColor(clrIvory); this.m_chart_stat_2.SetForeColor(C'200,200,200'); this.m_chart_stat_3.SetBackColor(clrIvory); this.m_chart_stat_3.SetForeColor(C'200,200,200'); this.m_chart_stat_4.SetBackColor(clrIvory); this.m_chart_stat_4.SetForeColor(C'200,200,200'); //--- Установим толщину и цвет выбранной линии лучшего прохода this.SetSelectedLineWidth(selected_line_width); this.SetSelectedLineColor(selected_line_color); //--- Нарисуем на вкладке 0 (Optimization) две таблицы с результатами оптимизации и входными параметрами, //--- и окно с полосой прогресса для вывода графиков и процесса оптимизации this.TableStatDraw(0, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(0, 4, this.m_table_stat_0.Y2()+4, CELL_W*2, CELL_H, 0, false); this.ChartOptDraw(0, this.m_completed, true); //--- Создадим на вкладке 0 кнопку воспроизведения оптимизации if(!this.m_chart_stat_0.CreateButtonReplay()) { Print("Button Replay creation failed"); return INIT_FAILED; } //--- Нарисуем на вкладке 1 (Sharpe Ratio) две таблицы с результатами оптимизации и входными параметрами, //--- и окно для вывода графиков результатов оптимизации this.TableStatDraw(1, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(1, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false); this.ChartOptDraw(1, this.m_completed, true); //--- Создадим на вкладке 1 кнопку выбора результата if(!this.m_chart_stat_1.CreateButtonResults()) { Print("Tab1: There were errors when creating the result buttons"); return INIT_FAILED; } //--- Нарисуем на вкладке 2 (Net Profit) две таблицы с результатами оптимизации и входными параметрами, //--- и окно для вывода графиков результатов оптимизации this.TableStatDraw(2, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(2, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false); this.ChartOptDraw(2, this.m_completed, true); //--- Создадим на вкладке 2 кнопку выбора результата if(!this.m_chart_stat_2.CreateButtonResults()) { Print("Tab2: There were errors when creating the result buttons"); return INIT_FAILED; } //--- Нарисуем на вкладке 3 (Profit Factor) две таблицы с результатами оптимизации и входными параметрами, //--- и окно для вывода графиков результатов оптимизации this.TableStatDraw(3, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(3, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false); this.ChartOptDraw(3, this.m_completed, true); //--- Создадим на вкладке 3 кнопку выбора результата if(!this.m_chart_stat_3.CreateButtonResults()) { Print("Tab3: There were errors when creating the result buttons"); return INIT_FAILED; } //--- Нарисуем на вкладке 4 (Recovery Factor) две таблицы с результатами оптимизации и входными параметрами, //--- и окно для вывода графиков результатов оптимизации this.TableStatDraw(4, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(4, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false); this.ChartOptDraw(4, this.m_completed, true); //--- Создадим на вкладке 4 кнопку выбора результата if(!this.m_chart_stat_4.CreateButtonResults()) { Print("Tab4: There were errors when creating the result buttons"); return INIT_FAILED; } return INIT_SUCCEEDED; }
Здесь создание всех элементов выполнено поблочно. Каждый блок кода отвечает за создание какого-либо элемента интерфейса программы.
По окончании оптимизации, необходимо внести некоторые изменения в созданный интерфейс — перекрасить заголовки графиков, поменять на них тексты и отобразить на первой вкладке (с идентификатором 0) кнопку запуска воспроизведения. Всё это нужно выполнить в обработчике OnTesterDeinit().
Обработчик OnTesterDeinit:
//+------------------------------------------------------------------+ //| Должна вызываться в обработчике эксперта OnTesterDeinit() | //+------------------------------------------------------------------+ void CFrameViewer::OnTesterDeinit(void) { //--- Получаем указатели на канвас для рисования фона и переднего плана CCanvas *background=this.m_tab_control.GetTabBackground(0); CCanvas *foreground=this.m_tab_control.GetTabForeground(0); if(background==NULL || foreground==NULL) return; //--- Устанавливаем флаг завершения оптимизации this.m_completed=true; //--- Координаты заголовка графика int x1=this.m_chart_stat_0.HeaderX1(); int y1=this.m_chart_stat_0.HeaderY1(); int x2=this.m_chart_stat_0.HeaderX2(); int y2=this.m_chart_stat_0.HeaderY2(); int x=(x1+x2)/2; int y=(y1+y2)/2; //--- Перекрасим фон и сотрём текст заголовка background.FillRectangle(x1, y1, x2, y2, ::ColorToARGB(clrLightGreen)); foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); //--- Изменим текст и цвет шапки заголовка string text="Optimization Complete: Click to Replay"; foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut(x, y, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); background.Update(false); foreground.Update(true); //--- Получаем индекс активной вкладки и вызываем метод управления отображением управляющих объектов на графиках оптимизации int tab_selected=this.m_tab_control.GetSelectedTabID(); this.ControlObjectsView(tab_selected); //--- На каждой вкладке (1 - 4) нарисуем графики трёх лучших проходов оптимизации this.DrawBestFrameDataAll(); ::ChartRedraw(); }
По завершении каждого прохода оптимизатора, генерируется событие Tester, которое можно обработать в обработчике OnTester(). Запускается он на стороне экземпляра эксперта, запущенного на агенте тестирования.
В этом обработчике необходимо собрать все данные о завершённом проходе, сформировать фрейм и отправить его в клиентский терминал при помощи функции FrameAdd().
Обработчик OnTester:
//+------------------------------------------------------------------+ //| Готовит массив значений баланса и отправляет его во фрейме | //| Должна вызываться в эксперте в обработчике OnTester() | //+------------------------------------------------------------------+ void CFrameViewer::OnTester(const double OnTesterValue) { //--- Переменные для работы с результатами прохода double balance[]; int data_count=0; double balance_current=::TesterStatistics(STAT_INITIAL_DEPOSIT); //--- Временные переменные для работы со сделками ulong ticket=0; double profit; string symbol; long entry; //--- Запросим всю торговую историю ::ResetLastError(); if(!::HistorySelect(0, ::TimeCurrent())) { PrintFormat("%s: HistorySelect() failed. Error ",__FUNCTION__, ::GetLastError()); return; } //--- Собираем данные о сделках uint deals_total=::HistoryDealsTotal(); for(uint i=0; i<deals_total; i++) { ticket=::HistoryDealGetTicket(i); if(ticket==0) continue; symbol=::HistoryDealGetString(ticket, DEAL_SYMBOL); entry =::HistoryDealGetInteger(ticket, DEAL_ENTRY); profit=::HistoryDealGetDouble(ticket, DEAL_PROFIT); if(entry!=DEAL_ENTRY_OUT && entry!=DEAL_ENTRY_INOUT) continue; balance_current+=profit; data_count++; ::ArrayResize(balance, data_count); balance[data_count-1]=balance_current; } //--- Массив data[] для отправки данных во фрейм double data[]; ::ArrayResize(data, ::ArraySize(balance)+DATA_COUNT); ::ArrayCopy(data, balance, DATA_COUNT, 0); //--- Заполним первые DATA_COUNT значений массива результатами тестирования data[0]=::TesterStatistics(STAT_SHARPE_RATIO); // коэффициент Шарпа data[1]=::TesterStatistics(STAT_PROFIT); // чистая прибыль data[2]=::TesterStatistics(STAT_PROFIT_FACTOR); // фактор прибыльности data[3]=::TesterStatistics(STAT_RECOVERY_FACTOR); // фактор восстановления data[4]=::TesterStatistics(STAT_TRADES); // количество трейдов data[5]=::TesterStatistics(STAT_DEALS); // количество сделок data[6]=::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // максимальная просадка средств в процентах data[7]=OnTesterValue; // значение пользовательского критерия оптимизации if(data[2]==DBL_MAX) data[2]=0; //--- Создадим фрейм с данными и отправим его в терминал if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME), FRAME_ID, deals_total, data)) ::PrintFormat("%s: Frame add error: ",__FUNCTION__, ::GetLastError()); }
При получении экспертом в клиентском терминале фрейма, отправленного из агента, генерируется событие TesterPass, которое обрабатывается в обработчике OnTesterPass().
В этом обработчике берём информацию из фрейма, рисуем на чарте график баланса этого прохода и заполняем таблицы результатов и параметров тестирования. Обработанный фрейм сохраняем в новом объекте-фрейме и складываем его в список фреймов для работы с ним когда потребуется искать нужные проходы для отображения их на графиках.
Обработчик OnTesterPass:
//+------------------------------------------------------------------+ //| Получает фрейм с данными при оптимизации и отображает график | //| Должна вызываться в эксперте в обработчике OnTesterPass() | //+------------------------------------------------------------------+ void CFrameViewer::OnTesterPass(void) { //--- Переменные для работы со фреймами string name; ulong pass; long id; double value, data[]; string params[]; uint par_count; //--- Вспомогательные переменные static datetime start=::TimeLocal(); static int frame_counter=0; //--- При получении нового фрейма получаем из него данные while(!::IsStopped() && ::FrameNext(pass, name, id, value, data)) { frame_counter++; string text=::StringFormat("Frames completed (tester passes): %d in %s", frame_counter,::TimeToString(::TimeLocal()-start, TIME_MINUTES|TIME_SECONDS)); //--- Получим входные параметры эксперта, для которых сформирован фрейм, и отправим их в таблицы и на график //--- При успешном получении фрейма запишем его данные в объект фрейм и разместим его в списке if(this.DrawFrameData(0, text, clrNONE, 0, pass, params, par_count, data)) { //--- Результаты прохода тестера double sharpe_ratio=data[0]; double net_profit=data[1]; double profit_factor=data[2]; double recovery_factor=data[3]; //--- Создаём новый объект фрейм и сохраняем его в списке CFrameData *frame=new CFrameData(pass, sharpe_ratio, net_profit, profit_factor, recovery_factor); if(frame!=NULL) { if(!this.AddFrame(frame)) delete frame; } ::ChartRedraw(); } } }
После завершения процесса оптимизации, в терминале на плавающем графике останется работать эксперт, запущенный во frame-режиме. И вся работа с этим экспертом будет организована внутри обработчика OnChartEvent(), так как мы будем управлять нужными нам процессами при помощи кнопок на графике и курсора мышки.
Обработчик OnChartEvent:
//+------------------------------------------------------------------+ //| Обработка событий на графике | //+------------------------------------------------------------------+ void CFrameViewer::OnChartEvent(const int id,const long &lparam, const double &dparam,const string &sparam, const int delay_ms) { //--- Вызываем обработчики событий объекта управления вкладками и графиков результатов оптимизации this.m_tab_control.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_0.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_1.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_2.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_3.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_4.OnChartEvent(id, lparam, dparam, sparam); //--- Если пришло событие изменения графика if(id==CHARTEVENT_CHART_CHANGE) { //--- получим размеры графика int w=(int)::ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int h=(int)::ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); if(w!=this.m_w || h!=this.m_h) { if(w==0 || h==0) return; //--- Изменим размер элемента управления Tab Control this.m_tab_control.Resize(w, h); //--- Получим идентификатор выбранной вкладки и нарисуем таблицы данных и график оптимизации на вкладке int tab_selected=this.m_tab_control.GetSelectedTabID(); this.DrawDataChart(tab_selected); //--- Получим указатель на кнопку-переключатель и выбранную кнопку показа результатов оптимизации CButtonSwitch *button_switch=(tab_selected>0 ? this.GetChartStats(tab_selected).ButtonResult() : NULL); uint res_index=(button_switch!=NULL ? button_switch.SelectedButton() : -1); //--- В зависимости от выбранной вкладки switch(tab_selected) { //--- вкладка 0 (Optimization) case 0 : //--- Рисуем график с линией последнего прохода и две пустые таблицы this.DrawDataChart(0); //--- Запускает воспроизведение проведённой оптимизации, //--- что останавливает работу с остальным пока длится воспроизведение //if(this.m_completed) // this.ReplayFrames(1); break; //--- вкладки 1 - 4 default: //--- Получаем индекс выбранной кнопки прохода оптимизации res_index=button_switch.SelectedButton(); //--- Рисуем график с результатами трёх лучших проходов выбранной вкладки this.DrawDataChart(tab_selected); this.DrawBestFrameData(tab_selected, -1); this.DrawBestFrameData(tab_selected, res_index); //--- На вкладке 0 рисуем график с линией последнего прохода и две пустые таблицы this.DrawDataChart(0); //--- Запускает воспроизведение проведённой оптимизации, //--- что останавливает работу с остальным пока длится воспроизведение //--- Чтобы заново нарисовать графики всех проходов, можно нажать кнопку воспроизведения //if(this.m_completed) // this.ReplayFrames(1); break; } //--- Запомним новые размеры для последующей проверки this.m_w=w; this.m_h=h; } } //--- Если процесс оптимизации не завершён - уходим if(!this.m_completed) return; //--- Если пришло пользовательское событие if(id>CHARTEVENT_CUSTOM) { //--- Если пришло событие кнопки Replay и оптимизация завершена if(sparam==this.m_chart_stat_0.ButtonReplay().Name() && this.m_completed) { //--- скроем кнопку Replay, this.m_chart_stat_0.ButtonReplay().Hide(); //--- Инициализируем график результатов оптимизации, this.ChartOptDraw(0, this.m_completed, true); //--- запустим воспроизведение, this.m_completed=false; // заблокируем, чтобы не запустить несколько раз подряд this.ReplayFrames(delay_ms); // процедура воспроизведения this.m_completed=true; // снимаем блокировку //--- После завершения воспроизведения покажем кнопку Replay и перерисуем график this.m_chart_stat_0.ButtonReplay().Show(); ::ChartRedraw(); } //--- Получаем указатели на кнопки вкладок CTabButton *tab_btn0=this.m_tab_control.GetTabButton(0); CTabButton *tab_btn1=this.m_tab_control.GetTabButton(1); CTabButton *tab_btn2=this.m_tab_control.GetTabButton(2); CTabButton *tab_btn3=this.m_tab_control.GetTabButton(3); CTabButton *tab_btn4=this.m_tab_control.GetTabButton(4); if(tab_btn0==NULL || tab_btn1==NULL || tab_btn2==NULL || tab_btn3==NULL || tab_btn4==NULL) return; //--- Получаем идентификатор выбранной вкладки int tab_selected=this.m_tab_control.GetSelectedTabID(); //--- Если пришло событие переключения на вкладку 0 if(sparam==tab_btn0.Name()) { //--- На вкладке 0 рисуем график с линией последнего прохода и две таблицы с пустыми результатами this.DrawDataChart(0); //--- Запускает воспроизведение проведённой оптимизации //--- (может долго длиться - при желании, чтобы отобразить графики, можно нажать кнопку Replay) //if(this.m_completed) // this.ReplayFrames(1); ::ChartRedraw(); return; } //--- Получаем указатель на чарт выбранной вкладки CStatChart *chart_stat=this.GetChartStats(tab_selected); if(tab_selected==0 || chart_stat==NULL) return; //--- Получаем указатели на кнопки чарта выбранной вкладки (индекс вкладки 1 - 4) CButtonTriggered *button_min=chart_stat.ButtonResultMin(); CButtonTriggered *button_mid=chart_stat.ButtonResultMid(); CButtonTriggered *button_max=chart_stat.ButtonResultMax(); if(button_min==NULL || button_mid==NULL || button_max==NULL) return; //--- Если пришло событие переключения на вкладку 1 if(sparam==tab_btn1.Name()) { //--- вызываем обработчик переключения на вкладку this.OnTabSwitchEvent(1); } //--- Если пришло событие переключения на вкладку 2 if(sparam==tab_btn2.Name()) { //--- вызываем обработчик переключения на вкладку this.OnTabSwitchEvent(2); } //--- Если пришло событие переключения на вкладку 3 if(sparam==tab_btn3.Name()) { //--- вызываем обработчик переключения на вкладку this.OnTabSwitchEvent(3); } //--- Если пришло событие переключения на вкладку 4 if(sparam==tab_btn4.Name()) { //--- вызываем обработчик переключения на вкладку this.OnTabSwitchEvent(4); } //--- Если пришло событие нажатие на кнопку минимального результата выбранной вкладки if(sparam==button_min.Name()) { //--- вызываем обработчик переключения кнопки-переключателя this.OnButtonSwitchEvent(tab_selected, 0); } //--- Если пришло событие нажатие на кнопку среднего результата выбранной вкладки if(sparam==button_mid.Name()) { //--- вызываем обработчик переключения кнопки-переключателя this.OnButtonSwitchEvent(tab_selected, 1); } //--- Если пришло событие нажатие на кнопку лучшего результата выбранной вкладки if(sparam==button_max.Name()) { //--- вызываем обработчик переключения кнопки-переключателя this.OnButtonSwitchEvent(tab_selected, 2); } } }
События переключения вкладок элемента управления вкладками и нажатия кнопок кнопки-переключателя обрабатываются в соответствующих пользовательских обработчиках. Все действия, выполняемые в них, идентичны. Разница только в идентификаторе вкладки. Поэтому эти события и оформлены для обработки в собственных обработчиках.
Обработчик переключения вкладки:
//+------------------------------------------------------------------+ //| Обработчик переключения вкладки | //+------------------------------------------------------------------+ void CFrameViewer::OnTabSwitchEvent(const int tab_id) { //--- Получаем указатель на чарт выбранной вкладки CStatChart *chart_stat=this.GetChartStats(tab_id); if(chart_stat==NULL) return; //--- Получаем указатель на кнопку-переключатель чарта выбранной вкладки CButtonSwitch *button_switch=chart_stat.ButtonResult(); if(button_switch==NULL) return; //--- Индекс нажатой кнопки uint butt_index=button_switch.SelectedButton(); //--- Инициализируем график результатов на вкладке tab_id и this.DrawDataChart(tab_id); //--- вызываем метод, контролирующий отображение управляющих элементов на всех вкладках this.ControlObjectsView(tab_id); //--- Рисуем все три лучших прохода this.DrawBestFrameData(tab_id, -1); //--- Выделяем проход, выбранный кнопкой this.DrawBestFrameData(tab_id, butt_index); }
Обработчик переключения кнопки-переключателя:
//+------------------------------------------------------------------+ //| Обработчик переключения кнопки-переключателя | //+------------------------------------------------------------------+ void CFrameViewer::OnButtonSwitchEvent(const int tab_id, const uint butt_id) { //--- Инициализируем график результатов на вкладке tab_id this.DrawDataChart(tab_id); //--- Рисуем все три лучших прохода this.DrawBestFrameData(tab_id, -1); //--- Выделяем проход, выбранный кнопкой butt_id this.DrawBestFrameData(tab_id, butt_id); }
Метод, рисующий таблицы данных и график оптимизации:
//+------------------------------------------------------------------+ //| Рисует таблицы данных и график оптимизации | //+------------------------------------------------------------------+ void CFrameViewer::DrawDataChart(const uint tab_id) { //--- Рисуем таблицу статистики, таблицу входных параметров и график оптимизации this.TableStatDraw(tab_id, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(tab_id, 4, this.GetTableStats(tab_id).Y2()+4, CELL_W*2, CELL_H, this.GetTableInputs(tab_id).RowsTotal(), false); this.ChartOptDraw(tab_id, this.m_completed, true); //--- вызываем метод, контролирующий отображение управляющих элементов на всех вкладках this.ControlObjectsView(tab_id); }
После того, как все таблицы и графики нарисованы, необходимо правильно расположить элементы управления. Кнопки на неактивных вкладках скрыть, а на активной отобразить, что и выполняет методControlObjectsView.
Метод, управляющий отображением контроллов на графиках оптимизации:
//+-------------------------------------------------------------------+ //|Управляет отображением управляющих объектов на графиках оптимизации| //+-------------------------------------------------------------------+ void CFrameViewer::ControlObjectsView(const uint tab_id) { //--- Получаем индекс активной вкладки int tab_index=this.m_tab_control.GetSelectedTabID(); //--- Получаем указатель на активную вкладку и таблицу статистики оптимизации CTab *tab=this.m_tab_control.GetTab(tab_index); CTableDataControl *table_stat=this.GetTableStats(tab_index); if(tab==NULL || table_stat==NULL) return; //--- Координаты левой и правой границ заголовка графика результатов оптимизации int w=0, cpx=0, x=0, y=0; int x1=table_stat.X2()+10; int x2=tab.GetField().Right()-10; //--- В зависимости от индекса выбранной вкладки switch(tab_index) { //--- Optimization case 0 : //--- Смещаем кнопку Replay к центру заголовка w=this.m_chart_stat_0.ButtonReplay().Width(); cpx=(x1+x2)/2; x=cpx-w/2; this.m_chart_stat_0.ButtonReplay().MoveX(x); //--- Если оптимизация завершена - показываем кнопку на переднем плане if(this.m_completed) { this.m_chart_stat_0.ButtonReplay().Show(); this.m_chart_stat_0.ButtonReplay().BringToTop(); } //--- Скрываем кнопки всех остальных вкладок this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; //--- Sharpe Ratio case 1 : //--- Скрываем кнопку Replay this.m_chart_stat_0.ButtonReplay().Hide(); //--- Получаем координату Y и смещаем на неё кнопку-переключатель y=this.m_chart_stat_1.ProgressBarY1()+CELL_H+2; this.m_chart_stat_1.ButtonResult().MoveY(y); //--- Кнопку-переключатель на вкладке 1 переносим на передний план, //--- а все остальные кнопки на других вкладках скрываем this.m_chart_stat_1.ButtonsResultBringToTop(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; //--- Net Profit case 2 : this.m_chart_stat_0.ButtonReplay().Hide(); //--- Получаем координату Y и смещаем на неё кнопку-переключатель y=this.m_chart_stat_2.ProgressBarY1()+CELL_H+2; this.m_chart_stat_2.ButtonResult().MoveY(y); //--- Кнопку-переключатель на вкладке 2 переносим на передний план, //--- а все остальные кнопки на других вкладках скрываем this.m_chart_stat_2.ButtonsResultBringToTop(); this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; //--- Profit Factor case 3 : this.m_chart_stat_0.ButtonReplay().Hide(); //--- Получаем координату Y и смещаем на неё кнопку-переключатель y=this.m_chart_stat_3.ProgressBarY1()+CELL_H+2; this.m_chart_stat_3.ButtonResult().MoveY(y); //--- Кнопку-переключатель на вкладке 3 переносим на передний план, //--- а все остальные кнопки на других вкладках скрываем this.m_chart_stat_3.ButtonsResultBringToTop(); this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; //--- Recovery Factor case 4 : this.m_chart_stat_0.ButtonReplay().Hide(); //--- Получаем координату Y и смещаем на неё кнопку-переключатель y=this.m_chart_stat_4.ProgressBarY1()+CELL_H+2; this.m_chart_stat_4.ButtonResult().MoveY(y); //--- Кнопку-переключатель на вкладке 4 переносим на передний план, //--- а все остальные кнопки на других вкладках скрываем this.m_chart_stat_4.ButtonsResultBringToTop(); this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); break; default: break; } //--- Перерисовываем график ::ChartRedraw(); }
Метод, выполняющий повторное проигрывание фреймов после окончания оптимизации:
//+------------------------------------------------------------------+ //| Повторное проигрывание фреймов после окончания оптимизации | //+------------------------------------------------------------------+ void CFrameViewer::ReplayFrames(const int delay_ms) { //--- Переменные для работы со фреймами string name; ulong pass; long id; double value, data[]; string params[]; uint par_count; //--- Счетчик фреймов int frame_counter=0; //--- Очистим счетчики прогресс-бара this.m_progress_bar.Reset(); this.m_progress_bar.Update(false); //--- Переводим указатель фреймов в начало и запускаем перебор фреймов ::FrameFirst(); while(!::IsStopped() && ::FrameNext(pass, name, id, value, data)) { //--- Увеличиваем счётчик фреймов и подготавливаем текст заголовка графика оптимизации frame_counter++; string text=::StringFormat("Playing with pause %d ms: frame %d", delay_ms, frame_counter); //--- Получаем входные параметры эксперта, для которых сформирован фрейм, данные фрейма и выводим их на график if(this.DrawFrameData(0, text, clrNONE, 0, pass, params, par_count, data)) ::ChartRedraw(); //--- Подождём delay_ms миллисекунд ::Sleep(delay_ms); } }
Все полученные фреймы после оптимизации доступны для их просмотра. Здесь в простом цикле от самого первого фрейма двигаемся по всем имеющимся фреймам и выводим их данные в таблицы и на график.
Метод, выводящий данные указанного фрейма на график оптимизации:
//+------------------------------------------------------------------+ //| Выводит данные указанного фрейма на график оптимизации | //+------------------------------------------------------------------+ bool CFrameViewer::DrawFrameDataByPass(const uint tab_id, const ulong pass_num, const string text, color clr, const uint line_width, double &data[]) { //--- Переменные для работы со фреймами string name; ulong pass; long id; uint par_count; double value; string params[]; //--- Переводим указатель фреймов в начало и запускаем поиск фрейма pass_num ::FrameFirst(); while(::FrameNext(pass, name, id, value, data)) { //--- Если номер прохода соответствует искомому - //--- получаем данные фрейма и выводим их в таблицу //--- и на график на вкладке tab_id if(pass==pass_num) { if(DrawFrameData(tab_id, text, clr, line_width, pass, params, par_count, data)) return true; } } //--- Проход не найден return false; }
Так как фреймы, доступные после оптимизации, могут быть получены только в цикле их перебора FrameFirst() --> FrameNext(), и стандартными методами никак более, то здесь мы в цикле перебираем все доступные фреймы в поисках того, номер прохода которого нам нужен. Как только нужный фрейм найден, его данные выводятся на график.
В принципе, у нас есть после оптимизации готовый список объектов-фреймов, и мы можем быстро получить из списка нужный объект. Такой доступ к нужному фрейму можно использовать, но в этом случае придётся писать ещё методы для получения данных из объекта-фрейма и массива серий, преобразования их в нужный формат и вывода на график. Но пока оставлен доступ именно в таком виде, какой представлен в методе выше, для уменьшения количества кода в классе и упрощения его понимания.
Метод, рисующий графики трёх лучших проходов по критерию оптимизации:
//+------------------------------------------------------------------+ //| Рисует графики трёх лучших проходов по критерию оптимизации | //+------------------------------------------------------------------+ void CFrameViewer::DrawBestFrameData(const uint tab_id, const int res_index) { //--- Если переданы некорректные идентификаторы таблицы и нажатой кнопки - уходим if(tab_id<1 || tab_id>4 || res_index>2) { ::PrintFormat("%s: Error. Incorrect table (%u) or selected button (%d) identifiers passed",__FUNCTION__, tab_id, res_index); return; } //--- Массивы для получения результатов проходов ulong array_passes[3]; double data[]; //--- Создаём текст заголовка графика проходов string res= ( tab_id==1 ? "Results by Sharpe Ratio" : tab_id==2 ? "Results by Net Profit" : tab_id==3 ? "Results by Profit Factor" : tab_id==4 ? "Results by Recovery Factor" : "" ); string text="Optimization Completed: "+res; //--- Заполняем массив array_passes индексами трёх лучших проходов this.FillArrayBestFrames(tab_id, array_passes); //--- Если индекс кнопки прохода задан отрицательным числом - if(res_index<0) { //--- выводим на график все три прохода //--- (цвет линий указывается как clrNONE для автоматического выбора цвета линий прибыльной или убыточной серий) for(int i=0; i<(int)array_passes.Size(); i++) this.DrawFrameDataByPass(tab_id, array_passes[i], text, clrNONE, 0, data); } //--- Иначе - выводим на график серию, указанную индексом нажатой кнопки (res_index), //--- цветом, заданным в m_selected_color, и толщиной, указанной в m_line_width else this.DrawFrameDataByPass(tab_id, array_passes[res_index], text, this.m_selected_color, this.m_line_width, data); }
Здесь сначала заполняется массив индексами фреймов трёх лучших проходов в методе FillArrayBestFrames(), и далее нужный проход (либо все три) выводятся на график.
Метод, заполняющий массив индексами фреймов трёх лучших проходов для указанного критерия оптимизации:
//+------------------------------------------------------------------+ //| Заполняет массив индексами фреймов трёх лучших проходов | //| для указанного критерия оптимизации (по индексу вкладки) | //+------------------------------------------------------------------+ bool CFrameViewer::FillArrayBestFrames(const uint tab_id, ulong &array_passes[]) { //--- Очищаем переданный в метод массив индексов проходов оптимизации ::ZeroMemory(array_passes); //FRAME_PROP_PASS_NUM, // Номер прохода //FRAME_PROP_SHARPE_RATIO, // Результат Sharpe Ratio //FRAME_PROP_NET_PROFIT, // Результат Net Profit //FRAME_PROP_PROFIT_FACTOR, // Результат Profit Factor //FRAME_PROP_RECOVERY_FACTOR, // Результат Recovery Factor //--- По идентификатору вкладки будем определять свойство, по которому искать лучшие проходы оптимизации //--- Проверяем идентификатор вкладки, чтобы был в пределах от 1 до 4 if(tab_id<FRAME_PROP_SHARPE_RATIO || tab_id>FRAME_PROP_RECOVERY_FACTOR) { ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id); return false; } //--- Преобразуем идентификатор таблицы в свойство фрейма ENUM_FRAME_PROP prop=(ENUM_FRAME_PROP)tab_id; //--- Сортируем список фреймов в порядке возрастания по свойству, //--- соответствующему значению tab_id в виде перечисления ENUM_FRAME_PROP this.m_list_frames.Sort(prop); //--- После сортировки, фрейм с лучшим результатом будет находиться в конце списка //--- Получаем по индексу фрейм из списка с максимальным значением результата и int index=this.m_list_frames.Total()-1; CFrameData *frame_next=this.m_list_frames.At(index); if(frame_next==NULL) return false; //--- записываем номер прохода в последнюю ячейку массива array_passes array_passes[2]=frame_next.Pass(); //--- Теперь найдём объекты, у которых результат оптимизации по убыванию меньше найденного максимального //--- В цикле от 1 до 0 (оставшиеся ячейки массива array_passes) for(int i=1; i>=0; i--) { //--- ищем предыдущий объект со значением свойства меньше, чем у объекта frame_next frame_next=this.FrameSearchLess(frame_next, prop); //--- В очередную ячейку массива array_passes вписываем номер прохода найденного объекта //--- Если объект не найден - значит, нет объектов со значением, меньше, чем у объекта frame_next, //--- и в очередную ячейку массива array_passes в этом случае записываем его предыдущее значение array_passes[i]=(frame_next!=NULL ? frame_next.Pass() : array_passes[i+1]); } //--- Всё успешно return true; }
Вся логика метода полностью расписана в комментариях к коду. По окончании работы метода в массиве, размером 3 будут записаны номера трёх лучших проходов по критерию оптимизации, соответствующему номеру вкладки, на график которой необходимо вывести данные этих проходов. Для поиска фреймов, у которых значение свойства меньше, чем у текущего, используется метод FrameSearchLess().
Метод для поиска и возврата указателя на объект фрейма со значением свойства меньше образца:
//+------------------------------------------------------------------+ //| Ищет и возвращает указатель на объект фрейма, | //| со значением свойства меньше образца | //+------------------------------------------------------------------+ CFrameData *CFrameViewer::FrameSearchLess(CFrameData *frame, const int mode) { //--- В зависимости от типа свойства фрейма switch(mode) { //--- во временный объект записываем соответствующее свойство переданного в метод объекта case FRAME_PROP_SHARPE_RATIO : this.m_frame_tmp.SetSharpeRatio(frame.SharpeRatio()); break; case FRAME_PROP_NET_PROFIT : this.m_frame_tmp.SetNetProfit(frame.NetProfit()); break; case FRAME_PROP_PROFIT_FACTOR : this.m_frame_tmp.SetProfitFactor(frame.ProfitFactor()); break; case FRAME_PROP_RECOVERY_FACTOR : this.m_frame_tmp.SetRecoveryFactor(frame.RecoveryFactor()); break; default : this.m_frame_tmp.SetPass(frame.Pass()); break; } //--- Сортируем массив фреймов по указанному свойству и this.m_list_frames.Sort(mode); //--- получаем индекс ближайшего объекта с меньшим значением свойства, либо -1 int index=this.m_list_frames.SearchLess(&this.m_frame_tmp); //--- Получаем из списка объект по индексу и возвращаем указатель на него, либо NULL CFrameData *obj=this.m_list_frames.At(index); return obj; }
В метод передаётся фрейм, и в сортированном списке фреймов при помощи метода SearchLess() класса CArrayObj Стандартной Библиотеки ищется ближайший объект, у которого значение свойства меньше, чем у переданного в метод.
Метод, выводящий на графики результатов оптимизации на каждой вкладке по три лучших прохода:
//+------------------------------------------------------------------+ //| Выводит на графики результатов оптимизации | //| на каждой вкладке по три лучших прохода | //+------------------------------------------------------------------+ void CFrameViewer::DrawBestFrameDataAll(void) { //--- В цикле по всем вкладкам от вкладки 1, рисуем графики трёх лучших проходов для каждой вкладки for(int i=1; i<this.m_tab_control.TabsTotal(); i++) this.DrawBestFrameData(i,-1); }
Метод для получения данных текущего фрейма и вывода их на указанной вкладке в таблицу и на график результатов оптимизации:
//+------------------------------------------------------------------+ //| Получение данных текущего фрейма и вывод их на указанной вкладке | //| в таблицу и на график результатов оптимизации | //+------------------------------------------------------------------+ bool CFrameViewer::DrawFrameData(const uint tab_id, const string text, color clr, const uint line_width, ulong &pass, string ¶ms[], uint &par_count, double &data[]) { //--- Проверяем переданный идентификатор вкладки if(tab_id>4) { ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id); return false; } //--- Получаем указатели на используемые объекты на указанной вкладке CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTableDataControl *table_stat=this.GetTableStats(tab_id); CTableDataControl *table_inp=this.GetTableInputs(tab_id); CStatChart *chart_stat=this.GetChartStats(tab_id); if(foreground==NULL || table_stat==NULL || table_inp==NULL || chart_stat==NULL) return false; //--- Получим входные параметры эксперта, для которых сформирован фрейм, данные фрейма и выведем их на график ::ResetLastError(); if(::FrameInputs(pass, params, par_count)) { //--- Нарисуем таблицу входных параметров на графике this.TableInpDraw(tab_id, 4, table_stat.Y2()+4, CELL_W*2, CELL_H, par_count, false); //--- Перебираем параметры, params[i], строка выглядит как "parameter=value" for(uint i=0; i<par_count; i++) { //--- Заполняем таблицу названиями и значениями входных параметров string array[]; //--- Расщепим строку в params[i] на две подстроки и обновим ячейки в строке таблицы параметров тестирования if(::StringSplit(params[i],'=',array)==2) { //--- Окрасим строки оптимизируемых параметров в бледно-желтый цвет, //--- недоступные для оптимизации параметры - в бледно-розовый, остальные - в цвета по умолчанию bool enable=false; double value=0, start=0, step=0, stop=0; color clr=clrMistyRose; if(::ParameterGetRange(array[0], enable, value, start, step, stop)) clr=(enable ? clrLightYellow : clrNONE); //--- Получим две ячейки таблицы по индексу параметра и выведем в них текст названия параметра и его значение CTableCell *cell_0=table_inp.GetCell(i, 0); CTableCell *cell_1=table_inp.GetCell(i, 1); if(cell_0!=NULL && cell_1!=NULL) { //--- Обновим надписи в ячейках cell_0.SetText(array[0]); cell_1.SetText(array[1]); cell_0.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER); cell_1.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER); } } } //--- Обновим таблицу статистики оптимизации //--- Строка заголовка таблицы foreground.FillRectangle(table_stat.X1()+1, 4+1, table_stat.X1()+CELL_W*2-1, 4+CELL_H-1, 0x00FFFFFF); foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut(4+(CELL_W*2)/2, 4+CELL_H/2, ::StringFormat("Optimization results (pass %I64u)", pass), ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); //--- В цикле по количеству строк таблицы int total=table_stat.RowsTotal(); for(int i=0; i<total; i++) { //--- получим две ячейки текущей строки и CTableCell *cell_0=table_stat.GetCell(i, 0); CTableCell *cell_1=table_stat.GetCell(i, 1); if(cell_0!=NULL && cell_1!=NULL) { //--- обновим значения результатов прохода во второй ячейке string text="---"; switch(i) { case 0 : text=::StringFormat("%.2f", data[0]); break; // Sharpe Ratio case 1 : text=::StringFormat("%.2f", data[1]); break; // Net Profit case 2 : text=::StringFormat("%.2f", data[2]); break; // Profit Factor case 3 : text=::StringFormat("%.2f", data[3]); break; // Recovery Factor case 4 : text=::StringFormat("%.0f", data[4]); break; // Trades case 5 : text=::StringFormat("%.0f", data[5]); break; // Deals case 6 : text=::StringFormat("%.2f%%", data[6]);break; // Equity DD case 7 : text=::StringFormat("%G", data[7]); break; // OnTester() default: break; } //--- Подсветим цветом фон строки таблицы, соответствующей выбранной вкладке. //--- Остальные строки будут иметь цвет по умолчанию color clr=(tab_id>0 ? (i==tab_id-1 ? C'223,242,231' : clrNONE) : clrNONE); //--- Обновим надписи в ячейках cell_0.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER); cell_1.SetText(text); cell_1.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER); } } //--- Массив для приема значений баланса текущего фрейма double seria[]; ::ArrayCopy(seria, data, 0, DATA_COUNT, ::ArraySize(data)-DATA_COUNT); //--- Отправим массив для вывода на специальных график баланса chart_stat.AddSeria(seria, data[1]>0); //--- Обновим линии баланса на графике chart_stat.Update(clr, line_width, false); //--- Обновим прогресс бар (только для вкладки с идентификатором 0) if(tab_id==0) this.m_progress_bar.AddResult(data[1]>0, false); //--- Обновим надпись на шапке графика int x1=chart_stat.HeaderX1(); int y1=chart_stat.HeaderY1(); int x2=chart_stat.HeaderX2(); int y2=chart_stat.HeaderY2(); int x=(x1+x2)/2; int y=(y1+y2)/2; foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut(x, y, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); foreground.Update(false); //--- Всё успешно return true; } //--- Что-то пошло не так... else PrintFormat("%s: FrameInputs() failed. Error %d",__FUNCTION__, ::GetLastError()); return false; }
В методе получаются данные из фрейма, все таблицы заполняются этими данными и рисуется график баланса этого прохода оптимизации.
Метод, рисующий таблицу статистики оптимизации на указанной вкладке:
//+------------------------------------------------------------------+ //| Рисует таблицу статистики оптимизации на указанной вкладке | //+------------------------------------------------------------------+ void CFrameViewer::TableStatDraw(const uint tab_id, const int x, const int y, const int w, const int h, const bool chart_redraw) { //--- Проверяем переданный идентификатор вкладки if(tab_id>4) { ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id); return; } //--- Получаем указатели на используемые объекты на указанной вкладке CCanvas *background=this.m_tab_control.GetTabBackground(tab_id); CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTableDataControl *table_stat=this.GetTableStats(tab_id); if(background==NULL || foreground==NULL || table_stat==NULL) return; //--- Рисуем заголовок таблицы результатов оптимизации background.FillRectangle(x, y, x+CELL_W*2, y+CELL_H, ::ColorToARGB(C'195,209,223')); // C'180,190,230' foreground.FillRectangle(x+1, y+1, x+CELL_W*2-1, y+CELL_H-1, 0x00FFFFFF); foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut(x+(CELL_W*2)/2, y+CELL_H/2, "Optimization results", ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); //--- Задаём таблице её идентификатор и рисуем сетку таблицы table_stat.SetID(TABLE_OPT_STAT_ID+10*tab_id); table_stat.DrawGrid(background, x, y+CELL_H, 0, DATA_COUNT, 2, CELL_H, CELL_W, C'200,200,200', false); //--- Нарисуем пустую таблицу результатов оптимизации - только заголовки, без значений //--- В цикле по строкам таблицы int total=table_stat.RowsTotal(); for(int row=0; row<total; row++) { //--- перебираем столбцы строк for(int col=0; col<2; col++) { //--- Получаем ячейку таблицы в текущей строке и столбце CTableCell *cell=table_stat.GetCell(row, col); //--- Определяем текст в ячейке //--- Для левой ячейки это будут заголовки результатов оптимизируемых параметров if(col%2==0) { string text="OnTester()"; switch(row) { case 0 : text="Sharpe Ratio"; break; case 1 : text="Net Profit"; break; case 2 : text="Profit Factor"; break; case 3 : text="Recovery Factor"; break; case 4 : text="Trades"; break; case 5 : text="Deals"; break; case 6 : text="Equity DD"; break; default: break; } cell.SetText(text); } //--- Для правой ячейки текст будет прочёркиванием для инициализируемой таблицы else cell.SetText(tab_id==0 ? " --- " : ""); //--- Выведем в ячейку соответствующий текст cell.TextOut(foreground, 4, CELL_H/2, clrNONE, 0, TA_VCENTER); } } //--- Обновим канвас фона и переднего плана background.Update(false); foreground.Update(chart_redraw); }
Метод рисует таблицу результатов оптимизации, заполняя только заголовки строк таблицы. Ячейки с данными вносятся в таблицу в методе, рассмотренном выше.
Метод, рисующий таблицу входных параметров оптимизации на указанной вкладке:
//+------------------------------------------------------------------+ //|Рисует таблицу входных параметров оптимизации на указанной вкладке| //+------------------------------------------------------------------+ void CFrameViewer::TableInpDraw(const uint tab_id, const int x, const int y, const int w, const int h, const uint rows, const bool chart_redraw) { //--- Проверяем переданный идентификатор вкладки if(tab_id>4) { ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id); return; } //--- Получаем указатели на используемые объекты на указанной вкладке CCanvas *background=this.m_tab_control.GetTabBackground(tab_id); CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTableDataControl *table_inp=this.GetTableInputs(tab_id); if(background==NULL || foreground==NULL || table_inp==NULL) return; //--- Рисуем заголовок таблицы параметров оптимизации background.FillRectangle(x, y, x+CELL_W*2, y+CELL_H, ::ColorToARGB(C'195,209,223')); foreground.FillRectangle(x+1, y+1, x+CELL_W*2-1, y+CELL_H-1, 0x00FFFFFF); foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut(x+(CELL_W*2)/2, y+CELL_H/2, "Input parameters", ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); //--- Задаём таблице её идентификатор и рисуем сетку таблицы table_inp.SetID(TABLE_OPT_INP_ID+10*tab_id); table_inp.DrawGrid(background, x, y+CELL_H, 0, rows, 2, CELL_H, CELL_W, C'200,200,200', false); //--- Обновим канвас фона и переднего плана background.Update(false); foreground.Update(chart_redraw); }
Этот метод так же, как и предыдущий, рисует пустую таблицу параметров оптимизации, которая заполняется данными в методе DrawFrameData(), где уже известны параметры, с которыми был проведён проход тестера.
Метод, рисующий график оптимизации на указанной вкладке:
//+------------------------------------------------------------------+ //| Рисует график оптимизации на указанной вкладке | //+------------------------------------------------------------------+ void CFrameViewer::ChartOptDraw(const uint tab_id, const bool opt_completed, const bool chart_redraw) { //--- Проверяем переданный идентификатор вкладки if(tab_id>4) { ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id); return; } //--- Получаем указатели на используемые объекты на указанной вкладке CCanvas *background=this.m_tab_control.GetTabBackground(tab_id); CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTab *tab=this.m_tab_control.GetTab(tab_id); CTableDataControl *table_stat=this.GetTableStats(tab_id); CStatChart *chart_stat=this.GetChartStats(tab_id); if(background==NULL || foreground==NULL || tab==NULL || table_stat==NULL || chart_stat==NULL) return; //--- Рассчитаем координаты четырёх углов графика результатов оптимизации int x1=table_stat.X2()+10; int y1=table_stat.Y1(); int x2=tab.GetField().Right()-10; int y2=tab.GetField().Bottom()-tab.GetButton().Height()-12; //--- Проверим ограничения размеров по минимальным ширине и высоте (480 x 180) int w_min=480; if(x2-x1<w_min) x2=x1+w_min; if(y2-y1<180) y2=y1+180; //--- Установим размеры ограничивающего прямоугольника графика результатов оптимизации chart_stat.SetChartBounds(x1, y1, x2, y2); //--- Цвет и текст заголовка графика color clr=clrLightGreen; // цвет заголовка при завершении оптимизации string suff= ( tab_id==1 ? "Results by Sharpe Ratio" : tab_id==2 ? "Results by Net Profit" : tab_id==3 ? "Results by Profit Factor" : tab_id==4 ? "Results by Recovery Factor" : "Click to Replay" ); string text="Optimization Completed: "+suff; //--- Если оптимизация не завершена, укажем цвет и текст заголовка if(!opt_completed) { clr=C'195,209,223'; text=::StringFormat("Optimization%sprogress%s", (tab_id==0 ? " " : " in "), (tab_id==0 ? "" : ": Waiting ... ")); } //--- Рисуем заголовок и текст background.FillRectangle(x1, 4, x2, y1, ::ColorToARGB(clr)); foreground.FillRectangle(x1, 4, x2, y2, 0x00FFFFFF); foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut((x1+x2)/2, 4+CELL_H/2, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); //--- Стираем полностью весь график результатов оптимизации background.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); //--- Обновляем график оптимизации chart_stat.Update(clrNONE, 0, chart_redraw); }
Метод подготавливает чистый график с заголовком, на который из методов рисования выводятся линии баланса завершённых проходов оптимизации.
Мы полностью написали все необходимые классы для визуальной оптимизации. Теперь файл класса CFrameViewer можно подключить к любому советнику для просмотра хода его оптимизации на отдельном графике в терминале.
Подключаем функционал к советнику
Давайте проверим, что у нас получилось.
Возьмём советник из стандартной поставки из расположения \MQL5\Experts\Advisors\ExpertMAMA.mq5 и сохраним его в новой, уже ранее созданной папке \MQL5\Experts\FrameViewer\ под именем ExpertMAMA_Frames.mq5.
Всё, что необходимо в него добавить — это подключить в конце листинга файл класса CFrameViewer, объявить объект с типом этого класса и дописать обработчики, в которых нужно вызывать одноимённые обработчики созданного класса.
Длину входных переменных советника можно немного сократить, убрав символы подчёркивания ("_") из имён переменных. Это даст им больше места, чтобы поместиться в ширину ячеек таблицы.
//+------------------------------------------------------------------+ //| ExpertMAMA.mq5 | //| Copyright 2000-2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Include | //+------------------------------------------------------------------+ #include <Expert\Expert.mqh> #include <Expert\Signal\SignalMA.mqh> #include <Expert\Trailing\TrailingMA.mqh> #include <Expert\Money\MoneyNone.mqh> //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ //--- inputs for expert input string InpExpertTitle = "ExpertMAMA"; int Expert_MagicNumber = 12003; bool Expert_EveryTick = false; //--- inputs for signal input int InpSignalMAPeriod = 12; input int InpSignalMAShift = 6; input ENUM_MA_METHOD InpSignalMAMethod = MODE_SMA; input ENUM_APPLIED_PRICE InpSignalMAApplied = PRICE_CLOSE; //--- inputs for trailing input int InpTrailingMAPeriod = 12; input int InpTrailingMAShift = 0; input ENUM_MA_METHOD InpTrailingMAMethod = MODE_SMA; input ENUM_APPLIED_PRICE InpTrailingMAApplied= PRICE_CLOSE; //+------------------------------------------------------------------+ //| Global expert object | //+------------------------------------------------------------------+ CExpert ExtExpert; //+------------------------------------------------------------------+ //| Initialization function of the expert | //+------------------------------------------------------------------+ int OnInit(void) { //--- Initializing expert if(!ExtExpert.Init(Symbol(),Period(),Expert_EveryTick,Expert_MagicNumber)) { //--- failed printf(__FUNCTION__+": error initializing expert"); ExtExpert.Deinit(); return(-1); } //--- Creation of signal object CSignalMA *signal=new CSignalMA; if(signal==NULL) { //--- failed printf(__FUNCTION__+": error creating signal"); ExtExpert.Deinit(); return(-2); } //--- Add signal to expert (will be deleted automatically)) if(!ExtExpert.InitSignal(signal)) { //--- failed printf(__FUNCTION__+": error initializing signal"); ExtExpert.Deinit(); return(-3); } //--- Set signal parameters signal.PeriodMA(InpSignalMAPeriod); signal.Shift(InpSignalMAShift); signal.Method(InpSignalMAMethod); signal.Applied(InpSignalMAApplied); //--- Check signal parameters if(!signal.ValidationSettings()) { //--- failed printf(__FUNCTION__+": error signal parameters"); ExtExpert.Deinit(); return(-4); } //--- Creation of trailing object CTrailingMA *trailing=new CTrailingMA; if(trailing==NULL) { //--- failed printf(__FUNCTION__+": error creating trailing"); ExtExpert.Deinit(); return(-5); } //--- Add trailing to expert (will be deleted automatically)) if(!ExtExpert.InitTrailing(trailing)) { //--- failed printf(__FUNCTION__+": error initializing trailing"); ExtExpert.Deinit(); return(-6); } //--- Set trailing parameters trailing.Period(InpTrailingMAPeriod); trailing.Shift(InpTrailingMAShift); trailing.Method(InpTrailingMAMethod); trailing.Applied(InpTrailingMAApplied); //--- Check trailing parameters if(!trailing.ValidationSettings()) { //--- failed printf(__FUNCTION__+": error trailing parameters"); ExtExpert.Deinit(); return(-7); } //--- Creation of money object CMoneyNone *money=new CMoneyNone; if(money==NULL) { //--- failed printf(__FUNCTION__+": error creating money"); ExtExpert.Deinit(); return(-8); } //--- Add money to expert (will be deleted automatically)) if(!ExtExpert.InitMoney(money)) { //--- failed printf(__FUNCTION__+": error initializing money"); ExtExpert.Deinit(); return(-9); } //--- Set money parameters //--- Check money parameters if(!money.ValidationSettings()) { //--- failed printf(__FUNCTION__+": error money parameters"); ExtExpert.Deinit(); return(-10); } //--- Tuning of all necessary indicators if(!ExtExpert.InitIndicators()) { //--- failed printf(__FUNCTION__+": error initializing indicators"); ExtExpert.Deinit(); return(-11); } //--- succeed return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Deinitialization function of the expert | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ExtExpert.Deinit(); } //+------------------------------------------------------------------+ //| Function-event handler "tick" | //+------------------------------------------------------------------+ void OnTick(void) { ExtExpert.OnTick(); } //+------------------------------------------------------------------+ //| Function-event handler "trade" | //+------------------------------------------------------------------+ void OnTrade(void) { ExtExpert.OnTrade(); } //+------------------------------------------------------------------+ //| Function-event handler "timer" | //+------------------------------------------------------------------+ void OnTimer(void) { ExtExpert.OnTimer(); } //+------------------------------------------------------------------+ //| Код, необходимый для визуализации оптимизации | //+------------------------------------------------------------------+ //--- При отладке, если во время оптимизации нажать "Стоп", то следующий запуск оптимизации продолжит незавершённые проходы с места остановки //--- Чтобы каждый новый запуск оптимизации начинался заново, определим директиву препроцессора #property tester_no_cache //--- Определим макроподстановки #define REPLAY_DELAY_MS 100 // Задержка воспроизведения оптимизации в миллисекундах #define STAT_LINES 1 // Количество отображаемых линий статистики оптимизации #define SELECTED_LINE_WD 3 // Толщина линии выбранного прохода оптимизации #define SELECTED_LINE_CLR clrDodgerBlue // Цвет линии выбранного прохода оптимизации //--- Подключим код для работы с результатами оптимизации просмотровщиком фреймов #include "FrameViewer.mqh" //--- Объявим объект просмотровщика фреймов CFrameViewer fw; //+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester() { //--- тут нужно вставить свою функцию для вычисления критерия оптимизации double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT)); TesterCritetia=TesterStatistics(STAT_PROFIT)>0?TesterCritetia:(-TesterCritetia); //--- вызываем на каждом окончании тестирования и передаем в качестве параметра критерий оптимизации fw.OnTester(TesterCritetia); //--- return(TesterCritetia); } //+------------------------------------------------------------------+ //| TesterInit function | //+------------------------------------------------------------------+ void OnTesterInit() { //--- подготавливаем график для отображения линий баланса //--- STAT_LINES задает количество линий баланса на графике, //--- SELECTED_LINE_WD - толщину, SELECTED_LINE_CLR - цвет линии выбранного прохода fw.OnTesterInit(STAT_LINES, SELECTED_LINE_WD, SELECTED_LINE_CLR); } //+------------------------------------------------------------------+ //| TesterDeinit function | //+------------------------------------------------------------------+ void OnTesterDeinit() { //--- завершение оптимизации fw.OnTesterDeinit(); } //+------------------------------------------------------------------+ //| TesterPass function | //+------------------------------------------------------------------+ void OnTesterPass() { //--- обрабатываем полученные результаты тестирования и выводим графику fw.OnTesterPass(); } //+------------------------------------------------------------------+ //| Обработка событий на графике | //+------------------------------------------------------------------+ void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- запускает воспроизведение фреймов по окончании оптимизации при нажатии на шапке fw.OnChartEvent(id,lparam,dparam,sparam,REPLAY_DELAY_MS); // REPLAY_DELAY_MS - пауза в ms между кадрами воспроизведения }
Это все изменения и дополнения в эксперте, которые нужно сделать (кроме укорачивания имён переменных), чтобы заработала визуальная оптимизация.
Давайте скомпилируем советник и запустим его на оптимизацию.
Настройки оптимизации для теста работы самой программы особо не важны, выставим их такими:

и запустим оптимизацию:

Перед началом процесса оптимизации открывается новое окно графика, на котором и расположены все элементы управления. Это удобно — чтобы не переключаться между прикреплёнными графиками результатов оптимизации и графиком визуальной оптимизации. Это отдельное окно можно сдвинуть за пределы терминала, или на второй монитор, и одновременно иметь доступ ко всем графикам оптимизации.
Заключение
В заключение хочется сказать, что мы рассмотрели лишь небольшой пример того, как можно сделать дополнительный функционал для контроля процесса оптимизации. На график визуальной оптимизации можно выводить любые данные, полученные из отчётов тестера, либо самостоятельно рассчитываемые после каждого прохода оптимизации. Какими могут быть функционал и визуальное отображение — здесь дело вкуса и потребностей каждого разработчика, использующего визуальную оптимизацию для получения нужных результатов и удобств в использовании полученных данных. Здесь, в данном моменте, важно, что мы посмотрели на конкретных примерах, как всё нужное для себя можно сделать и использовать.
К статье прикреплены все рассмотренные в статье файлы для самостоятельного изучения. В архиве Old_article_files.zip лежат файлы из статьи, на основе информации из которой всё сегодня и было сделано.
Также приложен архив MQL5.zip, распаковав который, можно сразу же получить установленные файлы для теста в нужных папках терминала.
Программы, используемые в статье:
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | Table.mqh | Библиотека классов | Библиотека классов для создания таблиц |
| 2 | Controls.mqh | Библиотека классов | Библиотека классов для создания графических элементов управления |
| 3 | FrameViewer.mqh | Библиотека классов | Библиотека классов для реализации в советнике функционала визуальной оптимизации |
| 4 | ExpertMAMA_Frames.mq5 | Советник | Советник для тестирования визуальной оптимизации |
| 5 | MQL5.zip | Архив | Архив файлов, представленных выше, для распаковки в каталог MQL5 клиентского терминала |
| 6 | Old_article_files.zip | Архив | Архив файлов из первоначальной статьи, на основе которых сделаны все файлы этой статьи |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 36): Q-обучение с цепями Маркова
Разработка системы репликации (Часть 71): Настройка времени (IV)
Разработка системы репликации (Часть 72): Неожиданный способ оповещений (I)
Построение модели для ограничения диапазона сигналов по тренду (Часть 8): Разработка советника (II)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
После раскрытия opt-формата использование фреймов осталось целесообразным только при передаче данных, которых нет в opt-файле.
На примере данной статьи можно было бы использовать предложенный GUI для визуализации opt-файла.
После раскрытия opt-формата использование фреймов осталось целесообразным только при передаче данных, которых нет в opt-файле.
На примере данной статьи можно было бы использовать предложенный GUI для визуализации opt-файла.