Разрабатываем мультивалютный советник (Часть 29): Доработка конвейера
Введение
В трех предыдущих частях мы немного отклонились в сторону от основной линии разработки, посвятив свои усилия развитию вспомогательных инструментов, которые нам так или иначе пригодятся в дальнейшем.
Например, в части 28 мы дополнили механизмы управления капиталом нашего мультивалютного советника, разработав и внедрив новый программный модуль — менеджер закрытия. Он позволил отслеживать общую прибыль или убыток относительно динамически обновляемого базового баланса и перезапускать работу всех торговых стратегий при достижении установленных значений. В дальнейшем было запланировано расширение его функциональности за счёт добавления возможностей трейлинга совокупной прибыли и перевода в безубыток совокупности открытых позиций.
В части 27 создали другой компонент — диалоговое окно на весь экран графика терминала, способное выводить многострочный текст с гибкими настройками шрифта и поддержкой прокрутки. Такой инструмент позволил сделать визуализацию информации более удобной и наглядной. Разработанный компонент был включен в состав библиотеки Adwizard, как средство вывода разнообразной информации о работе советников.
В целом, мы закрепили принятый ранее подход, состоящий в четком разделении всего программного кода на библиотечную часть (репозиторий Adwizard) и проектную часть или части (репозитории SimpleCandles и SymbolsInformer).
Сейчас давайте вернёмся к результатам, полученным в части 25. Там мы достигли важнейшего рубежа в разработке нашего мультивалютного советника, завершив в целом создание универсальной системы автоматической оптимизации и успешно подключив к ней новую торговую стратегию SimpleCandles. Весь процесс — от создания проекта оптимизации в базе данных и запуска многоэтапной оптимизации на кластере агентов, до генерации готового к работе итогового советника — был полностью автоматизирован.
Однако, за истекшее с момента публикации статьи время мы столкнулись с рядом сложностей или неудобств при использовании данной системы. В этом нет ничего удивительного, ведь это была лишь первая итерация разработки системы в целом. Поэтому рассмотрим некоторые улучшения, которые позволят более эффективно использовать автоматическую оптимизацию.
Намечаем путь
Вспомним суть предлагаемого конвейера по изготовлению итогового торгового советника. На первом этапе мы хотим провести в тестере стратегий MetaTrader 5 много процессов оптимизации одной торговой стратегии для разных символов, таймфремов и других параметров. Из полученных результатов оптимизаций мы выберем довольно много хороших для каждого символа (если они будут, конечно). Будем называть их одиночными экземплярами торговой стратегии.
На втором этапе проведём оптимизацию, выявляющую наилучшие группы из небольшого количества одиночных экземпляров торговых стратегий. То есть из тысяч экземпляров оставим группу только из 8 - 16 штук для каждого символа, показавшую лучшие результаты при совместной работе. И на третьем этапе эти лучшие группы объединим для загрузки и использования в итоговом советнике.
После длительного процесса разработки, все упомянутые действия были автоматизированы настолько, насколько возможно. Сейчас нам надо вручную указать параметры для формирования проекта оптимизации, то есть, по сути, некоторого общего сценария, по которому будет идти автоматическая оптимизация. И после её завершения понадобится совершить несколько манипуляций для запуска итогового советника на торговом счёте. Однако время, затрачиваемое сейчас на ручные операции (от нескольких минут), не идёт ни в какое сравнение со временем, в течение которого может работать конвейер автоматической оптимизации без необходимости вмешательства в процесс (часы, дни или даже недели). Вот с таким инструментом мы начали работать.
Первое неудобство проявилось, когда мы завершили обкатку подключения одной новой стратегии и решили перейти к другой новой стратегии. Выяснилось, что несмотря на заявленное разделение кода на две независимые части (библиотечную и проектные), некоторые связи между ними ещё сохранились. Посмотрим, где это встречается, и попробуем устранить.
Другая сложность проявилась в том, что длительность отдельных задач оптимизации, выполняемых в рамках конвейера проекта оптимизации, могла быть довольно большой. Она напрямую зависит от величины временного интервала, на котором проводятся все оптимизации. Если мы хотим провести оптимизацию на промежутке, например, в 5 лет, то этот процесс займет гораздо больше времени, по сравнению с оптимизацией на промежутке 3 месяца. Но как показала практика, в процессе генетической оптимизации, хорошие комбинации параметров стратегий могут встретиться гораздо раньше планового окончания этого процесса. Поэтому можно сократить общее время конвейера, если останавливать процессы оптимизации спустя какой-то выбранный промежуток времени. Добавим в нашу систему возможность указать предельное время для выполнения каждой задачи оптимизации.
Ну и напоследок, добавим немного удобства в наблюдение за процессом автоматической оптимизации, реализовав вывод более обширной информации от текущей задаче, чем просто её номер.
Для наглядности промоделируем по шагам весь процесс создания итогового советника, останавливаясь для внесения желаемых исправлений.
Создание базы данных
Для начала необходимо клонировать в папку MQL5/Shared Projects два репозитория: библиотечный репозиторий Adwizard и проектный репозиторий. Как можно клонировать на свой компьютер репозитории из хранилища MQL5 Algo Forge мы подробно рассказывали в этой статье.
В качестве проектного репозитория можно создать и свой собственный репозиторий, воспользовавшись репозиторием SimpleCandles как шаблоном. Как это сделать, мы рассматривали в статьях, посвящённых переходу на новую стратегию. Это понадобится, если вы хотите реализовать какую-то свою торговую стратегию. Мы же продолжим использовать уже созданную ранее стратегию SimpleCandles и одноимённый проектный репозиторий. В нём расположен проект для создания итогового мультивалютного советника, использующего эту торговую стратегию.
Когда оба репозитория есть в рабочей папке терминала по адресу MQL5/Shared Projects, скомпилируем файл советника создания проекта автоматической оптимизации SimpleCandles/Optimization/CreateProject.mq5 и перетащим появившийся в Навигаторе терминала скомпилированный вариант на любой график. В диалоге задания входных параметров переключимся на вкладку Inputs и увидим следующие:

В параметре файла базы данных оптимизации у нас указано название базы данных, которое мы использовали в предыдущих статьях. Поскольку мы планируем вносить некоторые изменения, затрагивающие, в том числе, и структуру базы данных оптимизации, то мы не можем продолжать использовать старую базу данных. Необходимо создать новую.
Делается это очень просто: достаточно в этом входном параметре указать другое желаемое имя базы данных. Если файла с таким именем не существует, то советник создания проекта автоматически создаст пустую базу данных с этим именем и нужной структурой таблиц. Чтобы при повторных запусках этого советника нам не приходилось менять установленное по умолчанию значение имени, изменим его на новое в исходном файле советника:
//+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ sinput group "::: База данных" sinput string fileName_ = "article.17607.db.sqlite"; // - Файл базы данных оптимизации sinput group "::: Параметры проекта - Основные" sinput string projectName_ = "SimpleCandles"; // - Название sinput string projectVersion_ = "1.00"; // - Версия sinput string symbols_ = "GBPUSD,EURUSD,EURGBP"; // - Символы sinput string timeframes_ = "H1,M30"; // - Таймфреймы ...
Значения других параметров тоже удобнее изменять в исходном коде этого советника, чем в диалоге ввода параметров при запуске советника. Это ответственный этап, на котором мы должны продумать общий сценарий конвейера автоматической оптимизации, зафиксировать его во входных параметрах советника создания проекта и потом запустить его один раз. На практике мы запускали его большее количество раз, так как сразу очень трудно полностью определиться по всем параметрам. Поэтому мы двигаемся итерациями: зафиксировали в исходном коде значения параметров, создали проект, запустили автоматическую оптимизацию и, если видим, что надо что-то поменять, то останавливаем процесс и возвращаемся к началу.
Что может пойти не так? Например, мы выбрали слишком большой временной промежуток для тестирования, и грубые прикидки позволяют сделать прогноз, что весь процесс займёт несколько недель. В этом случае, если мы не хотим столько ждать, можно попробовать уменьшить длительность временного промежутка тестирования или, например, уменьшить количество разных символов, для которых будут повторяться проходы оптимизации. Для этого мы вносим изменения во входные параметры и создаём проект заново. Можно даже предварительно удалить файл базы данных, чтобы вновь созданный проект был одним единственным в новой базе, а не добавился в уже существующую.
В целом, изменение значений по умолчанию этих параметров в проектной части допустимо и вполне обосновано: каждый проект требует индивидуальной настройки, которая в его рамках и выполняется.
Результатом этого шага является созданный файл базы данных с указанным именем в общей папке данных терминала. В созданной базе данных добавлена информация о сценарии проекта (в таблице projects) автоматической оптимизации, представляющая собой совокупность этапов (таблица stages). Этапы состоят из работ (таблица jobs), а в рамках одной работы может быть одна или несколько задач оптимизации (таблица tasks). Эти взаимосвязи показаны на рисунке стрелками:

Все задачи проекта находятся в статусе Queued, то есть поставлены в очередь на выполнение. Теперь можно переходить к следующему шагу — запуску конвейера автоматической оптимизации, в рамках которого все созданные задачи превратятся в отдельные запуски оптимизации советников этапов в тестере стратегий MetaTrader 5.
Запуск конвейера оптимизации
Для этого скомпилируем файл советника выполнения автоматической оптимизации SimpleCandles/Optimization/Optimization.mq5 и перетащим появившийся в Навигаторе терминала скомпилированный вариант на любой график. В диалоге задания входных параметров на вкладке Inputs мы увидим следующие:

Здесь мы снова видим, что в значениях параметров по умолчанию стоит название прошлого файла базы данных. Можно, конечно, вручную заменить его сейчас на новое название article.17607.db.sqlite, но практика показала, что это неудобно. Дело в том, что в процессе наладки конвейера нам вначале, скорее всего, придется сделать несколько тестовых запусков. И на каждом из них мы должны будем вручную менять название файла базы данных на актуальное.
Однако, если мы попробуем поступить так, как было описано выше, то есть поменять в коде значение названия, то выясним, что эти изменения надо вносить не в проектной части, а в библиотечной. В проектной части файл советника оптимизации (SimpleCandles/Optimization/Optimization.mq5) просто подключает библиотечный файл, чтобы при компиляции мы получали исполняемый файл в папке проекта:
#include "../Include/Adwizard/Experts/Optimization.mqh"
Больше в нём ничего нет, поэтому то значение, которое подставляется по умолчанию, находится в подключаемом библиотечном файле. Исправим это следующим образом: в проектном файле перед подключением библиотечного файла объявим константы с нужными значениями параметров по умолчанию и заодно исправим путь к подключаемому файлу, убрав название папки Include:
// Константы с параметрами по умолчанию для проекта: // - Файл с основной базой данных #define OPT_FILEMNAME "article.17607.db.sqlite" // - Путь к интерпретатору Python #define OPT_PYTHONPATH "C:\\Python\\Python312\\python.exe" #include "../../Adwizard/Experts/Optimization.mqh"
В библиотечной части (файле Adwizard/Experts/Optimization.mqh) предусмотрим, что эти константы могут не существовать. В этом случае объявим их с присвоением пустых значений. Далее используем их значения для подстановки во входные параметры:
// Создаём константы для параметров по умолчанию, // если они не определены в проектной части #ifndef OPT_FILEMNAME #define OPT_FILEMNAME "" #endif #ifndef OPT_PYTHONPATH #define OPT_PYTHONPATH "" #endif sinput string fileName_ = OPT_FILEMNAME; // - Файл с основной базой данных sinput string pythonPath_ = OPT_PYTHONPATH; // - Путь к интерпретатору Python
Теперь эти параметры мы можем указывать в коде проектной части, не трогая библиотечную. То есть она будет браться одинаковой для разных проектов.
Ограничение времени выполнения задач
Как уже было сказано, другая сложность проявилась в том, что длительность отдельных задач оптимизации, выполняемых в рамках конвейера проекта оптимизации, могла быть довольно большой. Для добавления ограничения на время выполнения одной задачи оптимизации нам потребуется внести изменения в нескольких местах.
Во-первых, в файле советника создания проекта надо добавить входные параметры, через которые мы могли бы указать желаемое максимальное время выполнения задач. Эти изменения касаются проектной части. Во-вторых, в базе данных в таблице tasks надо добавить поле для хранения этого значения. В-третьих, в коде советника оптимизации надо обеспечить поддержку использования значения максимального времени выполнения. Эти изменения будут выполняться в библиотечной части.
Добавим два параметра в файле SimpleCandles/Optimization/CreateProject.mq5 для указания максимального времени выполнения задач оптимизации на первом и втором этапе.
//+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ sinput group "::: База данных" sinput string fileName_ = "article.17607.db.sqlite"; // - Файл базы данных оптимизации sinput group "::: Параметры проекта - Основные" sinput string projectName_ = "SimpleCandles"; // - Название sinput string projectVersion_ = "1.00"; // - Версия sinput string symbols_ = "GBPUSD,EURUSD,EURGBP"; // - Символы sinput string timeframes_ = "H1,M30"; // - Таймфреймы sinput group "::: Параметры проекта - Интервал оптимизации" sinput datetime fromDate_ = D'2023-09-01'; // - Дата начала sinput datetime toDate_ = D'2024-01-01'; // - Дата окончания sinput group "::: Параметры проекта - Счёт" sinput string mainSymbol_ = "GBPUSD"; // - Основной символ sinput int deposit_ = 10000; // - Начальный депозит sinput group "::: Этап 1. Поиск" sinput string stage1ExpertName_ = "Stage1.ex5"; // - Советник этапа sinput string stage1Criterions_ = "6,6,6"; // - Критерии оптимизации для задач sinput long stage1MaxDuration_ = 20; // - Макс. продолж. задач (с) sinput group "::: Этап 2. Группировка" sinput string stage2ExpertName_ = "Stage2.ex5"; // - Советник этапа sinput string stage2Criterion_ = "6"; // - Критерий оптимизации для задач sinput long stage2MaxDuration_ = 20; // - Макс. продолж. задач (с) //sinput bool stage2UseClusters_= false; // - Использовать кластеризацию? sinput double stage2MinCustomOntester_ = 500; // - Мин. значение норм. прибыли sinput uint stage2MinTrades_ = 20; // - Мин. кол-во сделок sinput double stage2MinSharpeRatio_ = 0.7; // - Мин. коэфф. Шарпа sinput uint stage2Count_ = 8; // - Кол-во стратегий в группе (1 - 16) sinput group "::: Этап 3. Итог" sinput string stage3ExpertName_ = "Stage3.ex5"; // - Советник этапа sinput ulong stage3Magic_ = 27183; // - Magic sinput bool stage3Tester_ = true; // - Для тестера?
Для третьего этапа указать максимальное время мы не можем, так как на нём используется не оптимизация, а одиночный проход тестера стратегий. Только по его завершению рассчитываются нормированные объёмы открываемых позиций и формируется строка инициализации итогового советника. Если мы прервём этот проход раньше, то не сможем получить его результаты. Для первого и второго этапа более раннее прерывание процесса оптимизации приведёт только к уменьшению общего числа выполненных проходов. Если их всё равно окажется достаточно много, то в этом нет ничего страшного.
Для добавления нового поля в структуру таблицы tasks, нам достаточно дополнить один SQL-запрос в файле схемы базы данных оптимизации db.opt.schema.sql:
-- Table: tasks DROP TABLE IF EXISTS tasks; CREATE TABLE tasks ( id_task INTEGER PRIMARY KEY AUTOINCREMENT, id_job INTEGER NOT NULL REFERENCES jobs (id_job) ON DELETE CASCADE ON UPDATE CASCADE, optimization_criterion INTEGER DEFAULT (7) NOT NULL, start_date DATETIME, finish_date DATETIME, max_duration INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT Queued CHECK (status IN ('Queued', 'Process', 'Done') ) );
Теперь приступим к правкам библиотечной части. В основном, нам потребуется внести изменения в два файла. Сначала в файле класса задачи оптимизации Adwizard/Optimization/OptimizerTask.mqh добавим в состав структуры чтения параметров задачи из базы данных дополнительное поле для максимальной длительности:
//+------------------------------------------------------------------+ //| Класс для задачи оптимизации | //+------------------------------------------------------------------+ class COptimizerTask { protected: // ... public: // Структура данных для чтения одной строки результата запроса struct params { string expert; int optimization; string from_date; string to_date; int forward_mode; string forward_date; double deposit; string symbol; string period; string tester_inputs; ulong id_task; int optimization_criterion; long max_duration; } m_params; // ... };
Добавляем его и в запросе на получения из базы данных информации по задаче:
//+------------------------------------------------------------------+ //| Получение очередной задачи оптимизации из очереди | //+------------------------------------------------------------------+ void COptimizerTask::Load(ulong p_id) { // Запоминаем идентификатор задачи m_id = p_id; // Запрос на получение задачи оптимизации из очереди по идентификатору string query = StringFormat( "SELECT s.expert," " s.optimization," " s.from_date," " s.to_date," " s.forward_mode," " s.forward_date," " s.deposit," " j.symbol," " j.period," " j.tester_inputs," " t.id_task," " t.optimization_criterion," " t.max_duration" " FROM tasks t" " JOIN" " jobs j ON t.id_job = j.id_job" " JOIN" " stages s ON j.id_stage = s.id_stage" " WHERE t.id_task=%I64u;", m_id); // Открываем базу данных if(DB::Connect(m_fileName)) { // Выполняем запрос int request = DatabasePrepare(DB::Id(), query); // ... } }
В методе проверки, что задача завершена, мы добавим блок кода, который в случае указанного ненулевого значения для максимальной длительности выполнения задачи, будет получать из базы данных прошедшее время с начала выполнения задачи и сравнивать с заданным максимальным. Если текущая длительность уже превысила максимальную, вызывается метод принудительной остановки задачи:
//+------------------------------------------------------------------+ //| Задача выполнена? | //+------------------------------------------------------------------+ bool COptimizerTask::IsDone() { // Если нет текущей задачи, то всё выполнено if(m_id == 0) { return true; } // Результат bool res = false; // Если это задача на оптимизацию советника if(m_type == TASK_TYPE_EX5) { // Проверяем, завершил ли работу тестер стратегий res |= MTTESTER::IsReady(); // Если тестер работает и указана максимальная длительность, то if(!res && m_params.max_duration > 0) { // Запрос на получение прошедшего времени выполнения текущей задачи string query = StringFormat("SELECT unixepoch(datetime()) - unixepoch(start_date) AS duration" " FROM tasks" " WHERE id_task=%I64u;", m_id); // Получаем время выполнения в секундах DB::Connect(m_fileName); long duration = StringToInteger(DB::GetValue(query)); DB::Close(); // Если время выполнения больше максимально допустимого, то if(duration > m_params.max_duration) { // Останавливаем задачу Stop(); } } // Если это задача на запуск программы на Python, то } else if(m_type == TASK_TYPE_PY) { // ... } } else { res = true; } return res; }
В файле Adwizard/Optimization/OptimizationProject.mqh мы добавим в методы создания задач передачу параметра для указания максимальной длительности:
//+------------------------------------------------------------------+ //| Класс для проекта оптимизации | //+------------------------------------------------------------------+ class COptimizationProject { public: // ... // Добавление новых задач в базу данных для заданных критериев оптимизации COptimizationProject* AddTasks(string p_criterions, long p_maxDuration = 0); COptimizationProject* AddTasks(string &p_criterions[], long p_maxDuration = 0); // ... }; //+------------------------------------------------------------------+ //| Добавление новых задач в базу данных для заданных | //| критериев оптимизации в одной строке | //+------------------------------------------------------------------+ COptimizationProject* COptimizationProject::AddTasks(string p_criterions, long p_maxDuration) { // Массив для критериев оптимизации string criterions[]; StringReplace(p_criterions, ";", ","); StringSplit(p_criterions, ',', criterions); return AddTasks(criterions, p_maxDuration); } //+------------------------------------------------------------------+ //| Добавление новых задач в базу данных для заданных | //| критериев оптимизации в массиве | //+------------------------------------------------------------------+ COptimizationProject* COptimizationProject::AddTasks(string &p_criterions[], long p_maxDuration) { // Для каждой работы текущего этапа FOREACH_AS(m_stage.jobs, m_job) { // Для каждого критерия оптимизации FOREACH(p_criterions) { // Создаём новый объект задачи для данной работы m_task = new COptimizationTask(0, m_job, (int) p_criterions[i], p_maxDuration); // Вставляем его в базу данных оптимизации m_task.Insert(); // Добавляем его в массив всех задач APPEND(m_tasks, m_task); // Добавляем его в массив задач текущей работы APPEND(m_job.tasks, m_task); } } return &this; }
В файле Adwizard/Optimization/OptimizationTask.mqh нам остаётся добавить дополнительное поле в класс COptimizationTask и обеспечить его инициализацию в конструкторе и в методе вставки данных в таблицу tasks в базе данных:
//+------------------------------------------------------------------+ //| Класс для задачи оптимизации | //+------------------------------------------------------------------+ class COptimizationTask { public: ulong id_task; // ID задачи ulong id_job; // ID работы int optimization; // Критерий оптимизации long maxDuration; // Макс. продолжительность string status; // Статус задачи COptimizationJob* job; // Работа, для которй будет запускаться данная задача // Конструктор COptimizationTask(ulong p_taskId = 0, COptimizationJob* p_job = NULL, int p_optimization = 6, long p_maxDuration = 0, string p_status = "Done"); // Создание задачи в базе данных void Insert(); }; //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ COptimizationTask::COptimizationTask(ulong p_taskId = 0, COptimizationJob* p_job = NULL, int p_optimization = 6, long p_maxDuration = 0, string p_status = "Done") : id_task(p_taskId), job(p_job), id_job(!!p_job ? p_job.id_job : 0), optimization(p_optimization), maxDuration(p_maxDuration), status(p_status) {} //+------------------------------------------------------------------+ //| Создание задачи в базе данных | //+------------------------------------------------------------------+ void COptimizationTask::Insert() { string query = StringFormat("INSERT INTO tasks " " VALUES (NULL,%I64u,%d,NULL,NULL,%I64d,'%s');", id_job, optimization, maxDuration, status); id_task = DB::Insert(query); PrintFormat(__FUNCTION__" | %s -> %I64u", query, id_task); } //+------------------------------------------------------------------+
Теперь мы можем скомпилировать два советника, с помощью которых можно создавать проекты оптимизации в базе данных с указанием максимального возможного времени выполнения задач оптимизации на первых двух этапах конвейера (CreateProject.ex5) и запускать конвейер на выполнение (Optimization.ex5).
Вывод информации в процессе оптимизации
Поскольку раньше нам было более важным обеспечить корректную работу советника конвейера автоматической оптимизации, то вопрос вывода информации о ходе этого процесса был второстепенным. Мы воспользовались стандартной функцией Comment(), которая позволяет вывести мелкий текст в левом верхнем углу графика с запущенным советником. Всё, что выводилось — это идентификатор текущей задачи оптимизации. Теперь выполнение основной работы советника оптимизации более-менее налажено, и мы можем немного заняться менее важными вещами. К тому же, у нас теперь есть готовый компонент для более гибкого вывода текста на график советника — класс CConsoleDialog. Напомним, что объект этого класса позволяет создать развёрнутое на весь график диалоговое окно с кнопками сворачивания и закрытия, выводящее прокручиваемый и масштабируемый многострочный текст. Давайте им воспользуемся.
Чтобы это сделать, надо добавить следующее. В подключаемом библиотечном файле советника оптимизации (Adwizard/Experts/Optimization.mqh) надо создать глобальный указатель на объект этого класса, а в функции инициализации создать сам объект диалога и вызвать метод его запуска:
CConsoleDialog *dialog; // Диалог для вывода текста с информацией //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Если файл базы данных не указан, то выходим if(fileName_ == "") { PrintFormat(__FUNCTION__" | ERROR: Set const OPT_FILEMNAME with filename of DB in project", 0); return INIT_FAILED; } // Создаём оптимизатор optimizer = new COptimizer(fileName_, pythonPath_); // Создаём и запускаем диалог для вывода информации dialog = new CConsoleDialog(); dialog.Create(__FILE__); dialog.Run(); // Создаём таймер и запускаем его обработчик EventSetTimer(2); OnTimer(); return(INIT_SUCCEEDED); }
В обработчике таймера добавим передачу нового текста объекту диалога, получаемого от объекта оптимизатора:
//+------------------------------------------------------------------+ //| Expert timer function | //+------------------------------------------------------------------+ void OnTimer() { // Запускаем обработку оптимизатора optimizer.Process(); dialog.Text(optimizer.Text()); }
Добавим функцию обработки событий графика OnChartEvent(), передающую события объекту диалога, чтобы обеспечить взаимодействие с пользователем:
//+------------------------------------------------------------------+ //| Обработка событий | //+------------------------------------------------------------------+ void OnChartEvent(const int id, // event ID const long & lparam, // event parameter of the long type const double & dparam, // event parameter of the double type const string & sparam) { // event parameter of the string type if(!!dialog && !IsStopped()) { dialog.ChartEvent(id, lparam, dparam, sparam); } }
И не забудем удалить созданный объект диалога при завершении работы советника и перерисовать график:
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { PrintFormat(__FUNCTION__" | Reason: %d", reason); EventKillTimer(); // Удаляем оптимизатор if(!!optimizer) { delete optimizer; } // Удаляем диалог if(!!dialog) { dialog.Destroy(); delete dialog; ChartRedraw(); } }
Теперь нам надо сформировать желаемый текст, который будет выводиться на экран в процессе оптимизации. Реализуем это в методе Text() объекта оптимизатора в файле Adwizard/Optimization/Optimizer.mqh:
//+------------------------------------------------------------------+ //| Информация от текущем состоянии оптимизации | //+------------------------------------------------------------------+ string COptimizer::Text(void) { string text = ""; // Получим количество проектов с разными статусами DB::Connect(m_fileName); int process_projects_count = (int) DB::GetValue("SELECT count(status) FROM projects WHERE status = 'Process'"); int queued_projects_count = (int) DB::GetValue("SELECT count(status) FROM projects WHERE status = 'Queued'"); int done_projects_count = (int) DB::GetValue("SELECT count(status) FROM projects WHERE status = 'Done'"); int total_projects_count = process_projects_count + queued_projects_count + done_projects_count; DB::Close(); // Добавим это в текст сообщения text += StringFormat("DB: %s | %d Projects (Process: %d, Queued: %d, Done: %d)\n", m_fileName, total_projects_count, process_projects_count, queued_projects_count, done_projects_count ); // Если есть активный проект if(process_projects_count > 0) { // Добавим в текст сообщения информацию о текущей задачи text += m_task.Text(); // И общее количество задач в очереди if(m_totalTasks) text += StringFormat( "Total tasks in queue: %d\n", m_totalTasks); } return text; }
Информацию о текущей задаче мы будем получать, вызывая метод Text() класса COptimizerTask. Добавим в файле Adwizard/Optimization/OptimizerTask.mqh в этот метод получение информации о проекте, этапе и работе, к которым относится текущая задача оптимизации. Также вычислим время, прошедшее с момента запуска задачи, и оставшееся время до завершения, если указано максимальное допустимое время выполнения задачи:
//+------------------------------------------------------------------+ //| Информация о текущей задаче | //+------------------------------------------------------------------+ string COptimizerTask::Text() { string text = ""; // Если есть активная задача if(m_params.id_task) { DB::Connect(m_fileName); // Добавляем информацию о проекте text += StringFormat("═════════════════════════════════════════════════════════════════════════\n" "PROJECT: %s v. %s\n%s\n\n", DB::GetValue("SELECT name FROM projects WHERE status = 'Process' LIMIT 1"), DB::GetValue("SELECT version FROM projects WHERE status = 'Process' LIMIT 1"), DB::GetValue("SELECT description FROM projects WHERE status = 'Process' LIMIT 1") ); // Запрос на получение всей информации о задаче string query = "SELECT s.name, s.expert, s.from_date, s.to_date, " " j.symbol, j.period, t.optimization_criterion, t.start_date, " " time(max_duration, 'unixepoch') AS max_duration," " time(unixepoch('now') - unixepoch(t.start_date), 'unixepoch') AS elapsed_time," " time(MAX(0, max_duration - (unixepoch('now') - unixepoch(t.start_date))), 'unixepoch') AS remaining_time" " FROM stages s" " JOIN" " projects p ON s.id_project = p.id_project AND" " p.status = 'Process' AND" " s.expert IS NOT NULL" " JOIN jobs j ON j.id_stage = s.id_stage" " JOIN tasks t ON t.id_job = j.id_job AND t.status = 'Process';"; // Выполняем запрос int request = DatabasePrepare(DB::Id(), query); struct Row { string stage_name; string expert_name; string from_date; string to_date; string symbol; string timeframe; int optimization_criterion; string start_date; string max_duration; string elapsed_time; string remainig_time; } row; // Если нет ошибки if(request != INVALID_HANDLE) { // Читаем данные из первой строки результата и добавляем в текст if(DatabaseReadBind(request, row)) { text += StringFormat("TASK #%I64u:\n" " %10.10s │ %14.14s │ %-23s │ %6.6s │ %-3.3s │ %15.15s │ %-10.10s │ %-10.10s\n" "────────────┼────────────────┼─────────────────────────┼────────┼─────┼─────────────────┼────────────┼─────────────\n" " %10.10s │ %14.14s │ %s - %s │ %6.6s │ %-3.3s │ %15.15s │ %-10.10s │ %-10.10s \n\n" "═════════════════════════════════════════════════════════════════════════\n", m_id, "Stage", "Expert", "Testing period", "Symbol", "TF", "Criterion", "Max Durat.", "Remaining", row.stage_name, row.expert_name, row.from_date, row.to_date, row.symbol, row.timeframe, s_criterionNames[row.optimization_criterion], m_params.max_duration ? row.max_duration : "Unlimited", row.remainig_time ); } } DatabaseFinalize(request); DB::Close(); } return text; }
Полученные данные о задаче расположим в виде текстовой таблицы. На этом правки для обеспечения этой функциональности закончены, и мы можем переходить к тестированию.
Тестирование
Запустим советник оптимизации SimpleCandles/Optimization/Optimization.ex5 и увидим примерно следующее:

Для этого снимка экрана мы установили максимальное допустимое время выполнения одной задачи оптимизации равным 3 минутам (180 секунд). Мы будем проводить оптимизации на трёх символах и двух таймфреймах на временном промежутке длиной 4 месяца.
На первом этапе для каждой комбинации символ-таймфрейм мы будем 3 раза запускать процесс оптимизации. Поэтому всего нам понадобится выполнить на первом этапе 2 символа * 3 таймфрейма * 3 оптимизации = 18 задач.
На втором этапе мы будем выполнять по одному запуску оптимизации для подбора хорошей группы для каждой комбинации символа-таймфрейм. Таким образом, на втором этапе понадобится выполнить 2 символа * 3 таймфрейма = 6 задач.
На третьем этапе выполняется одна финальная задача, поэтому общее количество задач равно 18 + 6 + 1 = 25. Для всех, кроме последней, мы задали максимальное время. Если грубо оценить время выполнения последней задачи тоже в 3 минуты, то можно прикинуть, сколько времени займёт весь процесс конвейера автоматической оптимизации: 25 * 3 = 75 минут = 1 час 15 минут. Это, конечно, намного меньше дней или недель. Но давайте попробуем ещё более экстремальный вариант.
Ограничим максимальное время выполнения всего 20 секундами. В этом случае полное время процесса автоматической оптимизации ещё уменьшится и составит около 25 * 20 секунд = 500 секунд = 8 минут 20 секунд.
Спустя это небольшое время, все задачи оптимизации оказываются выполненными. В терминале MetaTrader 5, где мы запускали этот процесс, мы можем увидеть результаты задачи последнего третьего этапа, на котором был сделан одиночный проход советника, объединяющего 2 символа * 3 таймфрейма * 8 экземпляров в группе = 48 одиночных экземпляров торговой стратегии SimpleCandles:


Эти результаты пока не нормированы, то есть, просадка на протяжении этих четырёх месяцев составила менее $500 при максимальной ожидаемой $1000 (10%). Поэтому можно увеличить размер открываемых позиций примерно в 2 раза и на этом промежутке не выйти за пределы 10% просадки. Такая операция уже сделана в конце третьего этапа и в сформированную отдельную базу данных итогового советника уже сохранена информация о размерах позиций с учётом этой нормировки.
Также на третьем этапе не применялся ни риск-менеджер закрытия, ни менеджер закрытия.
По принятому ранее соглашению, имя файла базы данных итогового советника формируется по строго определённому алгоритму из названия проекта, указанного магического номера в параметрах проекта и суффикса ".test". В нашем конкретном случае после завершения процесса автоматической оптимизации в общей папке данных терминала был создан файл с именем SimpleCandles-27183.test.db.sqlite.
Посмотрим на содержимое базы данных итогового советника. В ней есть четыре таблицы, но нас сейчас интересуют только две последних:

В таблице strategy_groups у нас есть одна запись для сформированной группы стратегий с идентификатором id_group=1. Если мы будем запускать повторно конвейер автоматической оптимизации этого же проекта, то в эту таблицу будут добавляться новые строки с другими идентификаторами. Значение идентификатора группы стратегий нам нужно для указания его в параметрах итогового советника. В таблице strategies у нас добавились одиночные экземпляры торговых стратегий, относящихся к определённой группе стратегий. Остальные таблицы будут использоваться итоговым советником уже только в процессе работы на торговом счёте.
Скомпилируем файл итогового советника SimpleCandles/SimpleCandles.mq5 и запустим его тестирование с имеющимся идентификатором группы стратегий, без автообновления, отключенными риск-менеджером, менеджером закрытия и магическим номером 27183:

Получим следующие результаты:


Теперь просадка уже приближается к максимальной ожидаемой, заданной в настройках итогового советника, а прибыль соответственно, увеличилась пропорционально увеличению размеров позиций примерно в два раза.
Заключение
Проделанная в этой статье работа позволила существенно улучшить и сделать более удобным процесс использования нашего конвейера автоматической оптимизации. Мы не добавляли новую функциональность в торговую логику, а сосредоточились на решении практических задач, с которыми столкнулись при эксплуатации системы.
Мы продолжили разделять библиотечную и проектную части. Теперь для создания нового проекта на основе другой торговой стратегии достаточно описать её параметры в проектной части, не внося изменений в общее ядро. Этот процесс ещё не завершен и будет продолжен в дальнейшем.
Внедрение механизма ограничения времени выполнения задач оптимизации дало нам мощный инструмент для контроля над длительностью всего конвейера. Теперь мы можем гибко балансировать между глубиной оптимизации и временными затратами, останавливая процессы на первых двух этапах по достижении разумного лимита, что критически важно для оперативного тестирования и итерационной разработки.
Интеграция компонента CConsoleDialog для вывода подробной информации о ходе оптимизации превратила наблюдение за процессом из отслеживания идентификаторов задач в удобный и наглядный мониторинг. Мы теперь в реальном времени видим, на каком этапе, символе и таймфрейме работает конвейер, сколько времени осталось до завершения текущей задачи, и каков общий прогресс.
Таким образом, весь цикл — от создания базы данных проекта и запуска конвейера до получения готового к тестированию итогового советника с нормированными параметрами — был успешно продемонстрирован на практике. Проведённый «экстремальный» запуск с очень короткими лимитами времени наглядно показал жизнеспособность подхода: даже при сильном ограничении длительности отдельных задач система способна завершить полный цикл оптимизации и выдать рабочий результат. Это открывает возможности для быстрого прототипирования и проверки новых торговых стратегий.
Спасибо за внимание, до встречи!
Важное предупреждение
Все результаты, изложенные в этой статье и всех предшествующих статьях цикла, основываются только на данных тестирования на истории и не являются гарантией получения хоть какой-то прибыли в будущем. Работа в рамках данного проекта носит исследовательский характер. Все опубликованные результаты могут быть использованы всеми желающими на свой страх и риск.
| # | Имя | Версия | Описание | Последние изменения |
|---|---|---|---|---|
| SimpleCandles | Рабочая папка проекта (внутри MQL5/Shared Projects) | |||
| 1 | SimpleCandles.mq5 | 1.03 | Итоговый советник для параллельной работы нескольких групп модельных стратегий. Параметры будут браться из встроенной библиотеки групп. | Часть 29 |
| └ Optimization | Папка советников оптимизации проекта | |||
| 2 | CreateProject.mq5 | 1.05 | Советник-скрипт создания проекта с этапами, работами и задачами оптимизации. | Часть 29 |
| 3 | Optimization.mq5 | 1.03 | Советник для автоматической оптимизации проектов | Часть 29 |
| 4 | Stage1.mq5 | 1.02 | Советник оптимизации одиночного экземпляра торговой стратегии (Этап 1) | Часть 25 |
| 5 | Stage2.mq5 | 1.01 | Советник оптимизации группы экземпляров торговых стратегий (Этап 2) | Часть 25 |
| 6 | Stage3.mq5 | 1.01 | Советник, сохраняющий сформированную нормированную группу стратегий в базу данных эксперта с заданным именем. | Часть 25 |
| └ Strategies | Папка стратегий проекта | Часть 25 | ||
| 7 | SimpleCandlesStrategy.mqh | 1.03 | Класс торговой стратегии SimpleCandles | Часть 29 |
| Adwizard | Папка библиотеки Adwizard (внутри MQL5/Shared Projects) | |||
| └ Base | Базовые классы, от которых наследуются другие классы проекта | |||
| 8 | Advisor.mqh | 1.04 | Базовый класс эксперта | Часть 10 |
| 9 | Factorable.mqh | 1.06 | Базовый класс объектов, создаваемых из строки | Часть 28 |
| 10 | FactorableCreator.mqh | 1.00 | Класс создателей, связывающих названия и статические конструкторы классов-наследников CFactorable | Часть 24 |
| 11 | Interface.mqh | 1.01 | Базовый класс визуализации различных объектов | Часть 4 |
| 12 | Receiver.mqh | 1.04 | Базовый класс перевода открытых объемов в рыночные позиции | Часть 12 |
| 13 | Strategy.mqh | 1.04 | Базовый класс торговой стратегии | Часть 10 |
| └ Database | Файлы для работы со всеми типами баз данных, используемых советниками проекта | |||
| 14 | Database.mqh | 1.13 | Класс для работы с базой данных | Часть 29 |
| 15 | db.adv.schema.sql | 1.00 | Схема базы данных итогового советника | Часть 22 |
| 16 | db.cut.schema.sql | 1.00 | Схема урезанной базы данных оптимизации | Часть 22 |
| 17 | db.opt.schema.sql | 1.06 | Схема базы данных оптимизации | Часть 29 |
| 18 | Storage.mqh | 1.01 | Класс работы с хранилищем Key-Value для итогового советника в базе данных эксперта | Часть 23 |
| └ Experts | Файлы с общими частями используемых советников разного типа | |||
| 19 | Expert.mqh | 1.24 | Библиотечный файл для итогового советника. Параметры групп могут браться базы данных эксперта | Часть 28 |
| 20 | Optimization.mqh | 1.06 | Библиотечный файл для советника, управляющего запуском задач оптимизации | Часть 29 |
| 21 | Stage1.mqh | 1.19 | Библиотечный файл для советника оптимизации одиночного экземпляра торговой стратегии (Этап 1) | Часть 23 |
| 22 | Stage2.mqh | 1.04 | Библиотечный файл для советника оптимизации группы экземпляров торговых стратегий (Этап 2) | Часть 23 |
| 23 | Stage3.mqh | 1.04 | Библиотечный файл для советника, сохраняющего сформированную нормированную группу стратегий в базу данных эксперта с заданным именем. | Часть 23 |
| └ Optimization | Классы, отвечающие за работу автоматической оптимизации | |||
| 24 | OptimizationJob.mqh | 1.00 | Класс для работы этапа проекта оптимизации | Часть 25 |
| 25 | OptimizationProject.mqh | 1.00 | Класс для проекта оптимизации | Часть 25 |
| 26 | OptimizationStage.mqh | 1.00 | Класс для этапа проекта оптимизации | Часть 25 |
| 27 | OptimizationTask.mqh | 1.01 | Класс для задачи оптимизации (для создания) | Часть 29 |
| 28 | Optimizer.mqh | 1.04 | Класс для менеджера автоматической оптимизации проектов | Часть 29 |
| 29 | OptimizerTask.mqh | 1.06 | Класс для задачи оптимизации (для конвейера) | Часть 29 |
| └ Strategies | Примеры торговых стратегий, используемые для демонстрации работы проекта | |||
| 24 | HistoryStrategy.mqh | 1.00 | Класс торговой стратегии воспроизведения истории сделок | Часть 16 |
| 25 | SimpleVolumesStrategy.mqh | 1.11 | Класс торговой стратегии с использованием тиковых объемов | Часть 22 |
| └ Utils | Вспомогательные утилиты, макросы для сокращения кода | |||
| 26 | ConsoleDialog.mqh | 1.01 | Класс для вывода текстовой информации на график | Часть 28 |
| 26 | ExpertHistory.mqh | 1.00 | Класс для экспорта истории сделок в файл | Часть 16 |
| 27 | Macros.mqh | 1.07 | Полезные макросы для операций с массивами | Часть 26 |
| 28 | MTTester.mqh | — | Файл для работы с тестером стратегий из библиотеки MultiTester | Часть 28 |
| 29 | NewBarEvent.mqh | 1.00 | Класс определения нового бара для конкретного символа | Часть 8 |
| 30 | SymbolsMonitor.mqh | 1.01 | Класс получения информации о торговых инструментах (символах) | Часть 28 |
| └ Virtual | Классы для создания различных объектов, объединённых использованием системы виртуальных торговых ордеров и позиций | |||
| 31 | Money.mqh | 1.01 | Базовый класс управления капиталом | Часть 12 |
| 32 | TesterHandler.mqh | 1.07 | Класс для обработки событий оптимизации | Часть 23 |
| 33 | VirtualAdvisor.mqh | 1.12 | Класс эксперта, работающего с виртуальными позициями (ордерами) | Часть 28 |
| 34 | VirtualChartOrder.mqh | 1.02 | Класс графической виртуальной позиции | Часть 28 |
| 35 | VirtualCloseManager.mqh | 1.00 | Класс менеджера закрытия | Часть 28 |
| 36 | VirtualHistoryAdvisor.mqh | 1.00 | Класс эксперта воспроизведения истории сделок | Часть 16 |
| 37 | VirtualInterface.mqh | 1.00 | Класс графического интерфейса советника | Часть 4 |
| 38 | VirtualOrder.mqh | 1.09 | Класс виртуальных ордеров и позиций | Часть 22 |
| 39 | VirtualReceiver.mqh | 1.04 | Класс перевода открытых объемов в рыночные позиции (получатель) | Часть 23 |
| 40 | VirtualRiskManager.mqh | 1.06 | Класс управления риском (риск-менеждер) | Часть 28 |
| 41 | VirtualStrategy.mqh | 1.09 | Класс торговой стратегии с виртуальными позициями | Часть 23 |
| 42 | VirtualStrategyGroup.mqh | 1.04 | Класс группы торговых стратегий или групп торговых стратегий | Часть 28 |
| 43 | VirtualSymbolReceiver.mqh | 1.00 | Класс символьного получателя | Часть 3 |
| Common/Files | Общая папка данных терминалов MetaTrader 5 | |||
| 44 | article.17607.db.sqlite | — | База данных оптимизации | Часть 29 |
| 45 | SimpleCandles-27183.test.db.sqlite | — | База данных итогового советника | Часть 29 |
Также исходный код доступен в публичных репозиториях SimpleCandles и Adwizard
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Создание самооптимизирующихся советников на MQL5 (Часть 4): Динамическое изменение размера позиции
Искусство ведения логов (Часть 3): Изучение обработчиков для сохранения логов
Искусство ведения логов (Часть 4): Сохранение логов в файлах
Алгоритм Бизона — Bison Algorithm (BIA)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования