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

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

MetaTrader 5Трейдинг | 6 февраля 2024, 15:58
990 55
Yuriy Bykov
Yuriy Bykov

Введение

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

Также мы определили оптимальный размер открываемых позиций исходя из желаемого максимального уровня просадки (10% от депозита). Мы сделали это для каждой стратегии по отдельности. Когда мы объединили две стратегии вместе, то для удержания заданного уровня просадки нам пришлось уменьшить размер открываемых позиций. Для двух стратегий уменьшение было небольшим. Но что, если мы захотим объединить десятки или сотни экземпляров стратегий? Вполне может случиться, что у каких-то стратегий придется уменьшить размер позиций до значения, меньшего чем минимальный размер открываемых позиций, позволенный брокером. В этом случае эти стратегии просто не смогут участвовать в торговле. Как же всё-таки заставить их работать?

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

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

Попробуем это реализовать.


Вспомним сделанное

Мы разработали класс эксперта CAdvisor, который хранит массив экземпляров торговых стратегий (точнее, указателей на экземпляры). Это позволяет создать в основной программе один экземпляр эксперта и добавить в него несколько экземпляров классов стратегий. Причем, поскольку массив хранит указатели на объекты базового класса CStrategy, то в нём можно хранить указатели на объекты любых классов-потомков, унаследованных от CStrategy. В нашем случае мы создали один класс-потомок CSimpleVolumesStrategy, два объекта которого добавлялись в этот массив у эксперта.

Давайте договоримся об удобных названиях для простоты изложения:

  • Cоветник — наш итоговый файл с расширением mq5, который после компиляции дает запускаемый файл ex5, пригодный для запуска в тестере и терминале.
  • Эксперт — объект класса CAdvisor, объявленный в программе. В одной программе будем использовать только один экземпляр эксперта.
  • Стратегия — объект дочернего класса, унаследованного от базового класса стратегий CStrategy

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

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

Работа советника состояла из следующих этапов:

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

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

CAdvisor     expert;          // Объект эксперта
CAdvisor     *expert;         // Указатель на объект эксперта

int OnInit() {
   expert = new CAdvisor();   // Создаем объект эксперта
   
   // Остальной код из OnInit() ...
}

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

class CAdvisor {
protected:
   CStrategy         *m_strategies[];  // Массив торговых стратегий
public:
   ~CAdvisor();                        // Деструктор

   // ...
};

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

Теперь заменим в программе в функции OnDeinit() вызов метода CAdvisor::Deinit() на удаление объекта эксперта.

void OnDeinit(const int reason) {
   expert.Deinit();
   delete expert;
}


Наметим путь

Если торговые стратегии теперь не смогут сами открывать рыночные позиции, то 

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

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

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

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

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

#include "VirtualOrder.mqh"

//+------------------------------------------------------------------+
//| Базовый класс торговой стратегии                                 |
//+------------------------------------------------------------------+
class CStrategy {
protected:
   string            m_symbol;         // Символ (торговый инструмент)
   ENUM_TIMEFRAMES   m_timeframe;      // Период графика (таймфрейм)
   double            m_fixedLot;       // Размер открываемых позиций (фиксированный)

   CVirtualOrder     m_orders[];       // Массив виртуальных позиций (ордеров)
   int               m_ordersTotal;    // Общее количество открытых позиций и ордеров
   double            m_volumeTotal;    // Общий объем открытых позиций и ордеров

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

public:
   // Конструктор
   CStrategy(string p_symbol = "",
             ENUM_TIMEFRAMES p_timeframe = PERIOD_CURRENT,
             double p_fixedLot = 0.01);

   virtual void      Tick();           // Основной метод - обработка событий OnTick
   virtual double    Volume();         // Общий объём виртуальных позиций
   virtual string    Symbol();         // Символ стратегии (пока только один для одной стратегии)
   virtual bool      IsChanged();      // Есть изменения в открытых виртуальных позициях?
   virtual void      ResetChanges();   // Сбросить признак наличия изменений в открытых виртуальных позициях
};

Методы Symbol(), IsChanged() и ResetChanges() мы уже можем реализовать.

//+------------------------------------------------------------------+
//| Символ стратегии                                                 |
//+------------------------------------------------------------------+
string CStrategy::Symbol() {
   return m_symbol;
}

//+------------------------------------------------------------------+
//| Есть изменения в открытых виртуальных позициях?                  |
//+------------------------------------------------------------------+
bool CStrategy::IsChanged() {
   return m_isChanged;
}

//+------------------------------------------------------------------+
//| Сбросить признак наличия изменений в виртуальных позициях        |
//+------------------------------------------------------------------+
void CStrategy::ResetChanges() {
   m_isChanged = false;
}

Остальные методы — Tick(), Volume() и CountOrders() мы реализуем или в наследниках базового класса, или в самом базовом классе, но позже.

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

#include "Strategy.mqh"

//+------------------------------------------------------------------+
//| Базовый класс перевода открытых объемов в рыночные позиции       |
//+------------------------------------------------------------------+
class CReceiver {
protected:
   CStrategy         *m_strategies[];  // Массив стратегий
   ulong             m_magic;          // Magic

public:
   CReceiver(ulong p_magic = 0);                // Конструктор
   virtual void      Add(CStrategy *strategy);  // Добавление стратегии
   virtual bool      Correct();                 // Корректировка открытых объемов
};

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CReceiver::CReceiver(ulong p_magic) : m_magic(p_magic) {
   ArrayResize(m_strategies, 0, 128);
}

//+------------------------------------------------------------------+
//| Добавление стратегии                                             |
//+------------------------------------------------------------------+
void CReceiver::Add(CStrategy *strategy) {
   APPEND(m_strategies, strategy);
}

//+------------------------------------------------------------------+
//| Корректировка открытых объемов                                   |
//+------------------------------------------------------------------+
bool CReceiver::Correct() {
   return true;
}

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

Поэтому подготовим два советника, в которых стратегии из предыдущей статьи сами ведут реальную торговлю.

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

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


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

В прошлый раз оптимизацию параметров стратегии мы проводили, используя реализацию стратегии не в виде объекта класса CStrategy. Но теперь у нас уже есть готовый класс CSimpleVolumesStrategy, поэтому давайте создадим отдельную программу, в которой эксперт будет содержать единственный экземпляр этой стратегии. Только мы назовем этот класс немного иначе, чтобы подчеркнуть в названии, что стратегия сама будет открывать рыночные позиции: вместо CSimpleVolumesStrategy будем использовать CSimpleVolumesMarketStrategy и сохраним его в файле SimpleVolumesMarketStrategy.mqh в текущей папке.

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

#include "Advisor.mqh"
#include "SimpleVolumesMarketStrategy.mqh"
#include "VolumeReceiver.mqh"

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
input string      symbol_              = "EURGBP";    // Торговый инструмент (символ)
input ENUM_TIMEFRAMES
timeframe_           = PERIOD_H1;   // Период графика

input group "===  Параметры сигнала к открытию"
input int         signalPeriod_        = 130;   // Количество свечей для усреднения объемов
input double      signalDeviation_     = 0.9;   // Относ. откл. от среднего для открытия первого ордера
input double      signaAddlDeviation_  = 1.4;   // Относ. откл. от среднего для открытия второго и последующих ордеров

input group "===  Параметры отложенных ордеров"
input int         openDistance_        = 0;     // Расстояние от цены до отлож. ордера
input double      stopLevel_           = 2000;  // Stop Loss (в пунктах)
input double      takeLevel_           = 475;   // Take Profit (в пунктах)
input int         ordersExpiration_    = 6000;  // Время истечения отложенных ордеров (в минутах)

input group "===  Параметры управление капиталом"
input int         maxCountOfOrders_    = 3;     // Макс. количество одновременно отрытых ордеров
input double      fixedLot_            = 0.01;  // Объем одного ордера

input group "===  Параметры советника"
input ulong       magic_              = 27181; // Magic

CAdvisor     *expert;         // Указатель на объект эксперта

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   expert = new CAdvisor();
   expert.Add(new CSimpleVolumesMarketStrategy(
                 magic_, symbol_, timeframe_,
                 fixedLot_,
                 signalPeriod_, signalDeviation_, signaAddlDeviation_,
                 openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                 maxCountOfOrders_)
             );       // Добавляем один экземпляр стратегии

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   expert.Tick();
}

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

Сохраним его в файле SimpleVolumesMarketExpertSingle.mq5 в текущей папке.

Теперь немного усложним торговую стратегию, чтобы упростить реализацию поставленной задачи. Нам будет проще перевести на виртуальную торговлю такую стратегию, которая использует рыночные позиции, а не отложенные ордера. А текущий вариант стратегии как раз работает только с отложенными ордерами. Давайте добавим в стратегию анализ значения параметра openDistance_. Если он больше нуля, то стратегия будет открывать отложенные ордера BUY_STOP и SELL_STOP. Если он меньше нуля, то стратегия будет открывать отложенные ордера BUY_LIMIT и SELL_LIMIT. Если же он равен нулю, то будут открываться рыночные позиции. 

Для этого достаточно внести изменения в код методов CSimpleVolumesMarketStrategy::OpenBuyOrder() и CSimpleVolumesMarketStrategy::OpenSellOrder().

void CSimpleVolumesMarketStrategy::OpenBuyOrder() {
// Предшествующий код в методе ...

// Объем ордера
   double lot = m_fixedLot;

// Устанавливаем отложенный ордер
   bool res = false;
   if(openDistance_ > 0) {
      res = trade.BuyStop(lot, ...);
   } else if(openDistance_ < 0) {
      res = trade.BuyLimit(lot, ...);
   } else {
      res = trade.Buy(lot, ...);
   }

   if(!res) {
      Print("Error opening order");
   }
}

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

Скомпилируем новый советник и поставим его на оптимизацию таймфрейме H1 на трёх символах: EURGBP, GBPUSD и EURUSD.

Рис. 1. Результаты тестирования с параметрами [EURGBP, ]]

Рис. 1. Результаты тестирования для [EURGBP, H1, 17, 1.7, 0.5, 0, 16500, 100, 52000, 3, 0.01]

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

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

#include "Advisor.mqh"
#include "SimpleVolumesMarketStrategy.mqh"

input int startIndex_      = 0;        // Начальный индекс
input int totalStrategies_ = 1;        // Количество стратегий
input double depoPart_     = 1.0;      // Часть депозита для одной стратегии
input ulong  magic_        = 27182;    // Magic

CAdvisor     *expert;                  // Объект эксперта

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Проверяем корректность параметров
   if(startIndex_ < 0 || startIndex_ + totalStrategies_ > 9) {
      return INIT_PARAMETERS_INCORRECT;
   }

// Создаем и наполняем массив из экземпляров стратегий
   CStrategy *strategies[9];
   strategies[0] = new CSimpleVolumesMarketStrategy(
      magic_ + 0, "EURGBP", PERIOD_H1,
      NormalizeDouble(0.01 / 0.16 * depoPart_, 2),
      13, 0.3, 1.0, 0, 10500, 465, 1000, 3);
   strategies[1] = new CSimpleVolumesMarketStrategy(
      magic_ + 1, "EURGBP", PERIOD_H1,
      NormalizeDouble(0.01 / 0.09 * depoPart_, 2),
      17, 1.7, 0.5, 0, 16500, 220, 1000, 3);
   strategies[2] = new CSimpleVolumesMarketStrategy(
      magic_ + 2, "EURGBP", PERIOD_H1,
      NormalizeDouble(0.01 / 0.16 * depoPart_, 2),
      51, 0.5, 1.1, 0, 19500, 370, 22000, 3);
   strategies[3] = new CSimpleVolumesMarketStrategy(
      magic_ + 3, "GBPUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.25 * depoPart_, 2),
      80, 1.1, 0.2, 0, 6000, 1190, 1000, 3);
   strategies[4] = new CSimpleVolumesMarketStrategy(
      magic_ + 4, "GBPUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.09 * depoPart_, 2),
      128, 2.0, 0.9, 0, 2000, 1170, 1000, 3);
   strategies[5] = new CSimpleVolumesMarketStrategy(
      magic_ + 5, "GBPUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.14 * depoPart_, 2),
      13, 1.5, 0.8, 0, 2500, 1375, 1000, 3);
   strategies[6] = new CSimpleVolumesMarketStrategy(
      magic_ + 6, "EURUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.23 * depoPart_, 2),
      24, 0.1, 0.3, 0, 7500, 2400, 24000, 3);
   strategies[7] = new CSimpleVolumesMarketStrategy(
      magic_ + 7, "EURUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.20 * depoPart_, 2),
      18, 0.2, 0.4, 0, 19500, 1480, 6000, 3);
   strategies[8] = new CSimpleVolumesMarketStrategy(
      magic_ + 8, "EURUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.22 * depoPart_, 2),
      128, 0.7, 0.3, 0, 3000, 170, 42000, 3);

   expert = new CAdvisor();

// Добавляем нужные стратегии в эксперта
   for(int i = startIndex_; i < startIndex_ + totalStrategies_; i++) {
      expert.Add(strategies[i]);
   }

   return(INIT_SUCCEEDED);
}

void OnTick() {
   expert.Tick();
}

void OnDeinit(const int reason) {
   delete expert;
}

Благодаря этому мы можем использовать оптимизацию по параметру начального индекса стратегий в массиве стратегий для получения результатов для каждого экземпляра стратегии. Запустим её на начальном депозите $100 000 и получим такие результаты.


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

Из этих результатов видно, что просадка составляет около 1% от начального депозита, то есть примерно $1000, как мы и планировали, подбирая оптимальный размер открываемых позиций. Коэффициент Шарпа составляет в среднем 1.3.

Теперь включим все экземпляры и подберем подходящий множитель depoPart_ для сохранения просадки в $1000. Получаем, что при depoPart_ = 0.38, просадка остается в пределах допустимой.


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

Сравнивая результаты работы одиночных экземпляров стратегий и результаты одновременной работы всех экземпляров можно заметить, что при одинаковой просадке мы получили увеличение прибыли примерно в 3 раза, а также увеличение коэффициента Шарпа с 1.3 до 2.84.

Теперь приступим к реализации основной задачи.


Класс виртуальных позиций (ордеров)

Итак, создадим обещанный класс CVirtualOrder и добавим в него поля для хранения всех свойств открытых позиций.

class CVirtualOrder {
private:
//--- Свойства ордера (позиции)
   ulong             m_id;          // Уникальный ID

   string            m_symbol;      // Символ
   double            m_lot;         // Объем
   ENUM_ORDER_TYPE   m_type;        // Тип
   double            m_openPrice;   // Цена открытия
   double            m_stopLoss;    // Уровень StopLoss
   double            m_takeProfit;  // Уровень TakeProfit
   string            m_comment;     // Комментарий

   datetime          m_openTime;    // Время открытия

//--- Свойства закрытого ордера (позиции)
   double            m_closePrice;  // Цена закрытия
   datetime          m_closeTime;   // Время закрытия
   string            m_closeReason; // причина закрытия

   double            m_point;       // Величина пункта

   bool              m_isStopLoss;  // Признак срабатывания StopLoss
   bool              m_isTakeProfit;// Признак срабатывания TakeProfit
};

Каждая виртуальная позиция должна иметь уникальный идентификатор. Поэтому добавим статическую переменную класса s_count для подсчёта количества всех создаваемых в программе объектов позиций. При создании нового объекта позиции этот счётчик увеличивается на 1 и это значение становится уникальным номером позиции. Начальное значение s_count установим равным 0.

Также нам пригодится объект класса CSymbolInfo для получения информации о ценах. Сделаем его тоже статическим членом класса. 

class CVirtualOrder {
private:
   static int        s_count;
   static
   CSymbolInfo       s_symbolInfo;

//--- Свойства ордера (позиции) ...

};

int               CVirtualOrder::s_count = 0;
CSymbolInfo       CVirtualOrder::s_symbolInfo;

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

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

class CVirtualOrder {
//--- Предыдущий код...

public:
                     CVirtualOrder();  // Конструктор
                     
//--- Методы проверки состояния ордера (позиции)
   bool              IsOpen();         // Ордер открыт?
   bool              IsMarketOrder();  // Это рыночная позиция?
   bool              IsBuyOrder();     // Это открытая позиция BUY?
   bool              IsSellOrder();    // Это открытая позиция SELL?
  
//--- Методы получения свойств ордера (позиции)
   double            Volume();         // Объем с направлением
   double            Profit();         // Текущая прибыль

//--- Методы обработки ордеров (позиций)
   bool              Open(string symbol,
                          ENUM_ORDER_TYPE type,
                          double lot,
                          double sl = 0,
                          double tp = 0,
                          string comment = "",
                          bool inPoints = true);   // Открытие ордера (позиции)
   bool              Close();                      // Закрытие ордера (позиции)
};

Реализация некоторых из этих методов очень простая и короткая, поэтому ее можно поместить прямо внутри описания класса, например:

class CVirtualOrder : public CObject {
// ...

//--- Методы проверки состояния ордера (позиции)
   bool              IsOpen() {        // Ордер открыт?
      return(this.m_openTime > 0 && this.m_closeTime == 0);
   };
   bool              IsMarketOrder() { // Это рыночная позиция?
      return IsOpen() && (m_type == ORDER_TYPE_BUY || m_type == ORDER_TYPE_SELL);
   }
   
// ...
};

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

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CVirtualOrder::CVirtualOrder() :
// Список инициализации
   m_id(++s_count),  // Новый идентификатор = счётчик объектов + 1
   m_symbol(""),
   m_lot(0),
   m_type(-1),
   m_openPrice(0),
   m_stopLoss(0),
   m_takeProfit(0),
   m_openTime(0),
   m_comment(""),
   m_closePrice(0),
   m_closeTime(0),
   m_closeReason(""),
   m_point(0) {
}

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

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

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

Добавим в класс CVirtualOrder метод Tick(), который будет проверять условия закрытия позиции, и, если они выполнены, то позиция будет переводиться в состояние закрытой. Если произошло изменение состояния позиции, то метод будет возвращать true.

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

class CVirtualOrder {
private:
   //...
public:
   //...
   
//--- Методы обработки ордеров (позиций)
   bool              Open(string symbol,
                          ENUM_ORDER_TYPE type,
                          double lot,
                          double sl = 0,
                          double tp = 0,
                          string comment = "",
                          bool inPoints = false
                         );      // Открытие ордера (позиции)

   bool              Tick();     // Обработка тика для ордера (позиции)
   bool              Close();    // Закрытие ордера (позиции)

   static bool       Tick(CVirtualOrder &orders[]);   // Обработка тика для массива виртуальных ордеров
};               

//...

//+------------------------------------------------------------------+
//| Обработка тика одного виртуального ордера (позиции)              |
//+------------------------------------------------------------------+
bool CVirtualOrder::Tick() {
   if(IsMarketOrder()) {  // Если это рыночная виртуальная позиция
      if(CheckClose()) {  // Проверяем, достигнуты ли уровни SL или TP
         Close();         // Закрываем при достижении
         return true;     // Возвращаем, что есть изменения в составе открытых позиций
      }
   }

   return false;
}

//+------------------------------------------------------------------+
//| Обработка тика для массива виртуальных ордеров (позиций)         |
//+------------------------------------------------------------------+
bool CVirtualOrder::Tick(CVirtualOrder &orders[]) {
   bool isChanged = false;                      // Предполагаем, что изменений не будет
   for(int i = 0; i < ArraySize(orders); i++) { // Для всех ордеров (позиций)
      isChanged |= orders[i].Tick();            // Проверяем и закрываем, если надо
   }
   return isChanged;
}
//+------------------------------------------------------------------+

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


Доработка класса простой торговой стратегии

Теперь мы можем вернуться к классу торговой стратегии и внести в него изменения, позволяющие работать с виртуальными позициями. Как мы уже договорились, в базовом классе CStrategy у нас уже есть массив m_orders[] для хранения объектов виртуальных позиций. Поэтому в классе CSimpleVolumesStrategy он тоже доступен. У рассматриваемой стратегии есть параметр m_maxCountOfOrders, который определяет максимальное количество одновременно открытых позиций. Тогда установим в конструкторе размер массива виртуальных позиций, равный этому параметру.

Далее нам просто надо заменить открытие реальных позиций в методах OpenBuyOrder() и OpenSellOrder() на открытие виртуальных. Открытие реальных отложенных ордеров пока что заменить нечем, поэтому просто закомментируем эти операции.

//+------------------------------------------------------------------+
//| Открытие ордера BUY                                              |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::OpenBuyOrder() {
// ...
   
   if(m_openDistance > 0) {
      /* // Устанавливаем отложенный ордер BUY STOP
         res = trade.BuyStop(lot, ...);  */
   } else if(m_openDistance < 0) {
      /* // Устанавливаем отложенный ордер BUY LIMIT
         res = trade.BuyLimit(lot, ...); */
   } else {
      // Открытие виртуальной позиции BUY
      for(int i = 0; i < m_maxCountOfOrders; i++) {   // Перебираем все виртуальные позиции
         if(!m_orders[i].IsOpen()) {                  // Если нашли не открытую, то открываем
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot,
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits));
            break;                                    // и выходим
         }
      }
   } 

   ... 
}

//+------------------------------------------------------------------+
//| Открытие ордера SELL                                             |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::OpenSellOrder() {
// ...
   
   if(m_openDistance > 0) {
      /* // Устанавливаем отложенный ордер SELL STOP
      res = trade.SellStop(lot, ...);          */
   } else if(m_openDistance < 0) {
      /* // Устанавливаем отложенный ордер SELL LIMIT
      res = trade.SellLimit(lot, ...);         */
   } else {
      // Открытие виртуальной позиции SELL
      for(int i = 0; i < m_maxCountOfOrders; i++) {   // Перебираем все виртуальные позиции
         if(!m_orders[i].IsOpen()) {                  // Если нашли не открытую, то открываем
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_SELL, m_fixedLot,
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits));
            break;                                    // и выходим
         }
      }
   }

   ... 
}

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


Создание класса перевода открытых объемов в рыночные позиции

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

#include "Receiver.mqh"

//+------------------------------------------------------------------+
//| Класс перевода открытых объемов в рыночные позиции               |
//+------------------------------------------------------------------+
class CVolumeReceiver : public CReceiver {
protected:
   bool              m_isNetting;      // Это неттинг-счёт?
   string            m_symbols[];      // Массив используемых символов

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

   CPositionInfo     m_position;
   CSymbolInfo       m_symbolInfo;
   CTrade            m_trade;

   // Заполнение массива открытых рыночных объемов по символам
   void              FillSymbolVolumes(double &oldVolumes[]);

   // Коррекция открытых объёмов по массиву объёмов
   virtual bool      Correct(double &symbolVolumes[]);

   // Коррекция объема по данному символу
   bool              CorrectPosition(string symbol, double oldVolume, double diffVolume);

   // Вспомогательные методы
   bool              ClearOpen(string symbol, double diffVolume);
   bool              AddBuy(string symbol, double volume);
   bool              AddSell(string symbol, double volume);

   bool              CloseBuyPartial(string symbol, double volume);
   bool              CloseSellPartial(string symbol, double volume);
   bool              CloseHedgingPartial(string symbol, double volume, ENUM_POSITION_TYPE type);
   bool              CloseFull(string symbol = "");

   bool              FreeMarginCheck(string symbol, double volume, ENUM_ORDER_TYPE type);

public:
   CVolumeReceiver(ulong p_magic, double p_minMargin = 100);   // Конструктор
   virtual void      Add(CStrategy *strategy) override;        // Добавление стратегии
   virtual bool      Correct() override;                       // Коррекция открытых объёмов
};

Общий алгоритм метода коррекции открытых объемов такой:

  • Для каждого используемого символа перебираем все стратегии и считаем суммарный открытый объём по каждому используемому символу. В итоге получаем массив newVolumes, который передаем следующему перегруженному методу Correct()
    //+------------------------------------------------------------------+
    //| Коррекция открытых объёмов                                       |
    //+------------------------------------------------------------------+
    bool CVolumeReceiver::Correct() {
       int symbolsTotal = ArraySize(m_symbols);
       double newVolumes[];
    
       ArrayResize(newVolumes, symbolsTotal);
       ArrayInitialize(newVolumes, 0);
    
       for(int j = 0; j < symbolsTotal; j++) {  // Для каждого используемого символа        
          for(int i = 0; i < ArraySize(m_strategies); i++) { // Перебираем все стратегии
             if(m_strategies[i].Symbol() == m_symbols[j]) {  // Если стратегия использует этот символ
                newVolumes[j] += m_strategies[i].Volume();   // Добавим ее открытый объём
             }
          }
       }
       // Вызываем метод коррекция открытых объёмов по массиву объёмов
       return Correct(newVolumes);
    }
  • Для каждого символа находим, на сколько надо изменить объём открытых позиций по символу. При необходимости вызываем метод коррекции объема по данному символу
    //+------------------------------------------------------------------+
    //| Коррекция открытых объёмов по массиву объёмов                    |
    //+------------------------------------------------------------------+
    bool CVolumeReceiver::Correct(double &newVolumes[]) {
       // ...
       bool res = true;
    
       // Для каждого символа
       for(int j = 0; j < ArraySize(m_symbols); j++) {
          // ...
          // Находим, на сколько надо изменить объём открытых позиций по символу
          double oldVolume = oldVolumes[j];
          double newVolume = newVolumes[j];
          
          // ...
          double diffVolume = newVolume - oldVolume;
          
          // Если есть необходимость коррекции объема по данному символу, то выполняем её 
          if(MathAbs(diffVolume) > 0.001) {
             res = res && CorrectPosition(m_symbols[j], oldVolume, diffVolume);
          }
       }
    
       return res;
    }
  • Для одного символа исходя из значений предыдущего открытого объёма и необходимого изменения определяем, какой вид торговой операции нам нужно совершить (добавить, закрыть, закрыть и переоткрыть) и вызываем соответствующий вспомогательный метод:
    //+------------------------------------------------------------------+
    //| Коррекция объема по данному символу                              |
    //+------------------------------------------------------------------+
    bool CVolumeReceiver::CorrectPosition(string symbol, double oldVolume, double diffVolume) {
       bool res = false;
    
       // ...
    
       double volume = MathAbs(diffVolume);
    
       if(oldVolume > 0) { // Have BUY position
          if(diffVolume > 0) { // New BUY position
             res = AddBuy(symbol, volume);
          } else if(diffVolume < 0) { // New SELL position
             if(volume < oldVolume) {
                res = CloseBuyPartial(symbol, volume);
             } else {
                res = CloseFull(symbol);
    
                if(res && volume > oldVolume) {
                   res = AddSell(symbol, volume - oldVolume);
                }
             }
          }
       } else if(oldVolume < 0) { // Have SELL position
          if(diffVolume < 0) { // New SELL position
             res = AddSell(symbol, volume);
          } else if(diffVolume > 0) { // New BUY position
             if(volume < -oldVolume) {
                res = CloseSellPartial(symbol, volume);
             } else {
                res = CloseFull(symbol);
    
                if(res && volume > -oldVolume) {
                   res = AddBuy(symbol, volume + oldVolume);
                }
             }
          }
       } else { // No old position
          res = ClearOpen(symbol, diffVolume);
       }
    
       return res;
    }

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


Советник с одной стратегией и с виртуальными позициями

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

#include "Advisor.mqh"
#include "SimpleVolumesStrategy.mqh"
#include "VolumeReceiver.mqh"

// Input-параметры ...

CAdvisor     *expert;         // Указатель на объект эксперта

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   expert = new CAdvisor(new CVolumeReceiver(magic_));
   expert.Add(new CSimpleVolumesStrategy(
                         symbol_, timeframe_,
                         fixedLot_,
                         signalPeriod_, signalDeviation_, signaAddlDeviation_,
                         openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                         maxCountOfOrders_)
                     );       // Добавляем один экземпляр стратегии

   return(INIT_SUCCEEDED);
}

void OnTick() {
   expert.Tick();
}

void OnDeinit(const int reason) {
   delete expert;
}

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


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

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

Рис. 4. Сделки, совершенные двумя советниками (без виртуальных позиций и с ними)

Для уменьшения ширины из таблиц удалены столбцы с одинаковыми значениями во всех строках, такие как символ (всегда EURGBP), объем (всегда 0.01) и другие. Как видно, открытие первых позиций происходит в обоих случаях по одной и той же цене в те же моменты времени. Но если при имеющейся открытой позиции SELL (2018.03.02 15:46:47 sell in), открывается еще одна противоположная позиция BUY (2018.03.06 13:56:04 buy in), то советник, работающий через виртуальные позиции, просто закрывает ранее открытую позицию SELL (2018.03.06 13:56:04 buy out). Общий результат от этого только улучшился, так как первый советник продолжал платить свопы для открытых разнонаправленных позиций, во второй — нет.

Советник с несколькими стратегиями и с виртуальными позициями

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

Рис. 5. Результаты работы советника с девятью экземплярами стратегии и виртуальными позициями

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


Заключение

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

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

В общем, продолжение следует.


Последние комментарии | Перейти к обсуждению на форуме трейдеров (55)
fxsaber
fxsaber | 13 февр. 2024 в 07:34

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

Обсуждение статьи "Разрабатываем мультивалютный советник (Часть 2): Переход к виртуальным позициям торговых стратегий"

fxsaber, 2024.02.12 17:33

Ваша архитектура несколько отличается от моей

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy( const string sInputs ) : CStrategy(sInputs)
{
   this.Input = sInputs;

   ArrayResize(m_orders, this.Input.maxCountOfOrders);

   // Загружаем индикатор для получения тиковых объемов
   iVolumesHandle = iVolumes(this.InputStrategy.symbol, this.InputStrategy.timeframe, VOLUME_TICK);

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

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

fxsaber
fxsaber | 14 февр. 2024 в 11:36
Yuriy Bykov #:

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

Вы правы. Сделал так.
expert.Add(new CSimpleVolumesStrategy(inInputsAll));
Это глобальная string-переменная, в которую автоматически (и создается) прописываются  все input-переменные. Т.е. какие бы объекты не создавались, на вход всегда подается эта переменная.
Stanislav Korotky
Stanislav Korotky | 14 февр. 2024 в 13:21
fxsaber #:
Вы правы. Сделал так. Это глобальная string-переменная, в которую автоматически (и создается) прописываются  все input-переменные. Т.е. какие бы объекты не создавались, на вход всегда подается эта переменная.

На всякий случай напоминаю, что строковые инпуты режутся по 63 символу оптимизатором.

fxsaber
fxsaber | 14 февр. 2024 в 13:31
Stanislav Korotky #:

На всякий случай напоминаю, что строковые инпуты режутся по 63 символу оптимизатором.

Спасибо. Там не инпут, поэтому длина не ограничена.

string inInputsAll = NULL;
fxsaber
fxsaber | 14 февр. 2024 в 18:45

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

Обсуждение статьи "Разрабатываем мультивалютный советник (Часть 2): Переход к виртуальным позициям торговых стратегий"

fxsaber, 2024.02.14 11:36

Вы правы. Сделал так.
expert.Add(new CSimpleVolumesStrategy(inInputsAll));
Это глобальная string-переменная, в которую автоматически (и создается) прописываются  все input-переменные. Т.е. какие бы объекты не создавались, на вход всегда подается эта переменная.

Прикрепил.

Теория категорий в MQL5 (Часть 22): Другой взгляд на скользящие средние Теория категорий в MQL5 (Часть 22): Другой взгляд на скользящие средние
В этой статье мы попытаемся упростить описание концепций, рассматриваемых в этой серии, остановившись только на одном индикаторе - наиболее распространенном и, вероятно, самом легком для понимания. Речь идет о скользящей средней. Также мы рассмотрим значение и возможные применения вертикальных естественных преобразований.
Популяционные алгоритмы оптимизации: Искусственные мультисоциальные поисковые объекты (artificial Multi-Social search Objects, MSO) Популяционные алгоритмы оптимизации: Искусственные мультисоциальные поисковые объекты (artificial Multi-Social search Objects, MSO)
Продолжение предыдущей статьи как развитие идеи социальных групп. В новой статье исследуется эволюция социальных групп с использованием алгоритмов перемещения и памяти. Результаты помогут понять эволюцию социальных систем и применить их в оптимизации и поиске решений.
Создаем простой мультивалютный советник с использованием MQL5 (Часть 2): Сигналы индикатора - мультитаймфреймовый Parabolic SAR Создаем простой мультивалютный советник с использованием MQL5 (Часть 2): Сигналы индикатора - мультитаймфреймовый Parabolic SAR
Под мультивалютным советником в этой статье понимается советник, или торговый робот, который может торговать (открывать/закрывать ордера, управлять ордерами, например, трейлинг-стоп-лоссом и трейлинг-профитом) более чем одной парой символов с одного графика. На этот раз мы будем использовать только один индикатор, а именно Parabolic SAR или iSAR на нескольких таймфреймах, начиная с PERIOD_M15 и заканчивая PERIOD_D1.
Оцениваем будущую производительность с помощью доверительных интервалов Оцениваем будущую производительность с помощью доверительных интервалов
В этой статье мы углубимся в применение методов бутстреппинга (bootstrapping) как средства оценки будущей эффективности автоматизированной стратегии.