preview
Разрабатываем мультивалютный советник (Часть 29): Доработка конвейера

Разрабатываем мультивалютный советник (Часть 29): Доработка конвейера

MetaTrader 5Тестер |
701 0
Yuriy Bykov
Yuriy Bykov

Введение

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

Например, в части 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)
 
SimpleCandles.mq5
1.03
Итоговый советник для параллельной работы нескольких групп модельных стратегий. Параметры будут браться из встроенной библиотеки групп.
Часть 29
 └ Optimization
 Папка советников оптимизации проекта 
2   CreateProject.mq51.05Советник-скрипт создания проекта с этапами, работами и задачами оптимизации.
Часть 29
3   Optimization.mq51.03
Советник для автоматической оптимизации проектов
Часть 29
4   Stage1.mq51.02
Советник оптимизации одиночного экземпляра торговой стратегии (Этап 1)
Часть 25
5   Stage2.mq51.01
Советник оптимизации группы экземпляров торговых стратегий (Этап 2)
Часть 25
6   Stage3.mq51.01
Советник, сохраняющий сформированную нормированную группу стратегий в базу данных эксперта с заданным именем. Часть 25
 └ Strategies Папка стратегий проекта
Часть 25
7   SimpleCandlesStrategy.mqh
1.03
Класс торговой стратегии SimpleCandles
Часть 29
 Adwizard Папка библиотеки Adwizard
(внутри MQL5/Shared Projects)
 
 └ Base
 Базовые классы, от которых наследуются другие классы проекта   
8   Advisor.mqh1.04Базовый класс экспертаЧасть 10
9   Factorable.mqh
1.06
Базовый класс объектов, создаваемых из строки
Часть 28
10   FactorableCreator.mqh
1.00Класс создателей, связывающих названия и статические конструкторы классов-наследников CFactorableЧасть 24
11   Interface.mqh1.01
Базовый класс визуализации различных объектов
Часть 4
12   Receiver.mqh
1.04 Базовый класс перевода открытых объемов в рыночные позиции
Часть 12
13   Strategy.mqh
1.04
Базовый класс торговой стратегии
Часть 10
 └ Database
 Файлы для работы со всеми типами баз данных, используемых советниками проекта
 
14   Database.mqh1.13Класс для работы с базой данныхЧасть 29
15   db.adv.schema.sql1.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.mqh1.04Библиотечный файл для советника оптимизации группы экземпляров торговых стратегий (Этап 2)  Часть 23
23   Stage3.mqh
1.04Библиотечный файл для советника, сохраняющего сформированную нормированную группу стратегий в базу данных эксперта с заданным именем.Часть 23
 └ Optimization
 Классы, отвечающие за работу автоматической оптимизации
 
24   OptimizationJob.mqh1.00Класс для работы этапа проекта оптимизации
Часть 25
25   OptimizationProject.mqh1.00Класс для проекта оптимизацииЧасть 25
26   OptimizationStage.mqh1.00Класс для этапа проекта оптимизацииЧасть 25
27   OptimizationTask.mqh1.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.mqh1.01Класс для вывода текстовой информации на графикЧасть 28
26   ExpertHistory.mqh1.00Класс для экспорта истории сделок в файлЧасть 16
27   Macros.mqh1.07Полезные макросы для операций с массивамиЧасть 26
28    MTTester.mqh 
Файл для работы с тестером стратегий из библиотеки MultiTester
Часть 28
29   NewBarEvent.mqh1.00 Класс определения нового бара для конкретного символа Часть 8
30   SymbolsMonitor.mqh 1.01Класс получения информации о торговых инструментах (символах)Часть 28
 └ Virtual
 Классы для создания различных объектов, объединённых использованием системы виртуальных торговых ордеров и позиций

31   Money.mqh1.01 Базовый класс управления капиталом
Часть 12
32   TesterHandler.mqh 1.07Класс для обработки событий оптимизации Часть 23
33   VirtualAdvisor.mqh 1.12 Класс эксперта, работающего с виртуальными позициями (ордерами)Часть 28
34   VirtualChartOrder.mqh 1.02 Класс графической виртуальной позицииЧасть 28
35   VirtualCloseManager.mqh1.00Класс менеджера закрытияЧасть 28
36   VirtualHistoryAdvisor.mqh1.00 Класс эксперта воспроизведения истории сделок Часть 16
37   VirtualInterface.mqh 1.00 Класс графического интерфейса советника Часть 4
38   VirtualOrder.mqh1.09 Класс виртуальных ордеров и позиций Часть 22
39   VirtualReceiver.mqh1.04Класс перевода открытых объемов в рыночные позиции (получатель) Часть 23
40   VirtualRiskManager.mqh 1.06Класс управления риском (риск-менеждер) Часть 28
41   VirtualStrategy.mqh1.09 Класс торговой стратегии с виртуальными позициями Часть 23
42   VirtualStrategyGroup.mqh 1.04 Класс группы торговых стратегий или групп торговых стратегийЧасть 28
43   VirtualSymbolReceiver.mqh 1.00Класс символьного получателя Часть 3
 Common/Files Общая папка данных терминалов MetaTrader 5 
44article.17607.db.sqliteБаза данных оптимизации
Часть 29
45SimpleCandles-27183.test.db.sqlite

База данных итогового советника
Часть 29

Также исходный код доступен в публичных репозиториях SimpleCandles и Adwizard

Прикрепленные файлы |
MQL5.zip (2256.62 KB)
Создание самооптимизирующихся советников на MQL5 (Часть 4): Динамическое изменение размера позиции Создание самооптимизирующихся советников на MQL5 (Часть 4): Динамическое изменение размера позиции
Успешное применение алгоритмической торговли требует непрерывного междисциплинарного обучения. Однако бесконечный спектр возможностей может потребовать многолетних усилий, не принося ощутимых результатов. Чтобы решить эту проблему, мы предлагаем структуру, которая постепенно усложняется, позволяя трейдерам постепенно совершенствовать свои стратегии, а не тратить неопределенное время на неопределенные результаты.
Искусство ведения логов (Часть 3): Изучение обработчиков для сохранения логов Искусство ведения логов (Часть 3): Изучение обработчиков для сохранения логов
В этой статье мы разберем концепцию обработчиков в библиотеке логирования, поймем их работу, и создадим три начальные реализации: консоль, база данных и файл. Мы рассмотрим все: от базовой структуры обработчиков до практического тестирования, заложив основу для их дальнейшей полноценной реализации.
Искусство ведения логов (Часть 4): Сохранение логов в файлах Искусство ведения логов (Часть 4): Сохранение логов в файлах
В этой статье я расскажу вам об основных операциях с файлами и о том, как настроить гибкий обработчик для индивидуальной настройки. Мы обновим класс CLogifyHandlerFile, чтобы записывать логи непосредственно в файл. Мы выполним тест производительности, смоделировав торговлю по EURUSD в течение недели, при этом на каждом тике будут генерироваться логи, а весь процесс займет 5 минут и 11 секунд. Результат будет сравнен в следующей статье, где мы реализуем систему кэширования для улучшения производительности.
Алгоритм Бизона — Bison Algorithm (BIA) Алгоритм Бизона — Bison Algorithm (BIA)
Новый оптимизационный метод Bison Algorithm (BIA) — две стратегии, заимствованные из поведения бизонов, для непрерывных задач с одной целевой функцией. Ключевыми особенностями BIA являются два основополагающих принципа, заимствованных из поведения бизонов, это способность к динамичному перемещению и оборонительная стратегия.