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

Yuriy Bykov | 16 февраля, 2024

Введение

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

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


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

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

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

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



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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

// Полезные макросы для операций с массивами
#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 месяцев не наблюдалось устойчивого роста. Как раз эта проблема видится основной, и данный цикл статей в целом будет посвящен рассмотрению разных подходов её решения.


Заключение

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

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

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