Разрабатываем мультивалютный советник (Часть 3): Ревизия архитектуры
Yuriy Bykov | 16 февраля, 2024
Введение
В прошлых статьях мы продолжили разработку мультивалютного советника, работающего одновременно с различными торговыми стратегиями. Можно заметить, что решение, представленное во второй статье уже существенно отличалось от решения, представленного в первой. Это свидетельствует о том, что мы пока только нащупываем оптимальные варианты.
Попробуем взглянуть на разработанную систему в целом, отвлёкшись от мелких деталей реализации, чтобы понять пути её улучшения. Для этого проследим пусть и короткую, но всё-таки заметную эволюцию системы.
Первая схема работы
Мы выделили объект эксперта (класса CAdvisor или его потомков), который является агрегатором объектов торговых стратегий (класса CStrategy или его потомков). В начале работы советника в обработчике OnInit() происходит следующее:
- Создается объект эксперта.
- Создаются объекты торговых стратегий и добавляются к эксперту в его массив для торговых стратегий.
В советнике в обработчике события OnTick() происходит следующее:
- Вызывается метод CAdvisor::Tick() для объекта эксперта.
- Этот метод перебирает все стратегии и вызывает их метод CStrategy::Tick().
- Стратегии в рамках работы CStrategy::Tick() выполняют все необходимые операции для открытия и закрытия рыночных позиций.
Схематично это можно изобразить так:
Рис. 1. Схема работы из первой статьи
Достоинством такой схемы было то, что имея исходный код советника, работающего по какой-то одной торговой стратегии, можно было путем не сильно сложных операций переделать его для совместной работы с другими экземплярами торговых стратегий.
Но быстро обнаружился и главный недостаток: при объединении нескольких стратегий нам приходится уменьшать в той или иной степени размеры позиций, открываемых каждым экземпляром стратегии. Это может привести к полному исключению из торговли некоторых или даже всех экземпляров стратегий. Чем больше экземпляров стратегий мы включаем в параллельную работу или чем меньший начальный депозит выбирается для торговли, тем вероятнее подобный исход, так как минимальный размер открываемых рыночных позиций фиксирован.
Также при совместной работе нескольких экземпляров стратегий встречалась ситуация, когда были открыты противоположные позиции одинакового размера. С точки зрения совокупного объема это эквивалентно отсутствию открытых позиций, однако на открытые противоположные позиции продолжал начисляться своп.
Вторая схема работы
Для устранения недостатков мы решили перенести все операции с рыночными позициями в отдельное место, убрав возможность торговых стратегий напрямую открывать рыночные позиции. Это, правда, несколько усложняет переделку готовых стратегий, но это не очень большая потеря, которая с лихвой окупается устранением основного недостатка первой схемы.
В нашей схеме появляется две новые сущности: виртуальные позиции (класс CVirtualOrder) и получатель торговых объемов от стратегий (класс CReceiver и его потомки).
В начале работы советника в обработчике OnInit() происходит следующее:
- Создается объект получателя.
- Создается объект эксперта с передачей ему созданного получателя.
- Создаются объекты торговых стратегий и добавляются к эксперту в его массив для торговых стратегий.
- Каждая стратегия создает свой массив объектов виртуальных позиций с необходимым количеством этих объектов.
В советнике в обработчике события OnTick() происходит следующее:
- Вызывается метод CAdvisor::Tick() для объекта эксперта.
- Этот метод перебирает все стратегии и вызывает их метод CStrategy::Tick().
- Стратегии в рамках работы CStrategy::Tick() выполняют все необходимые операции для открытия и закрытия виртуальных позиций. Если происходит какое-то событие, связанное с изменением состава открытых виртуальных позиций, то стратегия запоминает, что произошли изменения, выставляя флаг.
- Если хотя бы одна стратегия выставила флаг изменений, то получатель запускает метод корректировки открытых объемов рыночных позиций. При успешном выполнении корректировки, флаг изменений у всех стратегий сбрасывается.
Схематично это можно изобразить так:
Рис. 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. Добавим в него следующие поля и методы:
- массив виртуальных позиций (ордеров);
- общее количество открытых позиций и ордеров;
- метод подсчета количества открытых виртуальных позиций и ордеров;
- методы обработки событий открытия/закрытия виртуальной позиции (ордера).
#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. Результаты работы советника из предыдущей статьи.
Рис. 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 месяцев не наблюдалось устойчивого роста. Как раз эта проблема видится основной, и данный цикл статей в целом будет посвящен рассмотрению разных подходов её решения.
Заключение
В рамках данной статьи мы подготовились к дальнейшему развитию кода, упростив и оптимизировав код из предыдущей части. Мы устранили выявленные недостатки, которые могли в дальнейшем ограничить наши возможности по использованию различных торговых стратегий. Результаты тестирования показали, что новая реализация работает не хуже предыдущей. Скорость работы реализации осталась без изменений, но, возможно, что прирост проявится только при кратном увеличении количества экземпляров стратегий.
Для этого нам нужно наконец-то заняться тем, как мы будем хранить входные параметры стратегий, как будем их объединять в библиотеки параметров, как будем осуществлять выбор наилучших комбинаций из тех, которые будут получены в результате оптимизации одиночных экземпляров стратегий.
В следующей статье мы продолжим работу в выбранном направлении.