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

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

MetaTrader 5Трейдинг | 16 февраля 2024, 10:43
695 11
Yuriy Bykov
Yuriy Bykov

Введение

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

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


Первая схема работы

Мы выделили объект эксперта (класса CAdvisor или его потомков), который является агрегатором объектов торговых стратегий (класса CStrategy или его потомков). В начале работы советника в обработчике OnInit() происходит следующее:

  • Создается объект эксперта.
  • Создаются объекты торговых стратегий и добавляются к эксперту в его массив для торговых стратегий.

В советнике в обработчике события OnTick() происходит следующее:

  • Вызывается метод CAdvisor::Tick() для объекта эксперта.
  • Этот метод перебирает все стратегии и вызывает их метод CStrategy::Tick().
  • Стратегии в рамках работы CStrategy::Tick() выполняют все необходимые операции для открытия и закрытия рыночных позиций.

Схематично это можно изобразить так:



Рис. 1. Схема работы из первой статьи

Рис. 1. Схема работы из первой статьи

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

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

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


Вторая схема работы

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

В нашей схеме появляется две новые сущности: виртуальные позиции (класс CVirtualOrder) и получатель торговых объемов от стратегий (класс CReceiver и его потомки).

В начале работы советника в обработчике OnInit() происходит следующее:

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

В советнике в обработчике события OnTick() происходит следующее:

  • Вызывается метод CAdvisor::Tick() для объекта эксперта.
  • Этот метод перебирает все стратегии и вызывает их метод CStrategy::Tick().
  • Стратегии в рамках работы CStrategy::Tick() выполняют все необходимые операции для открытия и закрытия виртуальных позиций. Если происходит какое-то событие, связанное с изменением состава открытых виртуальных позиций, то стратегия запоминает, что произошли изменения, выставляя флаг.
  • Если хотя бы одна стратегия выставила флаг изменений, то получатель запускает метод корректировки открытых объемов рыночных позиций. При успешном выполнении корректировки, флаг изменений у всех стратегий сбрасывается.

Схематично это можно изобразить так:

Рис. 2. Схема работы из второй статьи

Рис. 2. Схема работы из второй статьи

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

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

В процессе тестирования второй схемы пришло понимание следующих вещей:

  • Каждая стратегия первым делом проводит одинаковую обработку уже открытых виртуальных позиций для определения сработавших уровней StopLoss и TakeProfit. Если какой-то из уровней был достигнут, то такая виртуальная позиция закрывается. Поэтому эта обработка была сразу вынесена в статический метод класса CVirtualOrder. Но такое решение кажется всё ещё недостаточным обобщением.
  • Мы расширили состав базовых классов, добавив к ним новые обязательные сущности. В принципе, если мы не хотим двигаться в сторону работы с виртуальными позициями, то мы всё равно сможем использовать такие базовые классы, просто передавая им "пустые" объекты. Например, можно создать объект класса CReceiver, который содержит только пустые методы-заглушки. Но это тоже больше похоже на временное решение, нуждающееся в переделке.
  • Мы наделили базовый класс CStrategy дополнительными методами и свойством для отслеживания изменений в составе открытых виртуальных позиций, что перетекло в использовании этих методов в базовом классе CAdvisor. Опять-таки это выглядит как шаг к сужению возможностей и навязыванию излишне конкретной реализации в базовом классе.
  • Мы добавили в базовый класс CStrategy метод Volume(), возвращающий суммарный объём открытых виртуальных позиций, так как написанный класс получателя CVolumeReceiver нуждался в информации об открытых виртуальных объемах каждой стратегии. Но тем самым мы отрезали возможность в рамках одного экземпляра торговой стратегии открывать виртуальные позиции по нескольким символам  — в этом случае суммарный объём теряет свой смысл. Для обкатки односимвольных стратегий такое решение подойдёт, но не более того.
  • Мы использовали в классе CReceiver массив для хранения указателей на созданные стратегии у эксперта, чтобы получатель мог через них выяснять у стратегий открытый виртуальный объём. Это привело к дублированию кода, который занимается наполнением массивов стратегий в эксперте и получателе.
  • Мы прямо использовали в конкретном классе получателя CVolumeReceiver то, что каждая стратегия открывает позиции только по одному символу: при добавлении в массив стратегий получателя у стратегии спрашивается её символ, и он добавляется в массив используемых символов. Далее получатель работает только с символами, добавленными в его массив символов. Про порождаемое вследствие этого ограничение мы уже упоминали выше.
На основе анализа перечисленных недостатков и обсуждения в комментариях внесём следующие изменения:
  • Очистим всё-таки базовые классы CStrategy и CAdvisor максимально возможно. Для развития ветки разработки советников, использующих виртуальную торговлю сделаем, свои производные классы CVirtualStrategy и CVirtualAdvisor. Теперь они будут нашими родительскими классами для конкретных стратегий и экспертов.
  • Расширим класс виртуальных позиций. Добавим к каждой виртуальной позиции указатель на объект получателя, который будет заниматься выводом виртуального торгового объёма на рынок, и объект торговой стратегии, которая будет принимать решения об открытии/закрытии виртуальной позиции. Это позволит выполнять оповещение заинтересованных объектов об операциях открытия/закрытия виртуальных позиций.
  • Перенесём хранение всех виртуальных позиций в один массив, вместо распределения их по нескольким массивам, принадлежащих экземплярам стратегий. Каждый экземпляр стратегии будет запрашивать несколько элементов из этого массива для своей работы. Владельцем общего массива будет получатель торговых объемов.
  • Получатель будет только один в одном советнике. Поэтому реализуем его как Singleton, единственный экземпляр которого будет доступен во всех необходимых местах. Такую реализацию оформим в виде производного класса CVirtualReceiver.
  • В состав получателя мы добавим массив новых сущностей  — символьных получателей (класс CVirtualSymbolReceiver). Каждый символьный получатель будет работать только с виртуальными позициями своего символа, которые будут автоматически прикрепляться к символьному получателю при открытии и открепляться при закрытии от символьного получателя.
Попробуем всё это реализовать.


Очистка базовых классов

Оставим только самое необходимое в базовых классах CStrategy и CAdvisor. Для CStartegy оставим только метод обработки события OnTick и получим такой лаконичный код:

//+------------------------------------------------------------------+
//| Базовый класс торговой стратегии                                 |
//+------------------------------------------------------------------+
class CStrategy {
public:
   virtual void      Tick() = 0; // Обработка событий OnTick
};

Всё остальное будет уже в потомках этого класса.

В базовом классе CAdvisor подключим небольшой файл Macros.mqh, в котором собрано несколько полезных макросов для выполнения операций с обычными массивами:

  • APPEND(A, V)  — добавление в массив A элемента V в конец массива;
  • FIND(A, V, I)  — записать в переменную I индекс элемента массива A, равного значению V. Если элемент не найден, то в переменную I попадает значение -1;
  • ADD(A, V)  — добавление в массив A элемента V в конец, если такого элемента ещё нет в массиве;
  • FOREACH(A, D)  — цикл по индексам элементов массива A (индекс будет в локальной переменной i), выполняющий в теле действия D;
  • REMOVE_AT(A, I)  — удаление элемента из массива A на позиции с индексом I со сдвигом последующих элементов и сокращением размера массива;
  • REMOVE(A, V)  — удаление из массива A элемента, равного V

// Полезные макросы для операций с массивами
#ifndef __MACROS_INCLUDE__
#define APPEND(A, V)    A[ArrayResize(A, ArraySize(A) + 1) - 1] = V;
#define FIND(A, V, I)   { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } }
#define ADD(A, V)       { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } }
#define FOREACH(A, D)   { for(int i=0, im=ArraySize(A);i<im;i++) {D;} }
#define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);}
#define REMOVE(A, V)    { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) }
#define __MACROS_INCLUDE__
#endif
//+------------------------------------------------------------------+

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

Мы уберем из класса CAdvisor все места, в которых встречался получатель, а также оставим в методе обработки события OnTick только вызов соответствующих обработчиков у стратегий. Получим такой код:

#include "Macros.mqh"
#include "Strategy.mqh"

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

//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
void CAdvisor::~CAdvisor() {
// Удаляем все объекты стратегий
   FOREACH(m_strategies, delete m_strategies[i]);
}

//+------------------------------------------------------------------+
//| Обработчик события OnTick                                        |
//+------------------------------------------------------------------+
void CAdvisor::Tick(void) {
// Для всех стратегий вызываем обработку OnTick
   FOREACH(m_strategies, m_strategies[i].Tick());
}

//+------------------------------------------------------------------+
//| Метод добавления стратегии                                       |
//+------------------------------------------------------------------+
void CAdvisor::Add(CStrategy *strategy) {
   APPEND(m_strategies, strategy);  // Добавляем стратегию в конец массива
}
//+------------------------------------------------------------------+

Эти классы у нас останутся в файлах Strategy.mqh и Advisor.mqh в текущей папке.

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

Создадим класс CVirtualStrategy, унаследованный от CStrategy. Добавим в него следующие поля и методы:

  • массив виртуальных позиций (ордеров);
  • общее количество открытых позиций и ордеров;
  • метод подсчета количества открытых виртуальных позиций и ордеров;
  • методы обработки событий открытия/закрытия виртуальной позиции (ордера).
Пока что методы обработки событий открытия/закрытия виртуальных позиций будут просто вызывать метод пересчёта количества открытых виртуальных позиций, который будет обновлять значение поля m_ordersTotal. Другие действия совершать пока не понадобилось, но, возможно, что в будущем потребуется делать и что-нибудь ещё. Поэтому пока эти методы сделаны отдельными от метода подсчёта открытых виртуальных позиций.

#include "Strategy.mqh"
#include "VirtualOrder.mqh"

//+------------------------------------------------------------------+
//| Класс торговой стратегии с виртуальными позициями                |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
protected:
   CVirtualOrder     *m_orders[];   // Массив виртуальных позиций (ордеров)
   int               m_ordersTotal; // Общее количество открытых позиций и ордеров

   virtual void      CountOrders(); // Подсчет количества открытых виртуальных позиций и ордеров

public:
   virtual void      OnOpen();      // Обработчик события открытия виртуальной позиции (ордера)
   virtual void      OnClose();     // Обработчик события закрытия виртуальной позиции (ордера)
};

//+------------------------------------------------------------------+
//| Подсчет количества открытых виртуальных позиций и ордеров        |
//+------------------------------------------------------------------+
void CVirtualStrategy::CountOrders() {
   m_ordersTotal = 0;
   FOREACH(m_orders, if(m_orders[i].IsOpen()) { m_ordersTotal += 1; })
}

//+------------------------------------------------------------------+
//| Обработчик события открытия виртуальной позиции (ордера)         |
//+------------------------------------------------------------------+
void CVirtualStrategy::OnOpen() {
   CountOrders();
}

//+------------------------------------------------------------------+
//| Обработчик события закрытия виртуальной позиции (ордера)         |
//+------------------------------------------------------------------+
void CVirtualStrategy::OnClose() {
   CountOrders();
}

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

Поскольку работу с получателем мы удалили из базового класса CAdvisor, то её надо перенести в наш новый дочерний класс CVirtualAdvisor. В этом классе мы добавим поле m_receiver для хранения указателя на объект получателя торговых объёмов.

В конструкторе это поле будет проинициализировано указателем на единственный возможный объект получателя, который как раз будет создан в этот момент при вызове статического метода CVirtualReceiver::Instance(). А деструктор позаботится, чтобы этот объект был правильно удалён.

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

#include "Advisor.mqh"
#include "VirtualReceiver.mqh"

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

public:
                     CVirtualAdvisor(ulong p_magic = 1); // Конструктор
                    ~CVirtualAdvisor();                  // Деструктор
   virtual void      Tick() override;                    // Обработчик события OnTick

};

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1) :
// Инициализируем получателя статическим получателем
   m_receiver(CVirtualReceiver::Instance(p_magic)) {};

//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   delete m_receiver;         // Удаляем получатель
}

//+------------------------------------------------------------------+
//| Обработчик события OnTick                                        |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Получатель обрабатывает виртуальные позиции
   m_receiver.Tick();
   
// Запуск обработки в стратегиях
   CAdvisor::Tick();
   
// Корректировка рыночных объемов
   m_receiver.Correct();
}
//+------------------------------------------------------------------+

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


Расширение класса виртуальных позиций

Добавим к классу виртуальной позиции указатель на объект получателя m_receiver и объект торговой стратегии m_strategy. Значения для данных полей должны будут передаваться через параметры конструктора, поэтому внесём правки и в него. Также понадобилось добавить пару геттеров для частных свойств виртуальной позиции: Id() и Symbol(). Покажем добавленный код в описании класса:

//+------------------------------------------------------------------+
//| Класс виртуальных ордеров и позиций                              |
//+------------------------------------------------------------------+
class CVirtualOrder {
private:
//--- Статические поля ...
   
//--- Связанные объекты получателя и стратегии
   CVirtualReceiver  *m_receiver;
   CVirtualStrategy  *m_strategy;

//--- Свойства ордера (позиции) ...
   
//--- Свойства закрытого ордера (позиции) ...
   
//--- Частные методы
   
public:
                     CVirtualOrder(
      CVirtualReceiver *p_receiver,
      CVirtualStrategy *p_strategy
   );                                  // Конструктор

//--- Методы проверки состояния позиции (ордера) ...
   

//--- Методы получения свойств позиции (ордера) ...
   ulong             Id() {            // ID
      return m_id;
   }
   string            Symbol() {        // Символ
      return m_symbol;
   }

//--- Методы обработки позиций (ордеров) ...
  
};

В реализации конструктора мы просто добавили в списке инициализации две строчки для установки значений новых полей из параметров конструктора:

CVirtualOrder::CVirtualOrder(CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy) :
// Список инициализации
   m_id(++s_count),  // Новый идентификатор = счётчик объектов + 1
   m_receiver(p_receiver),
   m_strategy(p_strategy),
   ...,
   m_point(0) {
}

Оповещение получателя и стратегии должно происходить только при открытии или закрытии виртуальной позиции. Это происходит только в методе Open() и Close(), поэтому добавим в них небольшой код:

//+------------------------------------------------------------------+
//| Открытие виртуальной позиции                                     |
//+------------------------------------------------------------------+
bool CVirtualOrder::Open(...) {
   // Если позиция уже открыта, то ничего не делаем ...

   if(s_symbolInfo.Name(symbol)) {  // Выбираем нужный символ
      // Обновляем информацию о текущих ценах ...

      // Инициализируем свойства позиции ...
  
      // В зависимости от направления устанавливаем цену открытия и уровни SL и TP ...
            
      // Оповещаем получатель и стратегию, что позиция (ордер) открыта
      m_receiver.OnOpen(GetPointer(this));
      m_strategy.OnOpen();

      ...

      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Закрытие позиции                                                 |
//+------------------------------------------------------------------+
void CVirtualOrder::Close() {
   if(IsOpen()) { // Если позиция открыта
      ...
      // Определяем причину закрытия для вывода в лог ...
     
      // Запоминаем цену закрытия в зависимости от типа ...
    
      // Оповещаем получатель и стратегию, что позиция (ордер) открыта
      m_receiver.OnClose(GetPointer(this));
      m_strategy.OnClose();
   }
}

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

Этот код остается в текущей папке в файле с таким же именем — VirtualOrder.mqh.


Реализация нового получателя

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

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

//+------------------------------------------------------------------+
//| Класс перевода открытых объемов в рыночные позиции (получатель)  |
//+------------------------------------------------------------------+
class CVirtualReceiver : public CReceiver {
protected:
// Статический указатель на единственный экземпляр данного класса
   static   CVirtualReceiver *s_instance;

   ...

   CVirtualReceiver(ulong p_magic = 0);   // Закрытый конструктор

public:
//--- Статические методы
   static
   CVirtualReceiver  *Instance(ulong p_magic = 0);    // Синглтон - создание и получение единственного экземпляра

   ...
};

// Инициализация статического указателя на единственный экземпляр данного класса
CVirtualReceiver *CVirtualReceiver::s_instance = NULL;


//+------------------------------------------------------------------+
//| Синглтон - создание и получение единственного экземпляра         |
//+------------------------------------------------------------------+
CVirtualReceiver* CVirtualReceiver::Instance(ulong p_magic = 0) {
   if(!s_instance) {
      s_instance = new CVirtualReceiver(p_magic);
   }
   return s_instance;
}

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

class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualOrder     *m_orders[];         // Массив виртуальных позиций
   
   ...

public:
//--- Статические методы
   ...
   static void       Get(CVirtualStrategy *strategy,
                         CVirtualOrder *&orders[],
                         int n); // Выделение стратегии необходимого количества виртуальных позиций
   ...
};

...

//+------------------------------------------------------------------+
//| Выделение стратегии необходимого количества виртуальных позиций  |
//+------------------------------------------------------------------+
static void CVirtualReceiver::Get(CVirtualStrategy *strategy,   // Стратегия
                                  CVirtualOrder *&orders[],     // Массив позиций стратегии
                                  int n                         // Требуемое количество
                                 ) {
   CVirtualReceiver *self = Instance();   // Синглтон получателя
   ArrayResize(orders, n);                // Расширяем массив виртуальных позиций
   FOREACH(orders,
           orders[i] = new CVirtualOrder(self, strategy); // Наполняем массив новыми объектами
           APPEND(self.m_orders, orders[i])) // Регистрируем созданную виртуальную позицию
   ...
}

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

Но это чуть позже, а сейчас вернёмся к классу CVirtualReceiver и добавим в него ещё переопределение виртуального метода Correct().

class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualSymbolReceiver *m_symbolReceivers[];       // Массив получателей для отдельных символов

public:
   ...
//--- Публичные методы
   virtual bool      Correct() override;              // Корректировка открытых объёмов
};

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

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

class CVirtualReceiver : public CReceiver {
   ...
   bool              m_isChanged;         // Есть ли изменения в открытых позициях?
   ...
   bool              IsTradeAllowed();    // Торговля доступна?

public:
   ...

   virtual bool      Correct() override;  // Корректировка открытых объёмов
};
//+------------------------------------------------------------------+
//| Корректировка открытых объемов                                   |
//+------------------------------------------------------------------+
bool CVirtualReceiver::Correct() {
   bool res = true;
   if(m_isChanged && IsTradeAllowed()) {
      // Если есть изменения, то вызываем корректировку получателей отдельных символов
      FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct());
      m_isChanged = !res;
   }
   return res;
}

В методе IsTradeAllowed() мы проверим состояние терминала и торгового счёта, для определения, можно ли вести реальную торговлю:

//+------------------------------------------------------------------+
//| Торговля доступна?                                               |
//+------------------------------------------------------------------+
bool CVirtualReceiver::IsTradeAllowed() {
   return (true
           && MQLInfoInteger(MQL_TRADE_ALLOWED)
           && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)
           && AccountInfoInteger(ACCOUNT_TRADE_EXPERT)
           && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)
           && TerminalInfoInteger(TERMINAL_CONNECTED)
          );
}

Мы использовали в методе Correct() флаг наличия изменений, который сбрасывался, если корректировка объёмов прошла успешно. Но где же должен устанавливаться данный флаг? Очевидно, что это должно происходить, если открывается или закрывается какая-то виртуальная позиция. В классе CVirtualOrder мы специально добавили в методы открытия/закрытия вызов пока ещё отсутствовавших в классе CVirtualReceiver методов OnOpen() и OnClose(). В них мы и будем устанавливать флаг наличия изменений.

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

class CVirtualReceiver : public CReceiver {
   ...

public:
   ...

//--- Публичные методы
   void              OnOpen(CVirtualOrder *p_order);  // Обработка открытия виртуальной позиции
   void              OnClose(CVirtualOrder *p_order); // Обработка закрытия виртуальной позиции
   ...
};

//+------------------------------------------------------------------+
//| Обработка открытия виртуальной позиции                           |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   string symbol = p_order.Symbol();      // Определяем символ позиции
   CVirtualSymbolReceiver *symbolReceiver;
   int i;
   FIND(m_symbolReceivers, symbol, i);    // Ищем получателя для данного символа

   if(i == -1) {
      // Если не нашли, то создаем нового получателя для данного символа
      symbolReceiver = new CVirtualSymbolReceiver(m_magic, symbol);
      // и добавляем его в массив символьных получателей
      APPEND(m_symbolReceivers, symbolReceiver);
   } else {
      // Если нашли, то берем его
      symbolReceiver = m_symbolReceivers[i];
   }
   
   symbolReceiver.Open(p_order); // Оповещаем символьный получатель о новой позиции
   m_isChanged = true;           // Запомним, что изменения есть
}

//+------------------------------------------------------------------+
//| Обработка закрытия виртуальной позиции                           |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   string symbol = p_order.Symbol();   // Определяем символ позиции
   int i;
   FIND(m_symbolReceivers, symbol, i); // Ищем получателя для данного символа

   if(i != -1) {
      m_symbolReceivers[i].Close(p_order);   // Оповещаем символьный получатель о закрытии позиции
      m_isChanged = true;                    // Запомним, что изменения есть
   }
}

Помимо открытия/закрытия виртуальных позиций по сигналам торговой стратегии, они могут закрываться при достижении уровней StopLoss или TakeProfit. В классе CVirtualOrder у нас есть специально для этого метод Tick(), который проверяет уровни и закрывает виртуальную позицию при необходимости. Но его надо обязательно вызвать, причём на каждом тике и для всех виртуальных позиций. Именно это будет делать метод Tick() в классе CVirtualReceiver, который мы сейчас добавим:

class CVirtualReceiver : public CReceiver {
   ...

public:
   ...

//--- Публичные методы
   void              Tick();     // Обработка тика для массива виртуальных ордеров (позиций)
   ...
};

//+------------------------------------------------------------------+
//| Обработка тика для массива виртуальных ордеров (позиций)         |
//+------------------------------------------------------------------+
void CVirtualReceiver::Tick() {
   FOREACH(m_orders, m_orders[i].Tick());
}

И, наконец, позаботимся о корректном освобождении памяти, выделенной под объекты виртуальных позиций. Поскольку все они есть в массиве m_orders, то добавим деструктор, в котором будем выполнять их удаление:

//+------------------------------------------------------------------+
//| Деструктор                                                                 |
//+------------------------------------------------------------------+
CVirtualReceiver::~CVirtualReceiver() {
   FOREACH(m_orders, delete m_orders[i]); // Удаляем виртуальные позиции
}

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


Реализация символьного получателя

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

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

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

Добавим поля для символа, массива позиций и признака наличия изменений, а также два метода обработки открытия/закрытия к классу:

class CVirtualSymbolReceiver : public CReceiver {
   string            m_symbol;         // Символ
   CVirtualOrder     *m_orders[];      // Массив открытых виртуальных позиций
   bool              m_isChanged;      // Есть ли изменения в составе виртуальных позиций

   ...   

public:
   ...
   void              Open(CVirtualOrder *p_order);    // Регистрация открытия виртуальной позиции
   void              Close(CVirtualOrder *p_order);   // Регистрация закрытия виртуальной позиции
   ...
};

Сама реализация этих методов тривиальна: добавляем/удаляем переданную виртуальную позицию из массива и выставляем флаг наличия изменений.

//+------------------------------------------------------------------+
//| Регистрация открытия виртуальной позиции                         |
//+------------------------------------------------------------------+
void CVirtualSymbolReceiver::Open(CVirtualOrder *p_order) {
   APPEND(m_orders, p_order); // Добавляем позицию в массив
   m_isChanged = true;        // Устанавливаем флаг изменений
}

//+------------------------------------------------------------------+
//| Регистрация закрытия виртуальной позиции                         |
//+------------------------------------------------------------------+
void CVirtualSymbolReceiver::Close(CVirtualOrder *p_order) {
   REMOVE(m_orders, p_order); // Удаляем позицию из массива
   m_isChanged = true;        // Устанавливаем флаг изменений
}

Ещё нам понадобится из получателя осуществлять поиск нужного символьного получателя по названию символа. Для использования обычного алгоритма линейного поиска из макроса FIND(A,V,I) добавим перегруженный оператор сравнения символьного получателя со строкой, который будет возвращать истину, если символ данного экземпляра совпадает с переданной строкой:

class CVirtualSymbolReceiver : public CReceiver {
   ...

public:
   ...
   bool              operator==(const string symbol) {// Оператор сравнения по имени символа
      return m_symbol == symbol;
   }
   ...
};

Приведем полное описание класса CVirtualSymbolReceiver. Конкретную реализацию всех методов можно посмотреть в приложенных файлах.

class CVirtualSymbolReceiver : public CReceiver {
   string            m_symbol;         // Символ
   CVirtualOrder     *m_orders[];      // Массив открытых виртуальных позиций
   bool              m_isChanged;      // Есть ли изменения в составе виртуальных позиций

   bool              m_isNetting;      // Это неттинг-счёт?

   double            m_minMargin;      // Минимальная маржа для открытия

   CPositionInfo     m_position;       // Объект для получения свойств рыночных позиций
   CSymbolInfo       m_symbolInfo;     // Объект для получения свойств символа
   CTrade            m_trade;          // Объект для совершения торговых операций

   double            MarketVolume();   // Объём открытых рыночных позиций
   double            VirtualVolume();  // Объём открытых виртуальных позиций
   bool              IsTradeAllowed(); // Торговля по символу доступна? 

   // Необходимая разница объёмов
   double            DiffVolume(double marketVolume, double virtualVolume);

   // Коррекция объема на необходимую разницу
   bool              Correct(double oldVolume, double diffVolume);

   // Вспомогательные методы открытия
   bool              ClearOpen(double diffVolume);
   bool              AddBuy(double volume);
   bool              AddSell(double volume);
   
   // Вспомогательные методы закрытия
   bool              CloseBuyPartial(double volume);
   bool              CloseSellPartial(double volume);
   bool              CloseHedgingPartial(double volume, ENUM_POSITION_TYPE type);
   bool              CloseFull();

   // Проверка маржинальных требований
   bool              FreeMarginCheck(double volume, ENUM_ORDER_TYPE type);

public:
                     CVirtualSymbolReceiver(ulong p_magic, string p_symbol);  // Конструктор
   bool              operator==(const string symbol) {// Оператор сравнения по имени символа
      return m_symbol == symbol;
   }
   void              Open(CVirtualOrder *p_order);    // Регистрация открытия виртуальной позиции
   void              Close(CVirtualOrder *p_order);   // Регистрация закрытия виртуальной позиции
   
   virtual bool      Correct() override;              // Корректировка открытых объёмов
};

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


Сравнение результатов

Полученную схему работы можно представить в следующем виде:


Рис. 3. Схема работы из этой статьи

Рис. 3. Схема работы из этой статьи

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


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


Рис. 4. Результаты работы советника из данной статьи.

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


Оценка дальнейшего потенциала

В обсуждении предыдущей статьи на форуме был задан закономерный вопрос: а какие максимально привлекательные торговые результаты можно получить, используя рассматриваемый подход? Пока что на графиках была показана доходность в 20% за 5 лет, что выглядит не особо привлекательно.

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

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

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

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

Попробуем взять небольшой начальный депозит и подобрать новое оптимальное значение размера открываемых позиций для максимальной разрешенной просадки 50% на периоде 5 лет (2018.01.01 — 2023.01.01). Ниже представлены результаты прогонов советника из этой статьи с разным множителем размеров позиций, но постоянным на протяжении всех пяти лет с начальным депозитом $1000. В прошлой статье размеры позиций откалиброваны под размер депозита $10000, поэтому начальное значение depoPart_ было уменьшено примерно в 10 раз.

Рис. 5. Результаты тестирования с разным размером позиций.

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

При максимальном depoPart_ = 0.4 получаем прибыль примерно $22800. Однако просадка, показываемая здесь, — это относительная просадка, встретившаяся за всё время прогона. Но 10% от 23000 и от 1000 — это сильно отличающиеся значения. Поэтому надо смотреть обязательно результаты одиночного запуска:

Рис. 6. Результаты тестирования при максимальном depoPart_ = 0.4

Как видно, на самом деле достигалась просадка в $1167, что на момент достижения составило только 9.99% от текущего баланса, но если бы начало периода тестирования располагалось непосредственно перед этим неприятным моментом, то мы бы получили потерю всего депозита. Поэтому, такой размер позиций использовать нельзя.

Посмотрим на результаты при depoPart_ = 0.2


Рис. 7. Результаты тестирования при depoPart_ = 0.2


Тут максимальная просадка не превышала $494, то есть около 50% от начального депозита $1000. Поэтому можно сказать, что при таком размере позиций даже если максимально неудачно выбрать начало периода на протяжении рассматриваемых пяти лет, то потери всего депозита не будет.

При таком размере позиций результаты тестирования за 1 год (2022) будут такими:


Рис. 8. Результаты тестирования за 2022 год при depoPart_ = 0.2

То есть при просадке максимальной ожидаемой просадке примерно в 50% показана прибыль около 150% в год.

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

Рис. 9. Результаты тестирования за 2023 год при depoPart_ = 0.2

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


Заключение

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

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

В следующей статье мы продолжим работу в выбранном направлении.



Прикрепленные файлы |
Advisor.mqh (4.3 KB)
Macros.mqh (2.3 KB)
Receiver.mqh (2.55 KB)
Strategy.mqh (1.74 KB)
VirtualAdvisor.mqh (4.68 KB)
VirtualOrder.mqh (25.23 KB)
VirtualReceiver.mqh (15.29 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (11)
Yuriy Bykov
Yuriy Bykov | 16 февр. 2024 в 18:48
fxsaber #:
Не дочитал до конца. Но ощущение, что работа с отложками (не в Тестере) не очень вписывается в эту, действительно, значительно улучшенную архитектуру.

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

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

Yuriy Bykov
Yuriy Bykov | 16 февр. 2024 в 18:53
fxsaber #:

Хорошо бы добавить маску включения/выключения стратегии для учета объемника (получатель виртуальных объемов).

Например, нужно какую-то ТС из портфеля отключить на время: виртуально продолжает торговать, но при этом не влияет на реальное окружение. Аналогично и с обратным ее включением.

Такое реализовать несложно, но без для использования понадобятся чёткие критерии включения/выключения для каждой стратегии. Это уже более сложная задача, пока к ней не подступался; возможно, что и не подступлюсь.

Yuriy Bykov
Yuriy Bykov | 16 февр. 2024 в 18:58
fxsaber #:

А ведь где-то должно быть грамотное встраивание нечто подобного.

CAdvisor *m_advisors[];  // Массив виртуальных портфелей

со своими мэджиками.

Такого как раз не планируется. Объединение в портфели будет происходить на промежуточном уровне между CAdvisor и CStrategy. Черновое решение есть, но оно, скорее всего, сильно изменится при проводимом рефакторинге.

Yuriy Bykov
Yuriy Bykov | 16 февр. 2024 в 19:00
fxsaber #:

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

Спасти может флаг, что синхронизация объемов прошла успешно.

Вроде бы он уже есть:

class CVirtualSymbolReceiver : public CReceiver {
  ...
   bool              m_isChanged;      // Есть ли изменения в составе виртуальных позиций

Сбрасывается только при успешном открытии нужного реального объёма по каждому символу.

Yuriy Bykov
Yuriy Bykov | 16 февр. 2024 в 19:03
fxsaber #:

В виртуальный ордер вставлять совсем иную сущность - сомнительное решение.

Очень хотел этого избежать. Искал по-всякому, как можно оставить CVirtualOrder никак не связанным с этими сущностями. То, что получалось нравилось еще меньше. Поэтому пока так.

Нейросети — это просто (Часть 77): Кросс-ковариационный Трансформер (XCiT) Нейросети — это просто (Часть 77): Кросс-ковариационный Трансформер (XCiT)
В своих моделях мы часто используем различные алгоритмы внимание. И, наверное, чаще всего мы используем Трансформеры. Основным их недостатком является требование к ресурсам. В данной статье я хочу предложить Вам познакомиться с алгоритмом, который поможет снизить затраты на вычисления без потери качества.
Использование алгоритмов оптимизации для настройки параметров советника "на лету" Использование алгоритмов оптимизации для настройки параметров советника "на лету"
В статье рассматриваются практические аспекты использования алгоритмов оптимизации для поиска наилучших параметров советников "на лету", виртуализация торговых операций и логики советника. Данная статья может быть использована как своеобразная инструкция для внедрения алгоритмов оптимизации в торгового советника.
Изучение MQL5 — от новичка до профи (Часть II): Базовые типы данных и использование переменных Изучение MQL5 — от новичка до профи (Часть II): Базовые типы данных и использование переменных
Продолжение серии для начинающих. Здесь мы рассмотрим, как создавать константы и переменные, записывать дату, цвета и другие полезные данные. Научимся создавать перечисления вроде дней недели или стилей линий (сплошная, пунктирная и т.д.). Переменные и выражения - это база программирования. Они обязательно есть в 99% программ, поэтому понимать их критически важно. И поэтому, если вы - новичок в программировании - прошу. Уровень знания программирования: очень базовый - в пределах моей предыдущей статьи (ссылка - в начале).
Разметка данных в анализе временных рядов (Часть 3):Пример использования разметки данных Разметка данных в анализе временных рядов (Часть 3):Пример использования разметки данных
В этой серии статей представлены несколько методов разметки временных рядов, которые могут создавать данные, соответствующие большинству моделей искусственного интеллекта (ИИ). Целевая разметка данных может сделать обученную модель ИИ более соответствующей пользовательским целям и задачам, повысить точность модели и даже помочь модели совершить качественный скачок!