preview
Разрабатываем мультивалютный советник (Часть 17): Дальнейшая подготовка к реальной торговле

Разрабатываем мультивалютный советник (Часть 17): Дальнейшая подготовка к реальной торговле

MetaTrader 5Тестер | 23 августа 2024, 14:23
61 0
Yuriy Bykov
Yuriy Bykov

Введение

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

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

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


Намечаем путь 

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

На втором этапе мы проводили оптимизацию выбора группы наборов параметров, полученных на первом этапе, дающей наилучшие результаты при совместной работе (рассмотрен в части 6 и части 13). В одну группу мы, так же как и на первом этапе, включали наборы параметров, использующие одинаковую пару символ-таймфрейм. Информация о результатах всех перебираемых при оптимизации групп тоже сохранялась в нашу базу данных.

На третьем этапе мы уже не использовали штатный оптимизатор тестера стратегий, поэтому о его автоматизации речь пока не идёт. Третий этап состоял в отборе одной самой лучшей группы, найденной на втором этапе, для каждой имевшейся комбинации символа и таймфрейма. Мы использовали оптимизацию на трёх символах (EURGBP, EURUSD, GBPUSD) и трёх таймфреймах (H1, M30, M15). Таким образом, результатом третьего этапа будет девять отобранных групп. Но для упрощения и ускорения расчётов в тестере мы ограничились в последних статьях только тремя лучшими группами (с тремя разными символами и таймфреймом H1).

Результатом третьего этапа у нас был набор идентификаторов строк из таблицы passes, который мы передавали через входной параметр нашему итоговому советнику SimpleVolumesExpert.mq5:

input string     passes_ = "734469,"
                           "736121,"
                           "776928";    // - Идентификаторы проходов через запятую

При желании мы могли изменить перед запуском тестирования советника этот параметр. Таким образом, можно было запустить итоговый советник с любым желаемым подмножеством групп из множества имеющихся в базе данных в таблице passes. Точнее, не совсем любым, а только таким, который при записи в строку через запятую не превышает по длине 247 символов. Это ограничение, накладываемое языком MQL5 на значения входных строковых параметров. Согласно документации, максимальная длина значения строкового параметра может иметь размер от 191 до 253 символов, в зависимости от длины имени параметра.

Поэтому если мы захотим включить в работу больше, грубо говоря, 40 групп, то сделать это таким способом не получится. Придётся, например, сделать переменную passes_ не входным строковым параметром, а просто строковой переменной, убрав из кода слово input. Тогда задать нужный набор групп мы сможем только в исходном коде. Однако пока нам нет необходимости использовать такие большие наборы. Более того, как показали эксперименты, проведённые в части 5, нам более выгодно как раз не делать одну группу из большого количества одиночных экземпляров торговых стратегий или групп торговых стратегий. Выгоднее исходное количество одиночных экземпляров торговых стратегий разбить на несколько подгрупп, из которых собрать уже меньшее количество новых групп. Эти новые группы можно уже либо объединить в одну итоговую группу, или повторить процесс группировки на новые подгруппы. Таким образом нам на каждом уровне объединения придётся брать в одну группу относительно небольшое количество стратегий или групп.

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

Когда доступа к базе данных нет, то советнику всё равно надо как-то сформировать строку инициализации объекта эксперта, содержащую нужный состав одиночных экземпляров торговых стратегий или групп торговых стратегий. Можно, например, сохранить её в файл, и передавать советнику имя файла, откуда тот будет загружать строку инициализации. Или можно вставить содержимое строки инициализации в исходный код советника через дополнительный библиотечный файл mqh. Можно даже объединить эти два способа: сохранив строку инициализации в файл, затем импортировать его с помощью средств импорта файлов в редакторе MetaEditor (Edit → Insert → File).

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

С учётом сказанного, предстоящую работу можно разбить на следующие этапы:

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

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

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

Приступим к реализации задуманного.


Вспомним сделанное

Упомянутые шаги являются прообразом реализации Этапа 8, описанного в части 9. Напомним, что в той статье мы перечислили набор этапов, прохождение которых может позволить получить готовый советник с неплохими показателями торговли. Этап 8 как раз подразумевал, что все найденные лучшие группы групп для разных торговых стратегий, символов, таймфреймов и прочих параметров мы собираем в один итоговый советник. При этом вопрос "Как именно надо выбирать лучшие группы?" мы ещё детально не рассматривали.

С одной стороны, ответ на него может оказаться элементарным. Например, просто выбираем среди всех результатов групп лучшие по какому-либо параметру (общей прибыли, коэффициенту Шарпа, нормированной среднегодовой прибыли). Но с другой стороны, ответ может оказаться и гораздо сложнее. Например, вдруг более хорошие результаты на тестах будут достигаться, если использовать для отбора лучших групп комплексный критерий? Или даже какие-то лучшие группы не стоит вообще включать в итоговый советник, так как их включение будет ухудшать результаты, достигнутые без них? Эта тема, скорее всего, потребует своего обстоятельного изучения.

Ещё одним вопросом, который тоже потребует отдельного изучения, является вопрос оптимального разбиения групп на подгруппы с нормализацией подгрупп. В части 5, ещё до начала реализации какой-либо автоматизации этапов тестирования, мы уже затрагивали этот вопрос. Тогда мы вручную отобрали девять одиночных экземпляров торговых стратегий, по три экземпляра на каждый из трёх используемых торговых инструментов (символов).

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

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


Подбор и сохранение групп

Казалось бы, для этого у нас уже всё есть. Берём имеющийся советник SimpleVolumesExpert.mq5 из прошлой части, указываем во входном параметре passes_ идентификаторы проходов через запятую, запускаем одиночный проход тестера и получаем сохранение нужной строки инициализации в базу данных. Недостаёт только дополнительной информации. Но оказалось, что в базу информация о проходе не попадает.

Дело в том, что у нас есть выгрузка в базу данных только результатов проходов оптимизации. Но нет выгрузки результатов одиночного прохода. Вспомним, что выгрузка у нас производится внутри метода CTesterHandler::ProcessFrames(), который на верхнем уровне вызывается из обработчика OnTesterPass():

//+------------------------------------------------------------------+
//| Обработка пришедших фреймов                                      |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrames(void) {
// Открываем базу данных
   DB::Connect();

// Переменные для чтения данных из фреймов
   ...

// Проходим по фреймам и читаем данные из них
   while(FrameNext(pass, name, id, value, data)) {
      // Переводим в строку массив символов, прочитанный из фрейма
      values = CharArrayToString(data);
      
      // Формируем строку с именами и значениями параметров прохода
      inputs = GetFrameInputs(pass);

      // Формируем SQL-запрос из полученных данных
      query = StringFormat("INSERT INTO passes "
                           "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s');",
                           s_idTask, pass, values, inputs,
                           TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));

      // Добавляем его в массив SQL-запросов
      APPEND(queries, query);
   }

// Выполняем все запросы
   DB::ExecuteTransaction(queries);

// Закрываем базу данных
   DB::Close();
}

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

Можно, конечно, оставить всё как есть, и сделать советник, который надо будет запускать на оптимизацию по некому ненужному параметру. Целью такой оптимизации будет получение результатов первого прохода, после чего оптимизация будет останавливаться. Таким образом, в базу данных результаты прохода попадут. Но это кажется слишком некрасивым, поэтому мы пойдём другим путём.

При запуске одиночного прохода в советнике, по завершении этого прохода обязательно будет вызываться обработчик OnTester(). Поэтому код сохранения результатов одиночного прохода нам придётся вставить либо напрямую в обработчик, либо в один из методов, которые вызываются из этого обработчика. Наверное, наиболее подходящим местом для вставки будет метод CTesterHandler::Tester(). Однако стоит учесть, что этот метод будет вызываться также и при завершении советником прохода оптимизации. Сейчас в этом методе уже располагается код, который формирует и отправляет результаты прохода оптимизации через механизм фреймов данных.

При запуске одиночного прохода данные для фрейма всё равно формируются, но сам фрейм данных, даже если и создаётся, то не может быть использован. Если мы попытаемся воспользоваться функцией получения фрейма FrameNext() после создания фрейма функцией FrameAdd() в советнике, запущенном в режиме одиночного прохода, то FrameNext() не будет читать созданный фрейм. Она будет вести себя так, как будто ни одного фрейма не было создано.

Поэтому поступим следующим образом: в обработчике CTesterHandler::Tester() будем проверять, является ли данный проход одиночным или выполненным в рамках оптимизации. В зависимости от результата будем либо сразу сохранять в базу данных результаты прохода (для одиночного), либо создавать фрейм данных для отправки главному советнику (для оптимизации). Добавим новый метод, вызываемый для сохранения одиночного прохода и ещё один вспомогательный метод, формирующий SQL-запрос на вставку нужных данных в таблицу passes. Последний нам понадобится из-за того, что теперь такое действие будет выполняться уже в двух местах кода, а не в одном. Поэтому вынесем его в отдельный метод.

//+------------------------------------------------------------------+
//| Класс для обработки событий оптимизации                          |
//+------------------------------------------------------------------+
class CTesterHandler {
   
    ...

   static void       ProcessFrame(string values);  // Обработка данных одиночного прохода

   // Формирование SQL-запроса на вставку результатов прохода
   static string     GetInsertQuery(string values, string inputs, ulong pass = 0);
public:
   ...
};

Реализация метода GetInsertQuery() у нас фактически есть, нам остаётся только перенести в него блок кода из метода ProcessFrames() и вызвать его в нужном месте в методе ProcessFrames():

//+------------------------------------------------------------------+
//| Формирование SQL-запроса на вставку результатов прохода          |
//+------------------------------------------------------------------+
string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) {
   return StringFormat("INSERT INTO passes "
                       "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s');",
                       s_idTask, pass, values, inputs,
                       TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
}


//+------------------------------------------------------------------+
//| Обработка пришедших фреймов                                      |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrames(void) {
   ...

// Проходим по фреймам и читаем данные из них
   while(FrameNext(pass, name, id, value, data)) {
      // Переводим в строку массив символов, прочитанный из фрейма
      values = CharArrayToString(data);

      // Формируем строку с именами и значениями параметров прохода
      inputs = GetFrameInputs(pass);

      // Формируем SQL-запрос из полученных данных
      query = GetInsertQuery(values, inputs, pass);

      // Добавляем его в массив SQL-запросов
      APPEND(queries, query);
   }

   ...
}

Для сохранения данных одиночного прохода мы будем вызывать новый метод ProcessFrame(), принимающий в качестве параметра строку, являющуюся частью SQL-запроса и содержащую данные о проходе для вставки в таблицу passes. Внутри самого метода мы просто соединяемся с базой данных, формируем окончательный SQL-запрос и выполняем его:

//+------------------------------------------------------------------+
//| Обработка данных одиночного прохода                              |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrame(string values) {
// Открываем базу данных
   DB::Connect();

// Формируем SQL-запрос из полученных данных
   string query = GetInsertQuery(values, "", 0);

// Выполняем запрос
   DB::Execute(query);

// Закрываем базу данных
   DB::Close();
}

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

//+------------------------------------------------------------------+
//| Обработка завершения прохода тестера для агента                  |
//+------------------------------------------------------------------+
void CTesterHandler::Tester(double custom,   // Пользовательский критерий
                            string params    // Описание параметров советника в текущем проходе
                           ) {

    ... 

// Формируем строку с данными о проходе
   data = StringFormat("%s,'%s'", data, params);

// Если это проход в рамках процесса оптимизации, то
   if(MQLInfoInteger(MQL_OPTIMIZATION)) {
      // Открываем файл для записи данных для фрейма
      int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI);

      // Записываем описание параметров советника
      FileWriteString(f, data);

      // Закрываем файл
      FileClose(f);

      // Создаём фрейм с данными из записанного файла и отправляем его в главный терминал
      if(!FrameAdd("", 0, 0, s_fileName)) {
         PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError());
      }
   } else {
      // Иначе это одиночный проход, вызываем метод добавления его результатов в базу данных
      CTesterHandler::ProcessFrame(data);
   }
}

Сохраним сделанные изменения в файле TesterHandler.mqh в текущей папке.

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

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

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


Формирование библиотеки

Не будем перегружать новую таблицу избыточными полями с информацией, которую можно будет получить из других таблиц базы данных. Например, если запись в новой таблице имеет связь через внешний ключ с записью из таблицы проходов (passes), то там уже есть дата создания. Также по идентификатору прохода мы можем построить цепочку связей и определить, к какому проекту относится данный проход, а значит и используемая в проходе группа стратегий.

С учётом сказанного создадим таблицу strategy_groups со следующим набором полей:

  • id_pass. Идентификатор прохода из таблицы passes (внешний ключ)
  • name. Название группы стратегий, которое будет использоваться для формирования перечислений для входного параметра выбора группы стратегий.

SQL-код для создания нужной таблицы может быть таким:

-- Table: strategy_groups
DROP TABLE IF EXISTS strategy_groups;

CREATE TABLE strategy_groups (
    id_pass INTEGER REFERENCES passes (id_pass) ON DELETE CASCADE
                                                ON UPDATE CASCADE
                    PRIMARY KEY,
    name    TEXT
);

Для осуществления большинства дальнейших действий мы создадим вспомогательный класс CGroupsLibrary. В его задачи будет входить вставка и получение информации о группах стратегий из базы данных, формирование mqh-файла с собственно библиотекой хороших групп, которую будет использовать итоговый советник. Но к нему мы вернёмся чуть позже, а пока что давайте сделаем советник, который мы будем использовать для формирования библиотеки.

Имеющийся советник SimpleVolumesExpert.mq5, хотя и делает почти всё, что нужно, всё-таки нуждается в доработке. Да и использовать его мы планировали уже как окончательный вариант итогового советника. Поэтому сохраним его под новым именем SimpleVolumesStage3.mq5, и внесём в новый файл необходимые дополнения. Нам не хватает двух вещей: возможности указать имя группы, образованной для текущих выбранных проходов (в параметре passes_) и сохранения строки инициализации этой группы в новую таблицу strategy_groups.

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

input group "::: Сохранение в библиотеку"
input string groupName_  = "";         // - Название группы (если пустое - не сохранять)

А вот для второй нам придётся потрудиться немного больше. Дело в том, что для вставки данных в таблицу strategy_groups нам надо знать идентификатор, который был присвоен записи текущего прохода при вставке в таблицу passes. Так как его значение автоматически выделяется самой базой данных (в запросе мы просто передаём вместо его значения NULL), то в коде оно не существует в виде значения какой-либо переменной. Поэтому мы в данный момент не можем воспользоваться им в другом месте, где оно понадобится. Надо как-то определить это значение.

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

Гораздо более надёжным способом, свободным от возможных ошибок вышеописанного, является следующий вариант. Мы можем немного модифицировать SQL-запрос на вставку данных, превратив его в запрос, который будет возвращать сгенерированный идентификатор новой строки таблицы в качестве своего результата. Для этого достаточно добавить в конец SQL-запроса оператор "RETURNING rowid". Сделаем это в методе GetInsertQuery(), который формирует SQL-запрос на вставку новой строки в таблицу passes. Несмотря на то, что столбец идентификатора в таблице passes имеет название id_pass, мы можем называть его rowid, так как он имеет соответствующий тип (INTEGER PRIMARY KEY AUTOINCREMENT) и подменяет собой скрытый автоматически присутствующий в таблицах SQLite столбец rowid.

//+------------------------------------------------------------------+
//| Формирование SQL-запроса на вставку результатов прохода          |
//+------------------------------------------------------------------+
string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) {
   return StringFormat("INSERT INTO passes "
                       "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s') RETURNING rowid;",
                       s_idTask, pass, values, inputs,
                       TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
}

Также нам потребуется модифицировать и код на MQL5, отправляющий этот запрос. Сейчас мы пользуемся для этого методом DB::Execute(query), который подразумевает, что передаваемый ему запрос query не является запросом, возвращающим какие-либо данные.

Поэтому добавим в класс CDatabase новый метод Insert(), который будет выполнять передаваемый запрос на вставку и возвращать одно прочитанное значение результата. Внутри него вместо функции DatabaseExecute() мы воспользуемся функцией DatabasePrepare(), которая позволяет затем получить доступ к результатам запроса:

//+------------------------------------------------------------------+
//| Класс для работы с базой данных                                  |
//+------------------------------------------------------------------+
class CDatabase {
   ...
public:
   ...
   // Выполнение запроса к БД на вставку с возвратом идентификатора новой записи
   static ulong      Insert(string query);
};

...

//+------------------------------------------------------------------+
//| Выполнение запроса к БД на вставку с возвратом идентификатора    |
//| новой записи                                                     |
//+------------------------------------------------------------------+
ulong CDatabase::Insert(string query) {
   ulong res = 0;
   
// Выполняем запрос
   int request = DatabasePrepare(s_db, query);

// Если нет ошибки
   if(request != INVALID_HANDLE) {
      // Структура данных для чтения одной строки результата запроса
      struct Row {
         int         rowid;
      } row;

      // Читаем данные из первой строки результата
      if(DatabaseReadBind(request, row)) {
         res = row.rowid;
      } else {
         // Сообщаем об ошибке при необходимости
         PrintFormat(__FUNCTION__" | ERROR: Reading row for request \n%s\nfailed with code %d",
                     query, GetLastError());
      }
   } else {
      // Сообщаем об ошибке при необходимости
      PrintFormat(__FUNCTION__" | ERROR: Request \n%s\nfailed with code %d",
                  query, GetLastError());
   }
   return res;
}
//+------------------------------------------------------------------+

Мы не стали усложнять этот метод дополнительными проверками, что переданный запрос является именно INSERT-запросом, что в нём присутствует команда возврата идентификатора, и возвращаемое значение не является составным. Отступление от этих условий будет приводить к ошибкам при выполнении данного кода, но поскольку этот метод будет использоваться пока что в единственном месте проекта, то постараемся смочь передать ему корректный запрос.

Сохраним сделанные изменения в файле Database.mqh в текущей папке.

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

//+------------------------------------------------------------------+
//| Класс для обработки событий оптимизации                          |
//+------------------------------------------------------------------+
class CTesterHandler {
   ...
public:
   ...
   static ulong      s_idPass;
};

...
ulong CTesterHandler::s_idPass = 0;

...

//+------------------------------------------------------------------+
//| Обработка данных одиночного прохода                              |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrame(string values) {
// Открываем базу данных
   DB::Connect();

// Формируем SQL-запрос из полученных данных
   string query = GetInsertQuery(values, "", 0);

// Выполняем запрос
   s_idPass = DB::Insert(query);

// Закрываем базу данных
   DB::Close();
}

Сохраним сделанные изменения в файле TesterHandler.mqh в текущей папке.

Теперь настала пора вернуться к заявленному вспомогательному классу CGroupsLibrary. В нём в итоге понадобилось объявить два публичных метода, один частный метод и один статический массив:

//+------------------------------------------------------------------+
//| Класс для работы с библиотекой отобранных групп стратегий        |
//+------------------------------------------------------------------+
class CGroupsLibrary {
private:
   // Экспорт извлечённых из базы данных имён групп и строк инициализации в виде MQL5-кода
   static void       ExportParams(string &p_names[], string &p_params[]);

public:
   // Добавление в базу данных имени и идентификатора прохода
   static void       Add(ulong p_idPass, string p_name);

   // Экспорт проходов в mqh-файл
   static void       Export(string p_idPasses);

   // Массив для заполнения строк инициализации из mqh-файла
   static string     s_params[];
};

В советнике, который мы будем использовать для формирования библиотеки, будет использоваться только метод Add(). Ему будет передаваться идентификатор прохода и имя группы для сохранения в библиотеку. Сам код метода очень прост: формируем из входных данных SQL-запрос на вставку новой записи в таблицу strategy_groups и выполняем его.

//+------------------------------------------------------------------+
//| Добавление в базу данных имени и идентификатора прохода          |
//+------------------------------------------------------------------+
void CGroupsLibrary::Add(ulong p_idPass, string p_name) {
   string query = StringFormat("INSERT INTO strategy_groups VALUES(%d, '%s')",
                               p_idPass, p_name);

// Открываем базу данных
   if(DB::Connect()) {
      // Выполняем запрос
      DB::Execute(query);

      // Закрываем базу данных
      DB::Close();
   }
}

Теперь для завершения разработки инструмента по формированию библиотеки нам остаётся только добавить в советник SimpleVolumesStage3.mq5 вызов метода Add() после завершения прохода тестера:

//+------------------------------------------------------------------+
//| Результат тестирования                                           |
//+------------------------------------------------------------------+
double OnTester(void) {
   // Обрабатываем завершение прохода в объекте эксперта
   double res = expert.Tester();

   // Если имя группы не пустое, то сохраняем проход в библиотеку
   if(groupName_ != "") {
      CGroupsLibrary::Add(CTesterHandler::s_idPass, groupName_);
   }
   return res;
}

Сохраним сделанные изменения в файлах SimpleVolumesStage3.mq5 и GroupsLibrary.mqh в текущей папке. Если добавить заглушки для остальных методов класса CGroupsLibrary, то скомпилированным советником SimpleVolumesStage3.mq5 уже можно пользоваться. 


Наполняем библиотеку

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

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

SELECT sg.id_pass,
       sg.name,
       p.custom_ontester,
       p.sharpe_ratio,
       p.profit,
       p.profit_factor,
       p.equity_dd_relative
  FROM strategy_groups sg
       JOIN
       passes p ON sg.id_pass = p.id_pass;

В результате запроса получилось следующая таблица:

Рис. 1. Состав библиотеки групп 

В столбце name мы видим названия групп, которые отражают торговые инструменты (символы), таймфреймы и количество экземпляров торговых стратегий, используемые в данной группе. Например, наличие "EUR-GBP-USD" означает, что в эту группу вошли экземпляры торговых стратегий, работающие на трёх символах: EURGBP, EURUSD и GBPUSD. Если вначале имени группы идёт "Only EURGBP", то в неё входят экземпляры стратегий только для символа EURGBP. Аналогично читаются и используемые таймфреймы. В конце имени указывается количество экземпляров торговых стратегий. Например, "3х16 items" говорит о том, что в этой группе объединены три нормированных группы по 16 стратегий в каждой.

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


Экспорт библиотеки

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

В методе Export() мы будем получать из базы данных и добавлять в соответствующие массивы имена групп библиотеки и их строки инициализации. Сформированные массивы будут передаваться следующему методу ExportParams():

//+------------------------------------------------------------------+
//| Экспорт проходов в mqh-файл                                      |
//+------------------------------------------------------------------+
void CGroupsLibrary::Export(string p_idPasses) {
// Массив названий групп
   string names[];

// Массив строк инициализации групп
   string params[];

// Если соединение с основной базой данных установлено, то
   if(DB::Connect()) {
      // Формируем запрос на получение проходов с указанными идетификаторами
      string query = "SELECT sg.id_pass,"
                     "       sg.name,"
                     "       p.params"
                     "  FROM strategy_groups sg"
                     "       JOIN"
                     "       passes p ON sg.id_pass = p.id_pass";

      query = StringFormat("%s "
                           "WHERE p.id_pass IN (%s);",
                           query, p_idPasses);

      // Подготавливаем и выполняем запрос
      int request = DatabasePrepare(DB::Id(), query);

      // Если запрос выполнен успешно
      if(request != INVALID_HANDLE) {
         // Структура для чтения результатов
         struct Row {
            ulong          idPass;
            string         name;
            string         params;
         } row;

         // Для всех результатов запроса, добавляем название и строку инициализации в массивы
         while(DatabaseReadBind(request, row)) {
            APPEND(names, row.name);
            APPEND(params, row.params);
         }
      }

      DB::Close();

      // Выполняем экспорт в mqh-файл
      ExportParams(names, params);
   }
}

В методе ExportParams() мы формируем строку с кодом на языке MQL5, который будет создавать перечисление (enum) с заданным именем ENUM_GROUPS_LIBRARY и наполнять его элементами. К каждому элементу будет сделан комментарий, содержащий имя группы. Далее в коде будет объявляться статический строковый массив CGroupsLibrary::s_params[], который будет наполняться строками инициализации для групп из библиотеки. Каждая строка инициализации будет предварительно обрабатываться: все символы перевода строки будут заменяться на пробелы, а перед двойными кавычками будет добавляться обратный слэш. Это нужно для того, чтобы поместить строку инициализации внутрь двойных кавычек в генерируемом коде.

После того как код полностью сформирован в переменной data, мы создаём файл с именем ExportedGroupsLibrary.mqh и сохраняем в него полученный код.

//+------------------------------------------------------------------+
//| Экспорт извлечённых из базы данных имён групп и                  |
//| строк инициализации в виде MQL5-кода                             |
//+------------------------------------------------------------------+
void CGroupsLibrary::ExportParams(string &p_names[], string &p_params[]) {
   // Заголовок перечисления ENUM_GROUPS_LIBRARY
   string data = "enum ENUM_GROUPS_LIBRARY {\n";

   // Наполняем перечисление именами групп
   FOREACH(p_names, { data += StringFormat("   GL_PARAMS_%d, // %s\n", i, p_names[i]); });

   // Закрываем перечисление
   data += "};\n\n";

   // Заголовок массива строк инициализации групп и его открывающая скобка
   data += "string CGroupsLibrary::s_params[] = {";

   // Наполняем массив, заменив в строках инициализации недопустимые символы
   string param;
   FOREACH(p_names, {
      param = p_params[i];
      StringReplace(param, "\r", "");
      StringReplace(param, "\n", " ");
      StringReplace(param, "\"", "\\\"");
      data += StringFormat("\"%s\",\n", param);
   });

   // Закрываем массив
   data += "};\n";

// Открываем файл для записи данных
   int f = FileOpen("ExportedGroupsLibrary.mqh", FILE_WRITE | FILE_TXT | FILE_ANSI);

// Записываем сформированный код
   FileWriteString(f, data);

// Закрываем файл
   FileClose(f);
}

А далее идёт очень важная часть:

// Подключение экспортированного mqh-файла.
// В нём будет инициализироваться статическая переменная CGroupsLibrary::s_params[]
// и перечисление ENUM_GROUPS_LIBRARY
#include "ExportedGroupsLibrary.mqh"

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

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

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
input group "::: Экспорт из библиотеки"
input string     passes_ = "802150,802151,802152,802153,802154,"
                           "802155,802156,802157,802158,802159,"
                           "802160,802161,802162,802164,802165,"
                           "802166,802167,802168,802169,802173";    // - Идентификаторы сохраняемых проходов через запятую


//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Вызываем метод экспорта библиотеки групп
   CGroupsLibrary::Export(passes_);

// Успешная инициализация
   return(INIT_SUCCEEDED);
}

void OnTick() {
   ExpertRemove();
}

Через изменение параметра passes_ мы можем выбирать состав и порядок, в котором будут экспортированы группы из библиотеки в базе данных. После однократного запуска этого советника на графике в папке данных терминала появится файл ExportedGroupsLibrary.mqh, который необходимо перенести в текущую папку с кодом проекта.


Создание итогового советника

Наконец-то мы подошли к завершающей фазе. Осталось только внести небольшие правки в советник SimpleVolumesExpert.mq5. Во-первых, к нему надо подключить файл GroupsLibrary.mqh:

#include "GroupsLibrary.mqh"

Затем, вместо входного параметра passes_ поставить новый входной параметр, через который можно будет выбирать группу из библиотеки:

input group "::: Отбор в группу"
input ENUM_GROUPS_LIBRARY       groupId_     = -1;    // - Группа из библиотеки

В функции OnInit() вместо получения строк инициализации из базы данных по идентификаторам проходов (как было раньше), мы теперь просто возьмём строку инициализации из массива CGroupsLibrary::s_params[] с индексом, соответствующим выбранному значению входного параметра groupId_:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   ...

// Строка инициализации с наборами параметров стратегий
   string strategiesParams = NULL;

// Если выбранный индекс группы стратегий из библиотеки является допустимым, то
   if(groupId_ >= 0 && groupId_ < ArraySize(CGroupsLibrary::s_params)) {
      // Берём строку инициализации из библиотеки для выбранной группы
      strategiesParams = CGroupsLibrary::s_params[groupId_];
   }

// Если группа стратегий из библиотеки не задана, то прерываем работу
   if(strategiesParams == NULL) {
      return INIT_FAILED;
   }

   ...

// Успешная инициализация
   return(INIT_SUCCEEDED);
}

Сохраним сделанные изменения в файле SimpleVolumesExpert.mq5 в текущей папке.

Поскольку к элементам перечисления ENUM_GROUPS_LIBRARY мы добавили комментарии с именами, то в диалоге выбора параметров советника мы сможем видеть понятные названия, а не просто последовательность чисел:

Рис. 2. Выбор в параметрах советника группы из библиотеки по имени

Запустим советника с последней группой из списка и посмотрим на результат:

Рис. 3. Результаты тестирования итогового советника с самой привлекательной группой из библиотеки

Видно, что по показателю среднегодовой нормированной прибыли результаты получились близкие к тем, которые хранятся в базе данных. Небольшие отличия связаны в первую очередь с тем, что в итоговом советнике использовалась уже нормированная группа (в этом можно убедиться, посмотрев на значение максимальной относительной просадки, составляющей примерно 10% от используемого депозита). При генерации строки инициализации этой группы в советнике SimpleVolumesStage3.ex5 во время прохода группа была ещё не нормирована, поэтому там просадка составила примерно 5.4%. 


Заключение

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

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

И напоследок сделаем предупреждение, которое неявно присутствовало и раньше. Мы нигде в предыдущих частях не говорили, что следование в предлагаемом направлении позволит получить гарантированную прибыль. Наоборот, в некоторые моменты мы получали неутешительные результаты тестирования. Также, несмотря на затрачиваемые усилия по подготовке советника к реальной торговле, мы вряд ли сможем в какой-то момент сказать, что сделали всё возможное и невозможное для обеспечения корректной работы советника на реальных счетах. Это своего рода идеал, к которому можно и нужно стремиться, но достижение его всегда представляется делом туманного будущего. Что впрочем, не мешает нам приближаться к нему.

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

Спасибо за внимание, до встречи!


Содержание архива

#
 Имя
Версия  Описание   Последние изменения
 MQL5/Experts/Article.15360
1 Advisor.mqh 1.04 Базовый класс эксперта Часть 10
2 Database.mqh 1.04 Класс для работы с базой данных Часть 17
3 ExpertHistory.mqh 1.00 Класс для экспорта истории сделок в файл Часть 16
4 ExportedGroupsLibrary.mqh
Генерируемый файл с перечислением имён групп стратегий и массивом их строк инициализации Часть 17
5 Factorable.mqh 1.01 Базовый класс объектов, создаваемых из строки Часть 10
6 GroupsLibrary.mqh 1.00 Класс для работы с библиотекой отобранных групп стратегий Часть 17
7 HistoryReceiverExpert.mq5 1.00 Советник воспроизведения истории сделок с риск-менеджером Часть 16  
8 HistoryStrategy.mqh  1.00 Класс торговой стратегии воспроизведения истории сделок  Часть 16
9 Interface.mqh 1.00 Базовый класс визуализации различных объектов Часть 4
10 LibraryExport.mq5 1.00 Советник, сохраняющий строки инициализации выбранных проходов из библиотеки в файл ExportedGroupsLibrary.mqh Часть 17
11 Macros.mqh 1.02 Полезные макросы для операций с массивами Часть 16  
12 Money.mqh 1.01  Базовый класс управления капиталом Часть 12
13 NewBarEvent.mqh 1.00  Класс определения нового бара для конкретного символа  Часть 8
14 Receiver.mqh 1.04  Базовый класс перевода открытых объемов в рыночные позиции  Часть 12
15 SimpleHistoryReceiverExpert.mq5 1.00 Упрощённый советник воспроизведения истории сделок   Часть 16
16 SimpleVolumesExpert.mq5 1.20 Советник для параллельной работы нескольких групп модельных стратегий. Параметры будут браться из встроенной библиотеки групп. Часть 17
17 SimpleVolumesStage3.mq5 1.00 Советник, сохраняющий сформированную нормированную группу стратегий в библиотеку групп с заданным именем. Часть 17
18 SimpleVolumesStrategy.mqh 1.09  Класс торговой стратегии с использованием тиковых объемов Часть 15
19 Strategy.mqh 1.04  Базовый класс торговой стратегии Часть 10
20 TesterHandler.mqh  1.03 Класс для обработки событий оптимизации  Часть 17 
21 VirtualAdvisor.mqh  1.06  Класс эксперта, работающего с виртуальными позициями (ордерами) Часть 15
22 VirtualChartOrder.mqh  1.00  Класс графической виртуальной позиции Часть 4  
23 VirtualFactory.mqh 1.04  Класс фабрики объектов  Часть 16
24 VirtualHistoryAdvisor.mqh 1.00  Класс эксперта воспроизведения истории сделок  Часть 16
25 VirtualInterface.mqh  1.00  Класс графического интерфейса советника  Часть 4  
26 VirtualOrder.mqh 1.04  Класс виртуальных ордеров и позиций  Часть 8
27 VirtualReceiver.mqh 1.03  Класс перевода открытых объемов в рыночные позиции (получатель)  Часть 12
28 VirtualRiskManager.mqh  1.02  Класс управления риском (риск-менеждер)  Часть 15
29 VirtualStrategy.mqh 1.05  Класс торговой стратегии с виртуальными позициями  Часть 15
30 VirtualStrategyGroup.mqh  1.00  Класс группы торговых стратегий или групп торговых стратегий Часть 11 
31 VirtualSymbolReceiver.mqh  1.00 Класс символьного получателя  Часть 3



Прикрепленные файлы |
MQL5.zip (79.94 KB)
Нейросети в трейдинге: Иерархический векторный Transformer (HiVT) Нейросети в трейдинге: Иерархический векторный Transformer (HiVT)
Предлагаем познакомиться с методом Иерархический Векторный Transformer (HiVT), который был разработан для быстрого и точного прогнозирования мультимодальных временных рядов.
Возможности Мастера MQL5, которые вам нужно знать (Часть 15): Метод опорных векторов с полиномом Ньютона Возможности Мастера MQL5, которые вам нужно знать (Часть 15): Метод опорных векторов с полиномом Ньютона
Метод опорных векторов (Support Vector Machines) классифицирует данные на основе предопределенных классов, исследуя эффекты увеличения их размерности. Это метод обучения с учителем, который довольно сложен, учитывая его потенциальную возможность работы с многомерными данными. В этой статье мы рассмотрим, как эффективнее реализовать базовую версию двумерных данных с помощью полинома Ньютона при классификации ценовых действий.
Факторизация матриц: основы Факторизация матриц: основы
Поскольку цель здесь дидактическая, мы будем действовать максимально просто. То есть мы будем реализовывать только то, что нам необходимо: умножение матриц. Вы сегодня увидите, что этого достаточно для симуляции умножения матрицы на скаляр. Самая существенная трудность, с которой многие сталкиваются при реализации кода с использованием матричной факторизации, заключается в следующем: в отличие от скалярной факторизации, где почти во всех случаях порядок факторов не меняет результат, при использовании матриц это не так.
Нейронная сеть на практике: Секущая прямая Нейронная сеть на практике: Секущая прямая
Как уже объяснялось в теоретической части, при работе с нейронными сетями нам необходимо использовать линейные регрессии и производные. Но почему? Причина заключается в том, что линейная регрессия - одна из самых простых существующих формул. По сути, линейная регрессия - это просто аффинная функция. Однако, когда мы говорим о нейронных сетях, нас не интересуют эффекты прямой линейной регрессии. Нас интересует уравнение, которое порождает данную прямую. Созданная прямая не имеет большого значения. Но знаете ли вы, какое главное уравнение мы должны понять? Если нет, то я вам рекомендую прочесть эту статью, чтобы начать разбираться в этом.