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

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

MetaTrader 5Тестер | 14 февраля 2025, 11:59
566 0
Yuriy Bykov
Yuriy Bykov

Введение

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

  1. Оптимизация одиночных экземпляров стратегий для конкретных комбинаций символов и таймфреймов.
  2. Формирование групп из лучших одиночных экземпляров, полученных на первом этапе.
  3. Генерация строки инициализации итогового советника, объединяющей сформированные группы, и её сохранение в библиотеке.

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

Мы также поискали способы повышения производительности, используя профилирование и оптимизацию кода. Основное внимание было уделено работе с объектами, организующими получение информации о торговых инструментах (символах). Это позволило значительно сократить количество вызовов методов для получения данных о ценах и спецификациях символов.

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

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


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

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

  1. Генерируется проект с текущей датой в качестве даты окончания периода оптимизации. 
  2. Проект запускается на конвейере. Его выполнение занимает некоторое время от нескольких дней — до нескольких недель.
  3. Результаты загружаются в итоговый советник. Если итоговый советник ещё не торговал, то он запускается на реальном счёте. Если он уже и так работал на счёте, то его параметры заменяются на новые, полученные после завершения прохождения конвейера последним проектом.
  4. Переходим к пункту 1.

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

Для второго пункта нам достаточно иметь терминал с установленным советником Optimization.ex5, у которого в параметрах указана нужная база данных. Как только в ней появятся новые невыполненные задачи проектов, они будут запущены на выполнение в порядке очереди. Последний этап, идущий перед этапом создания нового проекта, должен в каком-то виде передавать результаты оптимизации проекта итоговому советнику.

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

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

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

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

Похоже, что назревают более масштабные изменения, чем хотелось бы. Но что поделать, если не видно другого способа добиться желаемого результата, то лучше принять необходимость изменений раньше. Если продолжать двигаться в не совсем верном направлении, то, чем дальше, тем труднее будет перебираться с него на новую дорогу.

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

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


Трансформация строки инициализации

Поставленная задача довольно объёмная, поэтому, будем двигаться маленькими шагами. Начнём с того, что в базе данных советника нам понадобится хранить информацию об одиночных экземплярах торговых стратегий. Сейчас эта информация представлена в строке инициализации советника. Получить её советник может либо из базы данных оптимизации, либо из встроенных в код советника данных (строковых констант), взятых из библиотеки параметров на этапе компиляции. Первый способ используется в советниках оптимизации (SimpleVolumesStage2.mq5 и SimpleVolumesStage3.mq5), а второй способ — в итоговом советнике (SimpleVolumesExpert.mq5).

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

Чтобы понять, как нам разделить строку инициализации, посмотрим на её типичный пример из прошлой статьи. Она довольно большая (~200 строк), поэтому покажем только минимально необходимую часть, дающую представление о её структуре.

class CVirtualStrategyGroup([
    class CVirtualStrategyGroup([
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,1.00,1.30,80,3200.00,930.00,12000,3)
        ],8.428150),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,172,1.40,1.20,140,2200.00,1220.00,19000,3)
        ],12.357884),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,1.20,0.10,0,1800.00,780.00,8000,3)
        ],4.756016),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,172,0.30,0.10,150,4400.00,1000.00,1000,3)
        ],4.459508),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.50,1.10,200,2800.00,1030.00,32000,3)
        ],5.021593),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,172,1.40,1.70,100,200.00,1640.00,32000,3)
        ],18.155410),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.40,160,8400.00,1080.00,44000,3)
        ],4.313320),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,52,0.50,1.00,110,3600.00,1030.00,53000,3)
        ],4.490144),
    ],4.615527),
    class CVirtualStrategyGroup([
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.80,240,4800.00,1620.00,57000,3)
        ],6.805962),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,52,0.50,1.80,40,400.00,930.00,53000,3)
        ],11.825922),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,212,1.30,1.50,160,600.00,1000.00,28000,3)
        ],16.866251),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.30,1.50,30,3000.00,1280.00,28000,3)
        ],5.824790),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,1.30,0.10,10,2000.00,780.00,1000,3)
        ],3.476085),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.10,0,16000.00,700.00,11000,3)
        ],4.522636),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,52,0.40,1.80,80,2200.00,360.00,25000,3)
        ],8.206812),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.10,0,19200.00,700.00,44000,3)
        ],2.698618),
    ],5.362505),
    class CVirtualStrategyGroup([
        ...
    ],5.149065),
    
    ...
    
    class CVirtualStrategyGroup([
        ...
    ],2.718278),
],2.072066)

Эта строка инициализации состоит из вложенных групп торговых стратегий первого, второго и третьего уровня. Сами одиночные экземпляры торговых стратегий вложены только в группы третьего уровня. У каждого экземпляра указаны параметры. У каждой группы есть масштабирующий множитель, он есть на первом, втором и третьем уровне. Про применение масштабирующих множителей рассказывалось в части 5. Они нужны для нормировки максимальной просадки, достигаемой на тестовом периоде до значения 10%. Причём, значение масштабирующего множителя у группы, содержащей несколько вложенных групп, или несколько вложенных экземпляров стратегий, вначале делятся на количество элементов в этой группе, а затем, этот новый множитель применяется ко всем вложенным элементам. Вот как это выглядит в коде файла VirtualStrategyGroup.mqh:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CVirtualStrategyGroup::CVirtualStrategyGroup(string p_params) {
// Запоминаем строку инициализации
   m_params = p_params;

   ...

// Читаем масштабирующий множитель
   m_scale = ReadDouble(p_params);

// Исправляем его при необходимости
   if(m_scale <= 0.0) {
      m_scale = 1.0;
   }

   if(ArraySize(m_groups) > 0 && ArraySize(m_strategies) == 0) {
      // Если мы наполнили массив групп, а массив стратегий пустой, то
      PrintFormat(__FUNCTION__" | Scale = %.2f, total groups = %d", m_scale, ArraySize(m_groups));
      // Масштабируем все группы
      Scale(m_scale / ArraySize(m_groups));
   } else if(ArraySize(m_strategies) > 0 && ArraySize(m_groups) == 0) {
      // Если мы наполнили массив стратегий, а массив групп пустой, то
      PrintFormat(__FUNCTION__" | Scale = %.2f, total strategies = %d", m_scale, ArraySize(m_strategies));
      // Масштабируем все стратегии
      Scale(m_scale / ArraySize(m_strategies));
   } else {
      // Иначе сообщаем об ошибке в строке инициализации
      SetInvalid(__FUNCTION__, StringFormat("Groups or strategies not found in Params:\n%s", p_params));
   }
}

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

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


Экспорт списка стратегий

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

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

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

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

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

Заменим в файле SimpleVolumesStage3.mq5 вызов функции, которая выполняла экспорт в таком виде, на вызов другой (пока ещё отсутствующей) функции:

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

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

Мы добавим новый метод Export() к классу эксперта CVirtualAdvisor. В качестве параметров ему будет передаваться имя новой группы и имя файла базы данных эксперта, в которую надо выполнить экспорт. Обратим внимание, что это уже новая база данных, а не используемая ранее база данных оптимизации. Для задания значения этому аргументу, мы добавим входной параметр советника третьего этапа:

input group "::: Сохранение в библиотеку"
input string groupName_  = "SimpleVolumes_v.1.20_2023.01.01";      // - Название версии (если пустое - не сохранять)
input string advFileName_  = "SimpleVolumes-27183.test.db.sqlite"; // - Название базы данных эксперта

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

//+------------------------------------------------------------------+
//| Экспорт текущей группы стратегий в заданную базу данных эксперта |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Export(string p_groupName, string p_advFileName) {
   CTesterHandler::Export(m_strategies, p_groupName, p_advFileName);
}

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


Доступ к разным базам данных

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

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

Создадим три файла для хранения SQL-кода создания баз данных каждого типа, подключим их в качестве ресурсов к файлу Database.mqh и создадим перечисление для трёх типов баз данных:

// Импорт sql-файлов создания структуры БД разных типов
#resource "db.opt.schema.sql" as string dbOptSchema
#resource "db.cut.schema.sql" as string dbCutSchema
#resource "db.adv.schema.sql" as string dbAdvSchema

// Тип базы данных
enum ENUM_DB_TYPE {
   DB_TYPE_OPT,   // БД оптимизации
   DB_TYPE_CUT,   // БД для подбора групп (урезанная БД оптимизации)
   DB_TYPE_ADV,   // БД эксперта (итогового советника)
};

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

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

//+------------------------------------------------------------------+
//| Создание пустой БД                                               |
//+------------------------------------------------------------------+
void CDatabase::Create(string p_schema) {
   bool res = Execute(p_schema);
   if(res) {
      PrintFormat(__FUNCTION__" | Database successfully created from %s", "db.*.schema.sql");
   }
}

//+------------------------------------------------------------------+
//| Проверка подключения к базе данных с заданным именем             |
//+------------------------------------------------------------------+
bool CDatabase::Connect(string p_fileName, ENUM_DB_TYPE p_dbType = DB_TYPE_OPT) {
// Если база данных открыта, то закроем её
   Close();

// Если задано имя файла, то запомним его
   s_fileName = p_fileName;

// Установим флаг общей папки для БД оптимизации и эксперта
   s_common = (p_dbType != DB_TYPE_CUT ? DATABASE_OPEN_COMMON : 0);

// Открываем базу данных
// Пробуем открыть существующий файл БД
   s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | s_common);

// Если файл БД не найден, то пытаемся создать его при открытии
   if(!IsOpen()) {
      s_db = DatabaseOpen(s_fileName,
                          DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | s_common);

      // Сообщаем об ошибке при неудаче
      if(!IsOpen()) {
         PrintFormat(__FUNCTION__" | ERROR: %s Connect failed with code %d",
                     s_fileName, GetLastError());
         return false;
      }
      if(p_dbType == DB_TYPE_OPT) {
         Create(dbOptSchema);
      } else if(p_dbType == DB_TYPE_CUT) {
         Create(dbCutSchema);
      } else {
         Create(dbAdvSchema);
      }
   }

   return true;
}

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


База данных эксперта

Для сохранения информации о сформированных группах стратегий в базе данных эксперта, было решено использовать две таблицы: strategy_groups и strategies со следующей структурой:

CREATE TABLE strategies (
    id_strategy INTEGER PRIMARY KEY AUTOINCREMENT
                        NOT NULL,
    id_group    INTEGER REFERENCES strategy_groups (id_group) ON DELETE CASCADE
                                                              ON UPDATE CASCADE,
    hash        TEXT    NOT NULL,
    params      TEXT    NOT NULL
);

CREATE TABLE strategy_groups (
    id_group    INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT,
    from_date   TEXT,
    to_date     TEXT,
    create_date TEXT
);

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

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

Поле params в таблице strategies будет хранить строку инициализации одиночного экземпляра торговой стратегии. Из него можно будет сформировать общую строку инициализации всей группы стратегий для создания объекта эксперта (класса CVirtualAdvisor) в итоговом советнике.

Поля from_date и to_date в таблице strategy_groups будут в дальнейшем хранить даты начала и окончания интервала оптимизации, использованного для получения данной группы. А пока будут просто пустыми.


Снова экспорт стратегий

Теперь мы готовы реализовать метод экспорта группы стратегий в базу данных эксперта в файле TesterHandler.mqh. Для этого необходимо подключиться к нужной базе данных, создать запись для новой группы стратегий в таблице strategy_groups, сформировать для каждой стратегии из группы строку инициализации с её текущим нормирующим множителем (оборачивая в "class CVirtualStrategyGroup([strategy], scale)") и сохранить их в таблице strategies.

//+------------------------------------------------------------------+
//| Экспорт массива стратегий в заданную базу данных эксперта        |
//| как новой группы стратегий                                       |
//+------------------------------------------------------------------+
void CTesterHandler::Export(CStrategy* &p_strategies[], string p_groupName, string p_advFileName) {
// Подключаемся к нужной базе данных эксперта
   if(DB::Connect(p_advFileName, DB_TYPE_ADV)) {

      string fromDate = "";   // Дата начала интервала оптимизации
      string toDate = "";     // Дата конца  интервала оптимизации

      // Создаём запись для новой группы стратегий
      string query = StringFormat("INSERT INTO strategy_groups VALUES(NULL, '%s', '%s', '%s', NULL) RETURNING rowid;",
                                  p_groupName, fromDate, toDate);
      ulong groupId = DB::Insert(query);

      PrintFormat(__FUNCTION__" | Export %d strategies into new group [%s] with ID=%I64u",
                  ArraySize(p_strategies), p_groupName, groupId);

      // Для каждой стратегии
      FOREACH(p_strategies, {
         CVirtualStrategy *strategy = p_strategies[i];
         // Формируем строку инициализации в виде группы из одной стратегии с нормирующим множителем
         string params = StringFormat("class CVirtualStrategyGroup([%s],%0.5f)",
                                      ~strategy,
                                      strategy.Scale());
                                      
         // Сохраняем её в базе данных эксперта с указанием нового идентификатора группы
         string query = StringFormat("INSERT INTO strategies "
                                     "VALUES (NULL, %I64u, '%s', '%s')",
                                     groupId, strategy.Hash(~strategy), params);
         DB::Execute(query);
      });

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

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

Теперь, если мы повторно запустим на выполнение третьи этапы проектов оптимизации, то увидим, что в таблице strategies появились записи с одиночными экземплярами торговых стратегий:

А в таблице strategy_group появились записи об итоговых группах для каждого проекта:

Итак, с экспортом разобрались, теперь перейдём к обратной операции — импорту этих групп в итоговом советнике.


Импорт стратегий

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

Возьмём наш итоговый советник SimpleVolumesExpert.mq5 и добавим новый входной параметр newGroupId_, через который можно будет задать значение идентификатора группы стратегий из новой библиотеки:

input group "::: Использовать группу стратегий"
input ENUM_GROUPS_LIBRARY groupId_     = -1// - Группа из старой библиотеки ИЛИ:
input int                 newGroupId_  = 0// - ID группы из новой библиотеки (0 - последняя)

Добавим константу для названия итогового советника:

#define __NAME__ "SimpleVolumes"

В функции инициализации итогового советника мы сначала проверим, не выбрана ли какая-то группа из старой библиотеки в параметре groupId_. Если нет, то тогда будем получать строку инициализации из новой библиотеки. Для этого мы добавили к классу эксперта CVirtualAdvisor два новых статических метода: FileName() и Import(). Их можно вызывать до момента создания объекта эксперта.

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

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

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

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

// ...

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

Дальнейшие изменения будем производить в файле VirtualAdvisor.mqh. Добавим два упомянутых выше метода:

//+------------------------------------------------------------------+
//| Класс эксперта, работающего с виртуальными позициями (ордерами)  |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   // ...
public:
   // ...

   // Имя файла с базой данных эксперта
   static string     FileName(string p_name, ulong p_magic = 1);
   
   // Получение строки инициализации группы стратегий 
   // из базы данных эксперта с заданным идентификатором
   static string     Import(string p_fileName, int p_groupId = 0);
   
};

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

//+------------------------------------------------------------------+
//| Имя файла с базой данных эксперта                                |
//+------------------------------------------------------------------+
string CVirtualAdvisor::FileName(string p_name, ulong p_magic = 1) {
   return StringFormat("%s-%d%s.db.sqlite",
                       (p_name != "" ? p_name : "Expert"),
                       p_magic,
                       (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                      );
}

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

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

//+------------------------------------------------------------------+
//| Получение строки инициализации группы стратегий                  |
//| из базы данных эксперта с заданным идентификатором               |
//+------------------------------------------------------------------+
string CVirtualAdvisor::Import(string p_fileName, int p_groupId = 0) {
   string params[];   // Массив для строк инициализации стратегий
   
   // Запрос на получение стратегий заданной группы либо последней группы
   string query = StringFormat("SELECT id_group, params "
                               "  FROM strategies"
                               " WHERE id_group = %s;",
                               (p_groupId > 0 ? (string) p_groupId 
                                : "(SELECT MAX(id_group) FROM strategy_groups)"));

// Открываем базу данных эксперта
   if(DB::Connect(p_fileName, DB_TYPE_ADV)) {
      // Выполняем запрос
      int request = DatabasePrepare(DB::Id(), query);

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

         // Читаем данные из первой строки результата
         while(DatabaseReadBind(request, row)) {
            // Запоминаем идентификатор группы стратегий 
            // в статическом свойстве класса эксперта
            s_groupId = row.groupId;
            
            // Добавляем очередную строку инициализации стратегии в массив
            APPEND(params, row.params);
         }
      } else {
         // Сообщаем об ошибке при необходимости
         PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", 
                     query, GetLastError());
      }

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

   // Строка инициализации группы стратегий
   string groupParams = NULL;

   // Общее количество стратегий в группе
   int totalStrategies = ArraySize(params);
   
   // Если стратегии есть, то
   if(totalStrategies > 0) {
      // Соединяем их строки инициализации через запятую
      JOIN(params, groupParams, ",");
      
      // Создаём строку инициализации группы стратегий
      groupParams = StringFormat("class CVirtualStrategyGroup([%s], %.5f)",
                                 groupParams,
                                 totalStrategies);
   }

   // Возвращаем строку инициализации группы стратегий
   return groupParams;
}

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


Перенос данных итогового советника

Раз уж мы организовали отдельную базу данных для хранения параметров создания используемых итоговым советником одиночных экземпляров торговых стратегий, то не будем останавливаться на половине пути и перенесём хранение остальной информации о работе итогового советника на торговом счёте в эту же базу данных. До этого, такая информация сохранялась в отдельный файл методом CVitrualAdvisor::Save() и могла загружаться из него при необходимости методом CVitrualAdvisor::Load().

К сохраняемой в файл информации относятся:

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

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

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


Хранилище Key-Value

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

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

  • Соединения с нужной базой данных: Connect()/Close()
  • Установки значений разных типов: Set(...)
  • Чтения значений разных типов: Get(...)
Вот что в итоге получилось:

//+------------------------------------------------------------------+
//| Класс для работы с базой данных эксперта в виде                  |
//| хранилища Key-Value для свойств и виртуальных позиций            |
//+------------------------------------------------------------------+
class CStorage {
protected:  
   static bool       s_res; // Результат всех операций чтения/записи базы данных
public:
   // Подключение к базе данных эксперта
   static bool       Connect(string p_fileName);
   
   // Закрытие подключения к базе данных
   static void       Close();

   // Сохранение виртуального ордера/позиции
   static void       Set(int i, CVirtualOrder* order);

   // Сохранение одного значения произвольного простого типа
   template<typename T>
   static void       Set(string key, const T &value);

   // Сохранение массива значений произвольного простого типа
   template<typename T>
   static void       Set(string key, const T &values[]);

   // Получение значения в виде строки по заданному ключу
   static string     Get(string key);

   // Получение массива виртуальных ордеров/позиций по заданному хешу стратегии
   static bool       Get(string key, CVirtualOrder* &orders[]);

   // Получение значения по заданному ключу в переменную произвольного простого типа
   template<typename T>
   static bool       Get(string key, T &value);

   // Получение массива значений простого типа по заданному ключу в переменную
   template<typename T>
   static bool       CStorage::Get(string key, T &values[]);

   // Результат операций
   static bool       Res() {
      return s_res;
   }
};

Мы добавили к классу статическое свойство s_res и метод чтения его значения. Оно будет хранить признак возникновения любой ошибки при операциях чтения/записи базы данных.

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

//+------------------------------------------------------------------+
//| Подключение к базе данных эксперта                               |
//+------------------------------------------------------------------+
bool CStorage::Connect(string p_fileName) {
   // Подключаемся к базе данных эксперта
   if(DB::Connect(p_fileName, DB_TYPE_ADV)) {
      // Устанавливаем, что пока ошибок нет
      s_res = true;
      
      // Начинаем транзакцию
      DatabaseTransactionBegin(DB::Id());
      
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Закрытие подключения к базе данных                               |
//+------------------------------------------------------------------+
void CStorage::Close() {
   // Если ошибок нет, то
   if(s_res) {
      // Подтверждаем транзакцию
      DatabaseTransactionCommit(DB::Id());
   } else {
      // Иначе транзакцию отменяем
      DatabaseTransactionRollback(DB::Id());
   }
   
   // Закрываем соединение с базой данных
   DB::Close();
}

Добавим в структуру базы данных итогового советника ещё две таблицы с таким набором столбцов:

Первая таблица (strorage) будет использоваться для хранения отдельных числовых значений и массивов числовых значений. Строки, впрочем, там тоже можно хранить. Вторая таблица (storage_orders) будет использоваться для хранения информации об элементах массивов виртуальных позиций для различных экземпляров торговых стратегий. Поэтому в начале таблице идут столбцы strategy_hash и strategy_index, в которых хранится хеш-значение параметров стратегии (уникальное для каждой стратегии) и индекс виртуальной позиции в массиве виртуальных позиций стратегии.

Все отдельные числовые значения сохраняются посредством вызова шаблонного метода Set(), который принимает в качестве параметров строку с названием ключа и переменную произвольно простого типа T. Это может быть, например, int, ulong или double. При формировании SQL-запроса на сохранение значение этой переменной приводится к типу string и хранится в базе данных в виде строки:

//+------------------------------------------------------------------+
//| Сохранение одного значения произвольного простого типа           |
//+------------------------------------------------------------------+
template<typename T>
void CStorage::Set(string key, const T &value) {
// Экранируем символы одинарных кавычек (пока не можно не использовать)
// StringReplace(key, "'", "\\'");
// StringReplace(value, "'", "\\'");

// Запрос на сохранение значения
   string query = StringFormat("REPLACE INTO storage(key, value) VALUES('%s', '%s');",
                               key, (string) value);

// Выполняем запрос
   s_res &= DatabaseExecute(DB::Id(), query);

   if(!s_res) {
      // Сообщаем об ошибке при необходимости
      PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n"
                  "%s\n"
                  "error code = %d",
                  query, GetLastError());
   }
}

В том случае, когда мы хотим для одного ключа сохранить массив значений простого типа, то предварительно создаём из всех значений переданного массива строку с разделителем. В качестве разделителя используется символ запятой. Это происходит в другом шаблонном методе с таким же названием Set(), только вторым параметром у него является не ссылка на переменную простого типа, а ссылка на массив значений простого типа:

//+------------------------------------------------------------------+
//| Сохранение массива значений произвольного простого типа          |
//+------------------------------------------------------------------+
template<typename T>
void CStorage::Set(string key, const T &values[]) {
   string value = "";
   
   // Соединяем все значения из массива в одну строку через запятую
   JOIN(values, value, ",");
   
   // Сохраняем строку с заданным ключом
   Set(key, value);
}

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

//+------------------------------------------------------------------+
//| Получение значения в виде строки по заданному ключу              |
//+------------------------------------------------------------------+
string CStorage::Get(string key) {
   string value = NULL; // Возвращаемое значение

// Запрос на получение значения
   string query = StringFormat("SELECT value FROM storage WHERE key='%s'", key);

// Выполняем запрос
   int request = DatabasePrepare(DB::Id(), query);

// Если нет ошибки
   if(request != INVALID_HANDLE) {
      // Читаем данные из первой строки результата
      DatabaseRead(request);

      if(!DatabaseColumnText(request, 0, value)) {
         // Сообщаем об ошибке при необходимости
         PrintFormat(__FUNCTION__" | ERROR: Reading row in DB [adv] for request \n%s\n"
                     "failed with code %d",
                     query, GetLastError());
      }
   } else {
      // Сообщаем об ошибке при необходимости
      PrintFormat(__FUNCTION__" | ERROR: Request in DB [adv] \n%s\nfailed with code %d",
                  query, GetLastError());
   }

   return value;
}

//+------------------------------------------------------------------+
//| Получение значения по заданному ключу в переменную               |
//| произвольного простого типа                                      |
//+------------------------------------------------------------------+
template<typename T>
bool CStorage::Get(string key, T &value) {
// Получаем значение в виде строки
   string res = Get(key);

// Если значение получено
   if(res != NULL) {
      // Приводим его к типу Т и присваиваем целевой переменной
      value = (T) res;
      return true;
   }
   return false;
}

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


Сохранение и загрузка советника

В методе сохранения состояния советника CVirtualAdvisor::Save() нам достаточно подключиться к базе данных эксперта и сохранить всё необходимое, вызывая либо напрямую методы класса CStorage, либо опосредовано через вызов методов Save()/Load() у тех объектов, которые нуждаются в сохранении.

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

//+------------------------------------------------------------------+
//| Сохранение состояния                                             |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Save() {
// Сохраняем состояние, если:
   if(true
// появились более поздние изменения
         && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime
// и сейчас не оптимизация
         && !MQLInfoInteger(MQL_OPTIMIZATION)
// и сейчас не тестирование либо сейчас визуальное тестирование
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      // Если подключение к базе данных эксперта установлено
      if(CStorage::Connect(m_fileName)) {
         // Сохраняем время последних изменений
         CStorage::Set("CVirtualReceiver::s_lastChangeTime", CVirtualReceiver::s_lastChangeTime);
         CStorage::Set("CVirtualAdvisor::s_groupId", CVirtualAdvisor::s_groupId);

         // Сохраняем все стратегии
         FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save());

         // Сохраняем риск-менеджер
         m_riskManager.Save();

         // Обновляем время последнего сохранения
         m_lastSaveTime = CVirtualReceiver::s_lastChangeTime;
         PrintFormat(__FUNCTION__" | OK at %s to %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS),
                     m_fileName);

         // Закрываем соединение
         CStorage::Close();

         // Возвращаем результат
         return CStorage::Res();
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Can't open database [%s], LastError=%d",
                     m_fileName, GetLastError());
         return false;
      }
   }
   return true;
}

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

К моменту вызова метода загрузки, объект эксперта уже создан с группой стратегий, идентификатор которой берётся из входных параметров советника. Этот идентификатор сохранился внутри метода CVirtualAdvisor::Import() в статическом свойстве CVirtualAdvisor::s_groupId. Поэтому, при загрузке идентификатора группы стратегий из базы данных эксперта, у нас есть возможность сравнить его с уже имеющимся значением. Если они отличаются, значит итоговый советник перезапущен с новой группой стратегий, и, возможно, нуждается в каких-то дополнительных действиях. Но пока что не совсем ясно, какие действия нам обязательно надо будет выполнять в этом случае. Так что просто оставим в коде соответствующий комментарий на будущее.

//+------------------------------------------------------------------+
//| Загрузка состояния                                               |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Load() {
   bool res = true;
   ulong groupId = 0;

// Загружаем состояние, если:
   if(true
// файл существует
         && FileIsExist(m_fileName, FILE_COMMON)
// и сейчас не оптимизация
         && !MQLInfoInteger(MQL_OPTIMIZATION)
// и сейчас не тестирование либо сейчас визуальное тестирование
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      // Если подключение к базе данных эксперта установлено
      if(CStorage::Connect(m_fileName)) {
         // Загружаем время последних изменений
         res &= CStorage::Get("CVirtualReceiver::s_lastChangeTime", m_lastSaveTime);

         // Загружаем идентификатор сохранённой группы стратегий
         res &= CStorage::Get("CVirtualAdvisor::s_groupId", groupId);

         // Если время последних изменений находится в будущем, то игнорируем загрузку
         if(m_lastSaveTime > TimeCurrent()) {
            PrintFormat(__FUNCTION__" | IGNORE LAST SAVE at %s in the future",
                        TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));
            m_lastSaveTime = 0;
            return true;
         }

         PrintFormat(__FUNCTION__" | LAST SAVE at %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

         if(groupId != CVirtualAdvisor::s_groupId) {
            // Действия при запуске эксперта с новой группой стратегий.
            // Пока тут ничего не происходит
         }

         // Загружаем все стратегии
         FOREACH(m_strategies, {
            res &= ((CVirtualStrategy*) m_strategies[i]).Load();
            if(!res) break;
         });

         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_fileName);
         }

         // Загружаем риск-менеджер
         res &= m_riskManager.Load();

         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_fileName);
         }

         // Закрываем соединение
         CStorage::Close();

         return res;
      }
   }

   return true;
}

Теперь спустимся на уровень ниже и посмотрим на реализацию методов сохранения и загрузки стратегий.


Сохранение и загрузка стратегии

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

//+------------------------------------------------------------------+
//| Сохранение состояния                                             |
//+------------------------------------------------------------------+
void CVirtualStrategy::Save() {
// Сохраняем виртуальные позиции (ордера) стратегии
   FOREACH(m_orders, CStorage::Set(i, m_orders[i]));
}

//+------------------------------------------------------------------+
//| Загрузка состояния                                               |
//+------------------------------------------------------------------+
bool CVirtualStrategy::Load() {
   bool res = true;
   
// Загружаем виртуальные позиции (ордера) стратегии
   res = CStorage::Get(this.Hash(), m_orders);

   return res;
}

Для потомков класса CVirtualStrategy (к которым относится CSimpleVolumnesStrategy), помимо сохранения массива виртуальных позиций, может потребоваться сохранять и другую информацию. Наша модельная стратегия слишком проста и не требует сохранения ничего, кроме списка виртуальных позиций. Но представим, что нам для чего-то захотелось сохранять массив тиковых объёмов и значение среднего тикового объёма. Поскольку методы сохранения и загрузки объявлены виртуальными, то мы можем переопределить их в классах-наследниках, добавив работу с нужными данными и вызвав методы базового класса для сохранения и загрузки виртуальных позиций:

//+------------------------------------------------------------------+
//| Сохранение состояния                                             |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::Save() {
   double avrVolume = ArrayAverage(m_volumes);

// Сформируем общую часть ключа с типом и хешем стратегии
   string key = "CSimpleVolumesStrategy[" + this.Hash() + "]";

// Сохраняем средний тиковый объём
   CStorage::Set(key + ".avrVolume", avrVolume);

// Сохраняем массив тиковых объёмов
   CStorage::Set(key + ".m_volumes", m_volumes);

// Вызываем метод базового класса (для сохранения виртуальных позиций)
   CVirtualStrategy::Save();
}

//+------------------------------------------------------------------+
//| Загрузка состояния                                               |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::Load() {
   bool res = true;

   double avrVolume = 0;

// Сформируем общую часть ключа с типом и хешем стратегии
   string key = "CSimpleVolumesStrategy[" + this.Hash() + "]";

// Загружаем массив тиковых объёмов
   res &= CStorage::Get(key + ".avrVolume", avrVolume);

// Загружаем массив тиковых объёмов
   res &= CStorage::Get(key + ".m_volumes", m_volumes);

// Вызываем метод базового класса (для загрузки виртуальных позиций)
   res &= CVirtualStrategy::Load();

   return res;
}

Остаётся только реализовать сохранение и загрузку виртуальных позиций.


Сохранение/загрузка виртуальных позиций

Ранее в классе виртуальных позиций методы Save() и Load() непосредственно выполняли сохранение нужной информации о текущем объекте виртуальной позиции в файл данных. Теперь мы немного поменяем схему. Добавим простую структуру CVirtualOrderStruct, в которой будут поля для всех нужных данных виртуальной позиции:

// Структура для чтения/записи из БД 
// основных свойств виртуального ордера/позиции
struct VirtualOrderStruct {
   string            strategyHash;
   int               strategyIndex;
   ulong             ticket;
   string            symbol;
   double            lot;
   ENUM_ORDER_TYPE   type;
   datetime          openTime;
   double            openPrice;
   double            stopLoss;
   double            takeProfit;
   datetime          closeTime;
   double            closePrice;
   datetime          expiration;
   string            comment;
   double            point;
};

В отличие от объектов виртуальных позиций, для которых ведётся строгий учёт всех созданных экземпляров и автоматическая обработка их в модуле получателя торговых объемов, такие структуры можно создавать когда и сколько угодно. Мы будем использовать их для передачи информации между объектами виртуальных позиций и методов их сохранения/загрузки в базе данных эксперта, реализованных в классе CStorage. Тогда методы сохранения и загрузки в классе самих виртуальных позиций будут только заполнять переданную структуру или брать значения полей переданной структуры для записи в свои свойства:

//+------------------------------------------------------------------+
//| Загрузка состояния                                               |
//+------------------------------------------------------------------+
void CVirtualOrder::Load(const VirtualOrderStruct &o) {
   m_ticket = o.ticket;
   m_symbol = o.symbol;
   m_lot = o.lot;
   m_type = o.type;
   m_openPrice = o.openPrice;
   m_stopLoss = o.stopLoss;
   m_takeProfit = o.takeProfit;
   m_openTime = o.openTime;
   m_closePrice = o.closePrice;
   m_closeTime = o.closeTime;
   m_expiration = o.expiration;
   m_comment = o.comment;
   m_point = o.point;

   PrintFormat(__FUNCTION__" | %s", ~this);

   s_ticket = MathMax(s_ticket, m_ticket);
   
   m_symbolInfo = m_symbols[m_symbol];

// Оповещаем получатель и стратегию, что позиция (ордер) открыта
   if(IsOpen()) {
      m_receiver.OnOpen(&this);
      m_strategy.OnOpen(&this);
   } else {
      m_receiver.OnClose(&this);
      m_strategy.OnClose(&this);
   }
}

//+------------------------------------------------------------------+
//| Сохранение состояния                                             |
//+------------------------------------------------------------------+
void CVirtualOrder::Save(VirtualOrderStruct &o) {
   o.ticket = m_ticket;
   o.symbol = m_symbol;
   o.lot = m_lot;
   o.type = m_type;
   o.openPrice = m_openPrice;
   o.stopLoss = m_stopLoss;
   o.takeProfit = m_takeProfit;
   o.openTime = m_openTime;
   o.closePrice = m_closePrice;
   o.closeTime = m_closeTime;
   o.expiration = m_expiration;
   o.comment = m_comment;
   o.point = m_point;
}

И наконец-то воспользуемся созданной таблицей storage_orders в базе данных эксперта для сохранения туда свойств каждой виртуальной позиции. Работу с ней будет проводить метод CStorage::Set(), которому надо передать индекс виртуальной позиции и сам объект виртуальной позиции:

//+------------------------------------------------------------------+
//| Сохранение виртуального ордера/позиции                           |
//+------------------------------------------------------------------+
void CStorage::Set(int i, CVirtualOrder* order) {
   VirtualOrderStruct o;   // Структура для информации о виртуальной позиции
   order.Save(o);          // Наполняем её

// Экранируем кавычки в комментарии
   StringReplace(o.comment, "'", "\\'");

// Запрос на сохранение
   string query = StringFormat("REPLACE INTO storage_orders VALUES("
                               "'%s',%d,%I64u,"
                               "'%s',%.2f,%d,%I64d,%f,%f,%f,%I64d,%f,%I64d,'%s',%f);",
                               order.Strategy().Hash(), i, o.ticket,
                               o.symbol, o.lot, o.type,
                               o.openTime, o.openPrice,
                               o.stopLoss, o.takeProfit,
                               o.closeTime, o.closePrice,
                               o.expiration, o.comment,
                               o.point);

// Выполняем запрос
   s_res &= DatabaseExecute(DB::Id(), query);

   if(!s_res) {
      // Сообщаем об ошибке при необходимости
      PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n"
                  "%s\n"
                  "error code = %d",
                  query, GetLastError());
   }
}

Метод CStorage::Get(), которому вторым аргументом передаётся массив объектов виртуальных позиций, будет загружать из таблицы storage_orders информацию о виртуальных позициях стратегии с заданным в первом аргументе хеш-значением:

//+------------------------------------------------------------------+
//| Получение массива виртуальных ордеров/позиций                    |
//| по заданному хешу стратегии                                      |
//+------------------------------------------------------------------+
bool CStorage::Get(string key, CVirtualOrder* &orders[]) {
// Запрос на получение данных о виртуальных позициях
   string query = StringFormat("SELECT * FROM storage_orders "
                               " WHERE strategy_hash = '%s' "
                               " ORDER BY strategy_index ASC;",
                               key);

// Выполняем запрос
   int request = DatabasePrepare(DB::Id(), query);

// Если нет ошибки
   if(request != INVALID_HANDLE) {
      // Структура для информации о виртуальной позиции 
      VirtualOrderStruct row;
      
      // Читаем построчно данные из результата запроса
      while(DatabaseReadBind(request, row)) {
         orders[row.strategyIndex].Load(row);
      }
   } else {
      // Запоминаем ошибку и сообщаем об ней при необходимости
      s_res = false;
      PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n"
                  "%s\n"
                  "error code = %d",
                  query, GetLastError());
   }

   return s_res;
}

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


Небольшое тестирование

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

Для этого мы выполнили экспорт массива строк инициализации из базы данных оптимизации старым и новым способом. Теперь информация о четырёх группах стратегий присутствует как в файле ExportedGroupsLibrary.mqh, так и в базе данных эксперта, которая носит название SimpleVolumes-27183.test.db.sqlite. Скомпилируем файл с кодом итогового советника SimpleVolumesExpert.mq5.

Если мы зададим значения входных параметров таким образом,

то будет использоваться загрузка выбранной строки инициализации из внутреннего массива итогового советника. Этот массив наполнялся при компиляции из данных, расположенных в файле ExportedGroupsLibrary.mqh (старый способ).

Если же значения параметров указать таким образом,

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

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

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

Теперь запустим итоговый советник с новым способом инициализации на том же временном интервале. Результаты получились такие:

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

Как видно, результаты, полученные с использованием старого и нового способа, полностью совпадают.


Заключение

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

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

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

Важное предупреждение:

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


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

#
 Имя
Версия Описание  Последние изменения
MQL5/Experts/Article.16452
1Advisor.mqh1.04Базовый класс экспертаЧасть 10
2ClusteringStage1.py1.01Программа кластеризации результатов первого этапа оптимизацииЧасть 20
3CreateProject.mq51.00Советник-скрипт создания проекта с этапами, работами и задачами оптимизации. Часть 21
4Database.mqh1.10Класс для работы с базой данныхЧасть 22
5db.adv.schema.sql1.00
Схема базы данных итогового советникаЧасть 22
6db.cut.schema.sql
1.00Схема урезанной базы данных оптимизации
Часть 22
7db.opt.schema.sql
1.05 Схема базы данных оптимизации
Часть 22
8ExpertHistory.mqh1.00Класс для экспорта истории сделок в файлЧасть 16
9ExportedGroupsLibrary.mqh
Генерируемый файл с перечислением имён групп стратегий и массивом их строк инициализацииЧасть 22
10Factorable.mqh1.03Базовый класс объектов, создаваемых из строкиЧасть 22
11GroupsLibrary.mqh1.01Класс для работы с библиотекой отобранных групп стратегийЧасть 18
12HistoryReceiverExpert.mq51.00Советник воспроизведения истории сделок с риск-менеджеромЧасть 16  
13HistoryStrategy.mqh 1.00Класс торговой стратегии воспроизведения истории сделок Часть 16
14Interface.mqh1.00Базовый класс визуализации различных объектовЧасть 4
15LibraryExport.mq51.01Советник, сохраняющий строки инициализации выбранных проходов из библиотеки в файл ExportedGroupsLibrary.mqhЧасть 18
16Macros.mqh1.05Полезные макросы для операций с массивамиЧасть 22   
17Money.mqh1.01 Базовый класс управления капиталомЧасть 12
18NewBarEvent.mqh1.00 Класс определения нового бара для конкретного символа Часть 8
19Optimization.mq5 1.04Советник, управляющей запуском задач оптимизацииЧасть 22
20Optimizer.mqh1.03Класс для менеджера автоматической оптимизации проектовЧасть 22
21OptimizerTask.mqh1.03Класс для задачи оптимизацииЧасть 22
22Receiver.mqh1.04 Базовый класс перевода открытых объемов в рыночные позиции Часть 12
23SimpleHistoryReceiverExpert.mq51.00Упрощённый советник воспроизведения истории сделок  Часть 16
24SimpleVolumesExpert.mq51.21Итоговый советник для параллельной работы нескольких групп модельных стратегий. Параметры будут браться из встроенной библиотеки групп.Часть 22
25SimpleVolumesStage1.mq5
1.18Советник оптимизации одиночного экземпляра торговой стратегии (Этап 1) Часть 19
26SimpleVolumesStage2.mq5
1.02Советник оптимизации группы экземпляров торговых стратегий (Этап 2)
Часть 19
27SimpleVolumesStage3.mq51.03Советник, сохраняющий сформированную нормированную группу стратегий в библиотеку групп с заданным именем.Часть 22
28SimpleVolumesStrategy.mqh1.11 Класс торговой стратегии с использованием тиковых объемовЧасть 22
29Storage.mqh 1.00Класс работы с хранилищем Key-Value для итогового советникаЧасть 22
30Strategy.mqh1.04 Базовый класс торговой стратегииЧасть 10
31SymbolsMonitor.mqh 1.00Класс получения информации о торговых инструментах (символах)Часть 21
32TesterHandler.mqh 1.06Класс для обработки событий оптимизации Часть 22  
33VirtualAdvisor.mqh 1.09 Класс эксперта, работающего с виртуальными позициями (ордерами)Часть 22
34VirtualChartOrder.mqh 1.01 Класс графической виртуальной позицииЧасть 18  
35VirtualFactory.mqh1.04 Класс фабрики объектов Часть 16
36VirtualHistoryAdvisor.mqh1.00 Класс эксперта воспроизведения истории сделок Часть 16
37VirtualInterface.mqh 1.00 Класс графического интерфейса советника Часть 4  
38VirtualOrder.mqh1.09 Класс виртуальных ордеров и позиций Часть 22
39VirtualReceiver.mqh1.03 Класс перевода открытых объемов в рыночные позиции (получатель) Часть 12
40VirtualRiskManager.mqh 1.02 Класс управления риском (риск-менеждер) Часть 15
41VirtualStrategy.mqh1.08 Класс торговой стратегии с виртуальными позициями Часть 22
42VirtualStrategyGroup.mqh 1.00 Класс группы торговых стратегий или групп торговых стратегийЧасть 11 
43VirtualSymbolReceiver.mqh 1.00Класс символьного получателя Часть 3
 MQL5/Common/Files Общая папка терминалов  
44SimpleVolumes-27183.test.db.sqliteБаза данных эксперта с добавленными четырьмя группами стратегий 
Прикрепленные файлы |
MQL5.zip (757.65 KB)
Упрощаем торговлю на новостях (Часть 3): Совершаем сделки Упрощаем торговлю на новостях (Часть 3): Совершаем сделки
В этой статье наш советник новостной торговли начнет открывать сделки на основе экономического календаря, хранящегося в нашей базе данных. Кроме того, мы улучшим графику советника, чтобы отображать более актуальную информацию о предстоящих событиях экономического календаря.
Алгоритм поиска по кругу — Circle Search Algorithm (CSA) Алгоритм поиска по кругу — Circle Search Algorithm (CSA)
В статье представлен новый метаэвристический алгоритм оптимизации CSA (Circle Search Algorithm), основанный на геометрических свойствах окружности. Алгоритм использует принцип движения точек по касательным для поиска оптимального решения, сочетая фазы глобального исследования и локальной эксплуатации.
Разработка системы репликации (Часть 67): Совершенствуем индикатор управления Разработка системы репликации (Часть 67): Совершенствуем индикатор управления
В данной статье мы рассмотрим, чего можно добиться с помощью небольшой доработки кода. Данная доработка направлена на упрощение нашего кода, более активное использование вызовов библиотеки MQL5 и, прежде всего, на то, чтобы сделать его гораздо более стабильным, безопасным и простым для использования в другом коде, который мы будем разрабатывать в будущем.
От начального до среднего уровня: Массивы и строки (I) От начального до среднего уровня: Массивы и строки (I)
В сегодняшней статье мы начнем изучать некоторые особые типы данных. Для начала мы определим, что такое строка, и объясним, как использовать некоторые базовые процедуры. Это позволит нам работать с этим типом данных, который может быть интересным, хотя иногда и немного запутанным для новичков. Представленные здесь материалы предназначены только для обучения. Ни в коем случае нельзя рассматривать это приложение как окончательное, цели которого будут иные, кроме изучения представленных концепций.