English Deutsch 日本語 Português
preview
Разрабатываем мультивалютный советник (Часть 10): Создание объектов из строки

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

MetaTrader 5Трейдинг | 9 мая 2024, 10:08
695 2
Yuriy Bykov
Yuriy Bykov

Введение

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

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

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


Хранение информации об объектах

Откроем таблицу в базе данных, которую мы заполнили в рамках прошлой статьи, и посмотрим на последние столбцы. Столбцы params и inputs хранят результат преобразования в строку объекта торговой стратегии класса CSimpleVolumesStrategy и входные параметры одного прохода оптимизации.

Рис. 1. Фрагмент таблицы passes с информацией об использованной стратегии и параметрах тестирования


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

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

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


Приступаем к реализации

Теперь же настал момент, когда стоит подумать о том, как мы сможем воссоздавать объекты по подобным строкам. Итак, у нас есть строка примерно такого вида:

class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3)

Если мы передаём её конструктору объекта класса CSimpleVolumesStrategy, то он должен сделать следующее:

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

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

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

Если попробовать представить, как может выглядеть строка инициализации объектов класса CVirtualStrategyGroup, то получится примерно следующее:

class CVirtualStrategyGroup([
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3),
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,27,0.70,0.90,60,10000.00,550.00,10000,3),
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,37,0.70,0.90,80,10000.00,150.00,10000,3)
], 0.33)

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

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

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


Новый базовый класс

До настоящего момента нашими базовыми классами, участвовавшими в иерархии наследования, были следующие:

  • СAdvisor. Класс для создания экспертов, от него унаследован класс CVirtualAdvisor.
  • CStrategy. Класс для создания торговых стратегий, от него унаследован в итоге CSimpleVolumesStrategy
  • CVirtualStrategyGroup. Класс для групп торговых стратегий. Наследников у него нет и не предвидится
  • ... и всё?

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

Название для нового предка мы выбрали пока что не очень удачное. Хотелось как-то подчеркнуть, что наследники этого класса смогут производиться на фабрике (Factory), то есть будут "factoryable". Что интересно, это слово онлайн-переводчик подчёркивает как ошибочное, но перевод для него звучит так, как и ожидалось: "пригодный для изготовления на заводе". Далее в процессе написания куда-то пропала буква "y", и осталось только имя CFaсtorable.

Изначально этот класс выглядел примерно так:

//+------------------------------------------------------------------+
//| Базовый класс объектов, создаваемых из строки                    |
//+------------------------------------------------------------------+
class CFactorable {
protected:
   virtual void      Init(string p_params) = 0;
public:
   virtual string    operator~() = 0;

   static string     Read(string &p_params);
};

То есть, наследникам этого класса предписывалось иметь метод Init(), который будет выполнять всю необходимую работу по преобразованию входной строки инициализации в значения свойств объекта, и оператор тильда, занимающийся обратным преобразованием свойств в строку инициализации. Так же заявлено существование статического метода Read(), который должен уметь читать часть данных из строки инициализации. Под частью данных мы будем подразумевать подстроку, которая содержит либо корректную строку инициализации другого объекта, либо массив других частей данных, либо число, либо строковую константу.

Хотя такую реализацию удалось довести до работоспособного состояния, было принято решение всё-таки внести в неё существенные изменения.

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

Во-вторых, начальная реализация не содержала никаких средств контроля правильности строк инициализации и сообщений об обнаруженных ошибках. Да, мы рассчитываем формировать строки инициализации автоматически, что почти полностью исключает возникновение подобного рода ошибок, но если вдруг мы что-то напутаем с формируемыми строками инициализации, то неплохо было бы об этом узнать своевременно и смочь найти конкретное место ошибки. Для этих целей мы добавили новое логическое свойство m_isValid, которое указывает, успешно ли выполнен весь код конструктора объекта, или некоторые части строки инициализации содержали ошибки. Это свойство сделано частным, а для получения и установки его значения добавлены соответствующие методы: IsValid() и SetInvalid(). Причём изначально это свойство всегда равно true, а метод SetInvalid() может только установить его значение в false.

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

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

С учётом сделанных добавлений, объявление класса CFactorable стало выглядеть так:

//+------------------------------------------------------------------+
//| Базовый класс объектов, создаваемых из строки                    |
//+------------------------------------------------------------------+
class CFactorable {
private:
   bool              m_isValid;  // Объект исправный?

   // Очистка пустых символов слева и справа в строке инициализации
   static void       Trim(string &p_params);

   // Поиск парной закрывающей скобки в строке инициализации
   static int        FindCloseBracket(string &p_params, char closeBraket = ')');

   // Очистка строки инициализации с проверкой на исправность текущего объекта
   bool              CheckTrimParams(string &p_params);

protected:
   string            m_params;   // Строка инициализации текущего объекта

   // Установка текущего объекта в неисправное состояние
   void              SetInvalid(string function = NULL, string message = NULL);

public:
                     CFactorable() : m_isValid(true) {}  // Конструктор
   bool              IsValid();                          // Объект исправный?

   // Преобразование объекта в строку
   virtual string    operator~() = 0;

   // Строка инициалазации начинается с определения объекта?
   static bool       IsObject(string &p_params, const string className = "");

   // Строка инициалазации начинается с определения объекта нужного класса?
   static bool       IsObjectOf(string &p_params, const string className);

   // Чтение имени класса объекта из строки инициализации
   static string     ReadClassName(string &p_params, bool p_removeClassName = true);

   // Чтение объекта из строки инициализации
   string            ReadObject(string &p_params);
   
   // Чтение массива из строки инициализации в виде строки
   string            ReadArrayString(string &p_params);
   
   // Чтение строки из строки инициализации
   string            ReadString(string &p_params);
   
   // Чтение числа из строки инициализации в виде строки
   string            ReadNumber(string &p_params);
   
   // Чтение вещественного числа из строки инициализации
   double            ReadDouble(string &p_params);
   
   // Чтение целого числа из строки инициализации
   long              ReadLong(string &p_params);
};


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

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

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

Сохраним код этого класса в файле Factorable.mqh в текущей папке.


Фабрика объектов

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

Конечно, для объектов тех классов, имя которых в данном месте программы может принимать единственное значение, наличие такой фабрики необязательно. Мы можем стандартным образом создать объект при помощи оператора new, передав конструктору строку инициализации с параметрами создаваемого объекта. Но если мы должны создавать объекты, имя класса которых может быть разным (например, различные торговые стратегии), то здесь нам оператор new не поможет, так как нам надо сначала понять какого класса объект сейчас предстоит создавать. Эту работу мы и поручим фабрике, точнее, её единственному статическому методу Create().

//+------------------------------------------------------------------+
//| Класс фабрики объектов                                           |
//+------------------------------------------------------------------+
class CVirtualFactory {
public:
   // Создание объекта из строки инициализации
   static CFactorable* Create(string p_params) {
      // Читаем имя класса объекта
      string className = CFactorable::ReadClassName(p_params);
      
      // Указатель на создаваемый объект
      CFactorable* object = NULL;

      // В зависимости от имени класса вызываем соответствующий конструктор
      if(className == "CVirtualAdvisor") {
         object = new CVirtualAdvisor(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      }

      // Если объект не создан или создан в неисправном состоянии, то сообщаем об ошибке
      if(!object) {
         PrintFormat(__FUNCTION__" | ERROR: Constructor not found for:\nclass %s(%s)",
                     className, p_params);
      } else if(!object.IsValid()) {
         PrintFormat(__FUNCTION__
                     " | ERROR: Created object is invalid for:\nclass %s(%s)",
                     className, p_params);
         delete object; // Удаляем неисправный объект
         object = NULL;
      }

      return object;
   }
};

Сохраним этот код в файле VirtualFactory.mqh в текущей папке.

Чтобы нам было удобнее пользоваться фабрикой в дальнейшем, создадим два полезных макроса. Первый будет создавать объект из строки инициализации, подставляя вместо себя вызов метода CVirtualFactory::Create():

// Создание объекта на фабрике из строки
#define NEW(Params) CVirtualFactory::Create(Params)

Второй макрос будет запускаться только из конструктора какого-то другого объекта, обязательно являющегося наследником класса CFactorable. То есть только в том случае, когда при создании одного объекта (главного) нам нужно внутри его конструктора создать другие объекты (вложенные) из строки инициализации. Этому макросу мы будем передавать три параметра: имя класса создаваемого объекта (Class), имя переменной, в которую запишется указатель на созданный объект (Object), и строку инициализации (Params).

В начале в макросе будет объявляться переменная-указатель с заданным именем и классом и инициализироваться значением NULL. Затем будет проверяться, является ли главный объект исправным. Если да, то вызываем метод создания объекта на фабрике через макрос NEW(). Созданный указатель пытаемся привести к нужному классу. Использование для этого оператора динамического приведения типов dynamic_cast<>() позволяет избежать ошибки времени выполнения в случае, если фабрика создала объект не того класса Class, который требуется в данный момент. В этом случае указатель Object просто останется равным NULL, и программа продолжит своё выполнение. А дальше как раз идёт проверка на корректность указателя. Если он пустой или неправильный, то главный объект устанавливается в неисправное состояние, сообщается об ошибке и прерывается выполнение конструктора главного объекта.

Вот как выглядит этот макрос:

// Создание дочернего объекта на фабрике из строки c проверкой.
// Вызывается только из конструктора текущего объекта.
// Если объект не создан, то текущий объект становится неисправным
// и осуществляется выход из конструктора
#define CREATE(Class, Object, Params)                                                                       \
    Class *Object = NULL;                                                                                   \
    if (IsValid()) {                                                                                        \
       Object = dynamic_cast<C*> (NEW(Params));                                                             \
       if(!Object) {                                                                                        \
          SetInvalid(__FUNCTION__, StringFormat("Expected Object of class %s() at line %d in Params:\n%s",  \
                                                #Class, __LINE__, Params));                                 \
          return;                                                                                           \
       }                                                                                                    \
    }                                                                                                       \

Добавим эти макросы в начало файла Factorable.mqh.


Модификация предыдущих базовых классов

Добавим класс CFactorable в качестве базового во все предыдущие базовые классы: СAdvisor, СStrategy, СVirtualStrategyGroup. В первых двух никаких больше изменений не потребуется:

//+------------------------------------------------------------------+
//| Базовый класс эксперта                                           |
//+------------------------------------------------------------------+
class CAdvisor : public CFactorable {
protected:
   CStrategy         *m_strategies[];  // Массив торговых стратегий
   virtual void      Add(CStrategy *strategy);  // Метод добавления стратегии
public:
                    ~CAdvisor();                // Деструктор
   virtual void      Tick();                    // Обработчик события OnTick
   virtual double    Tester() {
      return 0;
   }
};
//+------------------------------------------------------------------+
//| Базовый класс торговой стратегии                                 |
//+------------------------------------------------------------------+
class CStrategy : public CFactorable {
public:                     
   virtual void      Tick() = 0; // Обработка событий OnTick
};

А вот СVirtualStrategyGroup претерпел более серьезные изменения. Поскольку это уже не абстрактный базовый класс, то в нём нам понадобилось написать реализацию конструктора, создающего объект из строки инициализации. При этом мы избавились от двух отдельных конструкторов, которые принимали либо массив стратегий, либо массив групп. Также метод преобразования в строку теперь видоизменился. В нём мы теперь просто присоединяем имя класса к сохранённой строке инициализации с параметрами. А метод масштабирования Scale() остался без изменений.

//+------------------------------------------------------------------+
//| Класс группы торговых стратегий или групп торговых стратегий     |
//+------------------------------------------------------------------+
class CVirtualStrategyGroup : public CFactorable {
protected:
   double            m_scale;                // Коэффициент масштабирования
   void              Scale(double p_scale);  // Масштабирование нормированного баланса
public:
                     CVirtualStrategyGroup(string p_params); // Конструктор

   virtual string    operator~() override;      // Преобразование объекта в строку

   CVirtualStrategy      *m_strategies[];       // Массив стратегий
   CVirtualStrategyGroup *m_groups[];           // Массив групп стратегий
};

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

// Читаем строку инициализации массива стратегий или групп
   string items = ReadArrayString(p_params);

// Пока строка не опустела
   while(items != NULL) {
      // Читаем строку инициализации одного объекта стратегии или группы
      string itemParams = ReadObject(items);

      // Если это группа стратегий, то
      if(IsObjectOf(itemParams, "CVirtualStrategyGroup")) {
         // Создаём группу стратегий и добавляем её в массив групп
         CREATE(CVirtualStrategyGroup, group, itemParams);
         APPEND(m_groups, group);
      } else {
         // Иначе создаём стратегию и добавляем её в массив стратегий
         CREATE(CVirtualStrategy, strategy, itemParams);
         APPEND(m_strategies, strategy);
      }
   }

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

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

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

//+------------------------------------------------------------------+
//| Преобразование объекта в строку                                  |
//+------------------------------------------------------------------+
string CVirtualStrategyGroup::operator~() {
   return StringFormat("%s(%s)", typename(this), m_params);
}


    ... 

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

Модификация класса эксперта

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

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

public:
                     CVirtualAdvisor(string p_param);    // Конструктор
                    ~CVirtualAdvisor();         // Деструктор

   virtual string    operator~() override;      // Преобразование объекта в строку

   ...
};

...

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

// Читаем строку инициализации объекта группы стратегий
   string groupParams = ReadObject(p_params);

// Читаем магический номер
   ulong p_magic = ReadLong(p_params);

// Читаем название эксперта
   string p_name = ReadString(p_params);

// Читаем признак работы на только на открытии бара
   m_useOnlyNewBar = (bool) ReadLong(p_params);

// Если нет ошибок чтения, то
   if(IsValid()) {
      // Создаём группу стратегий
      CREATE(CVirtualStrategyGroup, p_group, groupParams);

      // Инициализируем получателя статическим получателем
      m_receiver = CVirtualReceiver::Instance(p_magic);

      // Инициализируем интерфейс статическим интерфейсом
      m_interface = CVirtualInterface::Instance(p_magic);

      m_name = StringFormat("%s-%d%s.csv",
                            (p_name != "" ? p_name : "Expert"),
                            p_magic,
                            (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                           );

      // Запоминаем время начала работы (тестирования)
      m_fromDate = TimeCurrent();

      // Сбрасываем время последнего сохранения
      m_lastSaveTime = 0;

      // Добавляем к эксперту содержимое группы
      Add(p_group);

      // Удаляем объект группы
      delete p_group;
   }
}

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

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

//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   if(!!m_receiver)  delete m_receiver;         // Удаляем получатель
   if(!!m_interface) delete m_interface;        // Удаляем интерфейс
   DestroyNewBar();           // Удаляем объекты отслеживания нового бара
}

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

Модификация класса торговой стратегии

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

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

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

//+------------------------------------------------------------------+
//| Торговая стратегия с использованием тиковых объемов              |
//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : public CVirtualStrategy {
   ...

public:
   //--- Публичные методы
                     CSimpleVolumesStrategy(string p_params); // Конструктор

   virtual string    operator~() override;         // Преобразование объекта в строку

   virtual void      Tick() override;              // Обработчик события OnTick
};


//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy(string p_params) {
// Запоминаем строку инициализации
   m_params = p_params;
   
// Читаем параметры из строки инициализации
   m_symbol = ReadString(p_params);
   m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params);
   m_signalPeriod = (int) ReadLong(p_params);
   m_signalDeviation = ReadDouble(p_params);
   m_signaAddlDeviation = ReadDouble(p_params);
   m_openDistance = (int) ReadLong(p_params);
   m_stopLevel = ReadDouble(p_params);
   m_takeLevel = ReadDouble(p_params);
   m_ordersExpiration = (int) ReadLong(p_params);
   m_maxCountOfOrders = (int) ReadLong(p_params);
   m_fittedBalance = ReadDouble(p_params);

// Если нет ошибок чтения, то
   if(IsValid()) {
      // Запрашиваем необходимое количество виртуальных позиций
      CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders);

      // Загружаем индикатор для получения тиковых объемов
      m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK);

      // Если индикатор загружен успешно
      if(m_iVolumesHandle != INVALID_HANDLE) {

         // Устанавливаем размер массива-приемника тиковых объемов и нужную адресацию
         ArrayResize(m_volumes, m_signalPeriod);
         ArraySetAsSeries(m_volumes, true);

         // Регистрируем обработчик события нового бара на минимальном таймфрейме
         IsNewBar(m_symbol, PERIOD_M1);
      } else {
         // Иначе устанавливаем ошибочное состояние объекта
         SetInvalid(__FUNCTION__, "Can't load iVolumes()");
      }
   }
}

//+------------------------------------------------------------------+
//| Преобразование объекта в строку                                  |
//+------------------------------------------------------------------+
string CSimpleVolumesStrategy::operator~() {
   return StringFormat("%s(%s)", typename(this), m_params);
}

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

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


Советник для одного экземпляра торговой стратегии

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

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

Core 1  2023.01.01 00:00:00   OnInit | Expert Params:
Core 1  2023.01.01 00:00:00   class CVirtualAdvisor(
Core 1  2023.01.01 00:00:00       class CVirtualStrategyGroup(
Core 1  2023.01.01 00:00:00          [
Core 1  2023.01.01 00:00:00           class CSimpleVolumesStrategy("EURGBP",16385,17,0.70,0.90,150,10000.00,85.00,10000,3,0.00)
Core 1  2023.01.01 00:00:00          ],1
Core 1  2023.01.01 00:00:00       ),
Core 1  2023.01.01 00:00:00       ,27181,SimpleVolumesSingle,1
Core 1  2023.01.01 00:00:00   )

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

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   CMoney::FixedBalance(fixedBalance_);

// Подготавливаем строку инициализации для одного экземпляра стратегии
   string strategyParams = StringFormat(
                              "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d,%.2f)",
                              symbol_, timeframe_,
                              signalPeriod_, signalDeviation_, signaAddlDeviation_,
                              openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                              maxCountOfOrders_, 0
                           );

// Подготавливаем строку инициализации для эксперта с группой из одной стратегии
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        %s\n"
                            "       ],1\n"
                            "    ),\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            strategyParams, magic_, "SimpleVolumesSingle", true
                         );

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

// Создаем эксперта, работающего с виртуальными позициями
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

...

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if(!!expert) delete expert;
}

Сохраним полученный код в файле SimpleVolumesExpertSingle.mq5 в текущей папке.


Советник для нескольких экземпляров

Для проверки создания объекта эксперта с многими экземплярами торговых стратегий, возьмем советник из восьмой части, который мы использовали для проведения нагрузочного тестирования. Изменим в нём в функции OnInit() механизм создания эксперта на тот, который был разработан в рамках этой статьи. Для этого, после загрузки параметров стратегий из CSV-файла, мы будем на их основе дополнять строку инициализации массива стратегий. Затем используем её для формирования строки инициализации группы стратегий и самого эксперта:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Загружаем наборы параметров стратегий
   int totalParams = LoadParams(fileName_, strategyParams);

// Если ничего не загрузили, то сообщим об ошибке
   if(totalParams == 0) {
      PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n"
                  "Check that it exists in data folder or in common data folder.",
                  fileName_);
      return(INIT_PARAMETERS_INCORRECT);
   }

// Сообщаем об ошибке, если
   if(count_ < 1) { // количество экземпляров меньше 1
      return INIT_PARAMETERS_INCORRECT;
   }

   ArrayResize(strategyParams, count_);

// Устанавливаем параметры в классе управления капиталом
   CMoney::DepoPart(expectedDrawdown_ / 10.0);
   CMoney::FixedBalance(fixedBalance_);

// Подготавливаем строку инициализации для массива экземпляров стратегий
   string strategiesParams;
   FOREACH(strategyParams, strategiesParams += StringFormat(" class CSimpleVolumesStrategy(%s),\n      ",
                                                            strategyParams[i % totalParams]));

// Подготавливаем строку инициализации для эксперта с группой стратегий
   string expertParams = StringFormat("class CVirtualAdvisor(\n"
                                      "   class CVirtualStrategyGroup(\n"
                                      "      [\n"
                                      "      %s],\n"
                                      "      %.2f\n"
                                      "   ),\n"
                                      "   %d,%s,%d\n"
                                      ")",
                                      strategiesParams, scale_,
                                      magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_);
   
// Создаем эксперта, работающего с виртуальными позициями
   expert = NEW(expertParams);

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

Аналогично предыдущему советнику, добавим в функцию OnDeinit() проверку на корректность указателя на объект эксперта перед его удалением.

Сохраним полученный код в файле BenchmarkInstancesExpert.mq5 в текущей папке.


Проверка работоспособности

Возьмем советник BenchmarkInstancesExpert.mq5 из восьмой части и тот же самый советник из этой статьи. Запустим их с одинаковыми параметрами: 256 экземпляров торговых стратегий из CSV-файла Params_SV_EURGBP_H1.csv, период тестирования — 2022 год.


Рис. 2. Результаты тестирования двух вариантов советника полностью совпали


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


Заключение

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

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

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

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



Прикрепленные файлы |
Advisor.mqh (4.51 KB)
Factorable.mqh (32.1 KB)
Strategy.mqh (1.88 KB)
VirtualAdvisor.mqh (21.65 KB)
VirtualFactory.mqh (4.29 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Viktor Kudriavtsev
Viktor Kudriavtsev | 16 мая 2024 в 14:05

Юрий здравствуйте. Спасибо за интересный цикл статей.

Юрий, а Вы могли бы выложить файл со стратегиями, с которым вы тестировали советник из текущей статьи? Вот с которым Вы получили скриншот, что внизу статьи. Если он где-то выложен, то подскажите пожалуйста где, я не нашёл под другими статьями. А его помещать нужно в папку C:\Users\Admin\AppData\Roaming\MetaQuotes\Terminal\Common\Files или в папку терминала ? Я хочу посмотреть получаться ли у меня в терминале примерно те же результаты что и у Вас на скрине.

Yuriy Bykov
Yuriy Bykov | 16 мая 2024 в 18:35

Здравствуйте, Виктор.

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

Риск-менеджер для алгоритмической торговли Риск-менеджер для алгоритмической торговли
Целями данной статьи являются: доказать обязательность применения риск-менеджера, адаптация принципов контролируемого риска при торговле алгоритмически в отдельном классе, чтобы каждый смог самостоятельно убедиться в эффективности подхода нормирования риска при внутридневной торговле и инвестировании на финансовых рынках. В данной статье мы подробно раскроем написание класса риск-менеджера для алгоритмической торговли в продолжение к предыдущей статье по написанию риск-менеджера для ручной торговли.
Алгоритм эволюции панциря черепахи (Turtle Shell Evolution Algorithm, TSEA) Алгоритм эволюции панциря черепахи (Turtle Shell Evolution Algorithm, TSEA)
Уникальный алгоритм оптимизации, вдохновленный эволюцией панциря черепахи. Алгоритм TSEA эмулирует постепенное формирование ороговевших участков кожи, которые представляют собой оптимальные решения задачи. Лучшие решения становятся более "твердыми" и располагаются ближе к внешней поверхности, в то время как менее удачные решения остаются "мягкими" и находятся внутри. Алгоритм использует кластеризацию решений по качеству и расстоянию, позволяя сохранять менее успешные варианты и обеспечивая гибкость и адаптивность.
Модифицированный советник Grid-Hedge в MQL5 (Часть II): Создание простого сеточного советника Модифицированный советник Grid-Hedge в MQL5 (Часть II): Создание простого сеточного советника
В статье рассматривается классическая сеточная стратегия, подробно описана ее автоматизация с помощью советника на MQL5 и проанализированы первоначальные результаты тестирования на истории. Также подчеркивается необходимость в долгом удержании позиций и рассматривается возможность оптимизации ключевых параметров (таких как расстояние, тейк-профит и размеры лотов) в будущих частях. Целью этой серии статей является повышение эффективности торговой стратегии и ее адаптируемости к различным рыночным условиям.
Разметка данных в анализе временных рядов (Часть 6):Применение и тестирование советника с помощью ONNX Разметка данных в анализе временных рядов (Часть 6):Применение и тестирование советника с помощью ONNX
В этой серии статей представлены несколько методов разметки временных рядов, которые могут создавать данные, соответствующие большинству моделей искусственного интеллекта (ИИ). Целевая разметка данных может сделать обученную модель ИИ более соответствующей пользовательским целям и задачам, повысить точность модели и даже помочь модели совершить качественный скачок!