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

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

MetaTrader 5Торговые системы | 29 февраля 2024, 16:18
715 5
Yuriy Bykov
Yuriy Bykov

Введение

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

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

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

  • добавим возможность открывать виртуальные отложенные ордера (Buy Stop, Sell Stop, Buy Limit, Sell Limit), а не только виртуальные позиции (Buy, Sell);
  • добавим простой способ визуализации выставленных виртуальных ордеров и позиций, чтобы мы могли визуально контролировать при тестировании правильность реализации правил открытия позиций/ордеров в используемых торговых стратегиях;
  • реализуем сохранение советником информации о текущем состоянии, чтобы при перезагрузке терминала или при необходимости перенести советник на другой терминал он мог бы продолжить работу из того состояния, в котором он оказался в момент прерывания своей работы.

Начнём с самого простого — работы с виртуальными отложенными ордерами.


Виртуальные отложенные ордера

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

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

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

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

Приступим к добавлениям в класс. Мы добавим:

  • свойство m_expiration для хранения времени истечения;
  • логическое свойство m_isExpired для признака истечения времени существования отложенного ордера;
  • метод CheckTrigger() для проверки срабатывания отложенного ордера, несколько методов проверки, является ли данный объект принадлежащим к определённому виду (это отложенный ордер, лимитный отложенный ордер и т.д.);
  • к методам проверки, является ли данный объект по направлению BUY или SELL условия, чтобы истина возвращалась и в тех случаях, когда это отложенный ордер нужного направления, независимо от того, лимитный он или стоповый. 
  • в список параметров метода Open() цену открытия и время истечения.

//+------------------------------------------------------------------+
//| Класс виртуальных ордеров и позиций                              |
//+------------------------------------------------------------------+
class CVirtualOrder {
private:
   ...
//--- Свойства ордера (позиции)
   ...
   datetime          m_expiration;     // Время истечения
   ...
   bool              m_isExpired;      // Признак истечения времени
   ...
//--- Частные методы
   ...
   bool              CheckTrigger();   // Проверка срабатывания отложенного ордера

public:
    ...

//--- Методы проверки состояния позиции (ордера)
   ...
   bool              IsPendingOrder() {// Это отложенный ордер?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_LIMIT
                          || m_type == ORDER_TYPE_BUY_STOP
                          || m_type == ORDER_TYPE_SELL_LIMIT
                          || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsBuyOrder() {    // Это открытая позиция BUY?
      return IsOpen() && (m_type == ORDER_TYPE_BUY
                          || m_type == ORDER_TYPE_BUY_LIMIT
                          || m_type == ORDER_TYPE_BUY_STOP);
   }
   bool              IsSellOrder() {   // Это открытая позиция SELL?
      return IsOpen() && (m_type == ORDER_TYPE_SELL 
                          || m_type == ORDER_TYPE_SELL_LIMIT
                          || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsStopOrder() {   // Это отложенный STOP-ордер?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_STOP || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsLimitOrder() {  // Это отложенный LIMIT-ордер?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_LIMIT || m_type == ORDER_TYPE_SELL_LIMIT);
   }
   ...

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

   ...
};

В методе открытия виртуальной позиции Open(), который теперь будет открывать и виртуальные отложенные ордера, мы сначала добавим присваивание свойству m_openPrice переданного значения цены открытия, а потом, если выяснится, что это не отложенный ордер, а позиция, присвоение этому свойству актуальной рыночной цены открытия:

bool CVirtualOrder::Open(string symbol,         // Символ
                         ENUM_ORDER_TYPE type,  // Тип (BUY или SELL)
                         double lot,            // Объём
                         double price = 0,      // Цена открытия
                         double sl = 0,         // Уровень StopLoss (цена или пункты)
                         double tp = 0,         // Уровень TakeProfit (цена или пункты)
                         string comment = "",   // Комментарий
                         datetime expiration = 0,  // Время истечения
                         bool inPoints = false  // Уровни SL и TP заданы в пунктах?
                        ) {
   ...

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

      // Инициализируем свойства позиции
      m_openPrice = price;
     ...
      m_expiration = expiration;

      // Открываемая позиция (ордер) не является закрытой по SL, TP или истечению
      ...
      m_isExpired = false;

      ...
      // В зависимости от направления устанавливаем цену открытия и уровни SL и TP.
      // Если SL и TP заданы в пунктах, то предварительно вычисляем их ценовые уровни
      // относительно цены открытия
      if(IsBuyOrder()) {
         if(type == ORDER_TYPE_BUY) {
            m_openPrice = s_symbolInfo.Ask();
         }
         ...
      } else if(IsSellOrder()) {
         if(type == ORDER_TYPE_SELL) {
            m_openPrice = s_symbolInfo.Bid();
         }
         ...
      }

      ...

      return true;
   }
   return false;
}

В методе проверки срабатывания виртуального отложенного ордера CheckTrigger() мы получаем текущую рыночную цену Bid или Ask в зависимости от направления ордера и проверяем, не достигла ли она цены открытия с нужной стороны. Если достигла, то меняем свойство m_type текущего объекта на значение, соответствующее позиции нужного направления, и оповещаем объекты получателя и стратегии, что открылась новая виртуальная позиция.

//+------------------------------------------------------------------+
//| Проверка срабатывания отложенного ордера                         |
//+------------------------------------------------------------------+
bool CVirtualOrder::CheckTrigger() {
   if(IsPendingOrder()) {
      s_symbolInfo.Name(m_symbol);     // Выбираем нужный символ
      s_symbolInfo.RefreshRates();     // Обновляем информацию о текущих ценах
      double price = (IsBuyOrder()) ? s_symbolInfo.Ask() : s_symbolInfo.Bid();
      int spread = s_symbolInfo.Spread();

      // Если цена дошла до уровней открытия, то превращаем ордер в позицию
      if(false
            || (m_type == ORDER_TYPE_BUY_LIMIT && price <= m_openPrice)
            || (m_type == ORDER_TYPE_BUY_STOP  && price >= m_openPrice)
        ) {
         m_type = ORDER_TYPE_BUY;
      } else if(false
                || (m_type == ORDER_TYPE_SELL_LIMIT && price >= m_openPrice)
                || (m_type == ORDER_TYPE_SELL_STOP  && price <= m_openPrice)
               ) {
         m_type = ORDER_TYPE_SELL;
      }

      // Если ордер превратился в позицию
      if(IsMarketOrder()) {
         m_openPrice = price; // Запоминаем цену открытия

         // Оповещаем получатель и стратегию, что открыта позиция
         m_receiver.OnOpen(GetPointer(this));
         m_strategy.OnOpen();
         return true;
      }
   }
   return false;
}

Этот метод будет вызываться при обработке нового тика в методе Tick() в том случае, если это действительно виртуальный отложенный ордер:

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

В методе Tick() вызывается метод CheckClose(), в который тоже нужно добавить код, проверяющий закрытие виртуального отложенного ордера по времени истечения:

//+------------------------------------------------------------------+
//| Проверка необходимости закрытия по SL, TP или EX                 |
//+------------------------------------------------------------------+
bool CVirtualOrder::CheckClose() {
   if(IsMarketOrder()) {               // Если это открытая рыночная виртуальная позиция, то
      ...
      // Проверяем, что цена достигла SL или TP
      ...
   } else if(IsPendingOrder()) {    // Если это виртуальный отложенный ордер
      // Проверяем, было ли достигнуто время истечения, если оно задано
      if(m_expiration > 0 && m_expiration < TimeCurrent()) {
         m_isExpired = true;
         return true;
      }
   }
   return false;
}

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

Теперь давайте вернемся к классу нашей торговой стратегии CSimpleVolumesStrategy. В ней мы оставляли места для будущих правок, в которых нам понадобится добавить поддержку работы с виртуальными отложенными ордерами. Такие места были в двух методах OpenBuyOrder() и OpenSellOrder(). Добавим в этих местах вызов метода Open() с параметрами, приводящими к открытию виртуальных отложенных ордеров. Цену открытия мы предварительно вычислим из текущей цены, отступив от неё в нужную сторону на количество пунктов, задаваемых параметром m_openDistance. Приведем код только для метода OpenBuyOrder(), в другом правки будут аналогичны.

//+------------------------------------------------------------------+
//| Открытие ордера BUY                                              |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::OpenBuyOrder() {
// Обновляем информацию о текущих ценах для символа
   ...
// Берем необходимую нам информацию о символе и ценах
   ...

// Сделаем, чтобы расстояние открытия было не меньше спреда
   int distance = MathMax(m_openDistance, spread);

// Цена открытия
   double price = ask + distance * point;

// Уровни StopLoss и TakeProfit
   ...

// Время истечения
   datetime expiration = TimeCurrent() + m_ordersExpiration * 60;

   ...
   for(int i = 0; i < m_maxCountOfOrders; i++) {   // Перебираем все виртуальные позиции
      if(!m_orders[i].IsOpen()) {                  // Если нашли не открытую, то открываем
         if(m_openDistance > 0) {
            // Устанавливаем отложенный ордер SELL STOP
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_STOP, m_fixedLot,
                                   NormalizeDouble(price, digits),
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits),
                                   "", expiration);

         } else if(m_openDistance < 0) {
            // Устанавливаем отложенный ордер SELL LIMIT
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_LIMIT, m_fixedLot,
                                   NormalizeDouble(price, digits),
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits),
                                   "", expiration);

         } else {
            // Открытие виртуальной позиции SELL
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot,
                                   0,
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits));

         }
         break; // и выходим
      }
   }
  ...
}

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

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

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


Сохранение состояния

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

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

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

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

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

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

Приступим к реализации. Поскольку у нас есть иерархическая структура объектов вида

  • эксперт
    • стратегии
      • виртуальные позиции

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

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


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

Добавим в класс CVirtualAdvisor поле m_name для хранения имени эксперта. Поскольку оно не будет изменяться в ходе работы, сделаем его назначение в конструкторе. Также имеет смысл сразу расширить имя за счёт добавления к нему магического номера и опционального суффикса ".test" в том случае, когда советник запускается в режиме визуального тестирования.

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

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

public:
   CVirtualAdvisor(ulong p_magic = 1, string p_name = ""); // Конструктор
   ...

   virtual bool      Save();           // Сохранение состояния
   virtual bool      Load();           // Загрузка состояния
};

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

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "") :
   ...
   m_lastSaveTime(0) {
   m_name = StringFormat("%s-%d%s.csv",
                         (p_name != "" ? p_name : "Expert"),
                         p_magic,
                         (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                        );
};

Логику проверки необходимости выполнения сохранения мы поместим внутри метода Save(). Поэтому на каждом тике мы можем просто добавить вызов этого метода после выполнения остальных действий на тике:

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

// Запуск обработки в стратегиях
   CAdvisor::Tick();

// Корректировка рыночных объемов
   m_receiver.Correct();

// Сохранение состояния
   Save();
}

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

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

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

//+------------------------------------------------------------------+
//| Сохранение состояния                                             |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Save() {
   bool res = true;

   // Сохраняем состояние, если:
   if(true
         // появились более поздние изменения
         && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime
         // и сейчас не оптимизация
         && !MQLInfoInteger(MQL_OPTIMIZATION)
         // и сейчас не тестирование либо сейчас визуальное тестирование
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_WRITE, '\t');

      if(f != INVALID_HANDLE) {  // Если файл открыт, то сохраняем
         FileWrite(f, CVirtualReceiver::s_lastChangeTime);  // Время последних изменений
         FileWrite(f, ArraySize(m_strategies));             // Количество стратегий

         // Все стратегии
         FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save(f));

         FileClose(f);

         // Обновляем время последнего сохранения
         m_lastSaveTime = CVirtualReceiver::s_lastChangeTime;
         PrintFormat(__FUNCTION__" | OK at %s to %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_name);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d",
                     m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

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

Метод загрузки состояния Load() должен выполнять похожую работу, только вместо записи будет происходить чтение данных из файла. То есть сначала мы читаем записанное время и количество стратегий. Тут можно на всякий случай проверить, соответствует ли прочитанное количество стратегий количеству добавленных в этот эксперт стратегий. Если нет — то дальше читать смысла нет, это какой-то неправильный файл. Если да, то всё в порядке, можно читать дальше. А дальше мы снова поручаем последующую работу объектам следующего уровня иерархии: перебираем все добавленные стратегии и вызываем их метод чтения из открытого файла.

В коде это может выглядеть примерно так:

//+------------------------------------------------------------------+
//| Загрузка состояния                                               |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Load() {
   bool res = true;

   // Загружаем состояние, если:
   if(true
         // файл существует
         && FileIsExist(m_name)
         // и сейчас не оптимизация
         && !MQLInfoInteger(MQL_OPTIMIZATION)
         // и сейчас не тестирование либо сейчас визуальное тестирование
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_READ, '\t');

      if(f != INVALID_HANDLE) {  // Если файл открыт, то загружаем
         m_lastSaveTime = FileReadDatetime(f);     // Время последнего сохранения
         PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

         // Количество стратегий
         long f_strategiesCount = StringToInteger(FileReadString(f));

         // Загруженное количество стратегий совпадает с текущим?
         res = (ArraySize(m_strategies) == f_strategiesCount);

         if(res) {
            // Загружаем все стратегии
            FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).Load(f));

            if(!res) {
               PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name);
            }
         } else {
            PrintFormat(__FUNCTION__" | ERROR: Wrong strategies count (%d expected but %d found in file %s)",
                        ArraySize(m_strategies), f_strategiesCount, m_name);
         }
         FileClose(f);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

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

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

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Создаем и наполняем массив из экземпляров стратегий
   CStrategy *strategies[9];
   strategies[0] = ...
   ...
   strategies[8] = ...

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

// Добавляем стратегии в эксперта
   FOREACH(strategies, expert.Add(strategies[i]));

// Загружаем прошлое состояние при наличии   
   expert.Load();

   return(INIT_SUCCEEDED);
}

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


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

Добавим в базовый класс торговой стратегии, использующей виртуальные позиции, методы Save() и Load(), а также метод преобразования текущего объекта стратегии в строку. Для краткости давайте этот метод реализуем как перегруженный унарный оператор ~ (тильда).

//+------------------------------------------------------------------+
//| Класс торговой стратегии с виртуальными позициями                |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
   ...

public:
   ...

   virtual bool      Load(const int f);   // Загрузка состояния
   virtual bool      Save(const int f);   // Сохранение состояния

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

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

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

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

//+------------------------------------------------------------------+
//| Сохранение состояния                                             |
//+------------------------------------------------------------------+
bool CVirtualStrategy::Save(const int f) {
   bool res = true;
   FileWrite(f, ~this); // Сохраняем параметры

   // Сохраняем виртуальные позиции (ордера) стратегии
   FOREACH(m_orders, res &= m_orders[i].Save(f));

   return res;
}

Метод загрузки Load() будет просто читать данные в том же порядке, в котором они записывались, с проверкой соответствия строки параметров в файле и в стратегии:

//+------------------------------------------------------------------+
//| Загрузка состояния                                               |
//+------------------------------------------------------------------+
bool CVirtualStrategy::Load(const int f) {
   bool res = true;
   // Текущие параметры равны прочитанным параметрам   
   res = (~this == FileReadString(f));
   
   // Если да, то загружеам виртуальные позиции (ордера) стратегии
   if(res) {
      FOREACH(m_orders, res &= m_orders[i].Load(f));
   }

   return res;
}

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


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

В классе конкретной торговой стратегии CSimpleVolumesStrategy нам нужно будет добавить такой же набор методов, как и в базовом классе:

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

public:
   ...

   virtual bool      Load(const int f) override;   // Загрузка состояния
   virtual bool      Save(const int f) override;   // Сохранение состояния

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

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

//+------------------------------------------------------------------+
//| Преобразование объекта в строку                                  |
//+------------------------------------------------------------------+
string CSimpleVolumesStrategy::operator~() {
   return StringFormat("%s(%s,%s,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",
                       // Параметры данного экземпляра стратегии
                       typename(this), m_symbol, EnumToString(m_timeframe), m_fixedLot,
                       m_signalPeriod, m_signalDeviation, m_signaAddlDeviation,
                       m_openDistance, m_stopLevel, m_takeLevel, m_ordersExpiration,
                       m_maxCountOfOrders
                      );
}

Да, это получилось еще одно место, где нам надо снова написать в коде все параметры стратегии. Мы будем помнить про него пока не дойдём до работы со входными параметрами.

Метод сохранения Save() получается весьма лаконичным, так как основную работу будет выполнять базовый класс:

//+------------------------------------------------------------------+
//| Сохранение состояния                                             |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::Save(const int f) {
   bool res = true;
   FileWrite(f, ~this);                // Сохраняем параметры
   res &= CVirtualStrategy::Save(f);   // Сохраняем стратегию
   return res;
}

Метод загрузки Load() будет немного объемнее, но в основном за счет повышения читабельности кода:

//+------------------------------------------------------------------+
//| Загрузка состояния                                               |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::Load(const int f) {
   bool res = true;
   string currentParams = ~this;             // Текущие параметры
   string loadedParams = FileReadString(f);  // Прочитанные параметры

   PrintFormat(__FUNCTION__" | %s", loadedParams);

   res = (currentParams == loadedParams);

   // Если прочитанные параметры совпадают с текущими, то загружаем
   if(res) {
      res &= CVirtualStrategy::Load(f);
   }

   return res;
}

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


Модификация виртуальных позиций

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

class CVirtualOrder {
   ...

   virtual bool      Load(const int f);   // Загрузка состояния
   virtual bool      Save(const int f);   // Сохранение состояния

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

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

//+------------------------------------------------------------------+
//| Преобразование объекта в строку                                  |
//+------------------------------------------------------------------+
string CVirtualOrder::operator~() {
   if(IsOpen()) {
      return StringFormat("#%d %s %s %.2f in %s at %.5f (%.5f, %.5f). %s, %f",
                          m_id, TypeName(), m_symbol, m_lot,
                          TimeToString(m_openTime), m_openPrice,
                          m_stopLoss, m_takeProfit,
                          TimeToString(m_closeTime), m_closePrice);
   } else {
      return StringFormat("#%d --- ", m_id);
   }

}

Хотя, возможно, что в дальнейшем мы это изменим, добавив еще метод чтения свойств объекта из строки.

В методе сохранения Save() у нас наконец-то будет записываться в файл какая-то более существенная информация:

//+------------------------------------------------------------------+
//| Сохранение состояния                                             |
//+------------------------------------------------------------------+
bool CVirtualOrder::Save(const int f) {
   FileWrite(f, m_id, m_symbol, m_lot, m_type, m_openPrice,
             m_stopLoss, m_takeProfit,
             m_openTime, m_closePrice, m_closeTime,
             m_expiration, m_comment, m_point);
   return true;
}

Метод загрузки Load() будет затем не только считывать то, что было записано, заполняя нужные свойства прочитанной информацией, но и оповещать связанные объекты стратегии и получателя о том, открыта эта виртуальная позиция (ордер) или закрыта:

//+------------------------------------------------------------------+
//| Загрузка состояния                                               |
//+------------------------------------------------------------------+
bool CVirtualOrder::Load(const int f) {
   m_id = (ulong) FileReadNumber(f);
   m_symbol = FileReadString(f);
   m_lot = FileReadNumber(f);
   m_type = (ENUM_ORDER_TYPE) FileReadNumber(f);
   m_openPrice = FileReadNumber(f);
   m_stopLoss = FileReadNumber(f);
   m_takeProfit = FileReadNumber(f);
   m_openTime = FileReadDatetime(f);
   m_closePrice = FileReadNumber(f);
   m_closeTime = FileReadDatetime(f);
   m_expiration = FileReadDatetime(f);
   m_comment = FileReadString(f);
   m_point = FileReadNumber(f);

   PrintFormat(__FUNCTION__" | %s", ~this);

// Оповещаем получатель и стратегию, что позиция (ордер) открыта
   if(IsOpen()) {
      m_receiver.OnOpen(GetPointer(this));
      m_strategy.OnOpen();
   } else {
      m_receiver.OnClose(GetPointer(this));
      m_strategy.OnClose();
   }

   return true;
}

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


Тестирование сохранения

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

Так как мы добавили в код вывод сообщений о ходе загрузки, то при перезапуске советника (нам проще всего для этого просто запустить повторную компиляцию) мы увидим в логах примерно следующее:

CVirtualAdvisor::Load | LAST SAVE at 2027.02.23 08:05:33

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.06,13,0.30,1.00,0,10500.00,465.00,1000,3)
CVirtualOrder::Load | Order#1 EURGBP 0.06 BUY in 2027.02.23 08:02 at 0.85494 (0.75007, 0.85985). 1970.01.01 00:00, 0.000000
CVirtualReceiver::OnOpen#EURGBP | OPEN VirtualOrder #1
CVirtualOrder::Load | Order#2  ---
CVirtualOrder::Load | Order#3  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.11,17,1.70,0.50,210,16500.00,220.00,1000,3)
CVirtualOrder::Load | Order#4 EURGBP 0.11 BUY STOP in 2027.02.23 08:02 at 0.85704 (0.69204, 0.85937). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#5  ---
CVirtualOrder::Load | Order#6  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.06,51,0.50,1.10,500,19500.00,370.00,22000,3)
CVirtualOrder::Load | Order#7 EURGBP 0.06 BUY STOP in 2027.02.23 08:02 at 0.85994 (0.66494, 0.86377). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#8  ---
CVirtualOrder::Load | Order#9  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.04,80,1.10,0.20,0,6000.00,1190.00,1000,3)
CVirtualOrder::Load | Order#10 GBPUSD 0.04 BUY in 2027.02.23 08:02 at 1.26632 (1.20638, 1.27834). 1970.01.01 00:00, 0.000000
CVirtualReceiver::OnOpen#GBPUSD | OPEN VirtualOrder #10
CVirtualOrder::Load | Order#11  ---
CVirtualOrder::Load | Order#12  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.11,128,2.00,0.90,220,2000.00,1170.00,1000,3)
CVirtualOrder::Load | Order#13 GBPUSD 0.11 BUY STOP in 2027.02.23 08:02 at 1.26852 (1.24852, 1.28028). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#14  ---
CVirtualOrder::Load | Order#15  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.07,13,1.50,0.80,550,2500.00,1375.00,1000,3)
CVirtualOrder::Load | Order#16 GBPUSD 0.07 BUY STOP in 2027.02.23 08:02 at 1.27182 (1.24682, 1.28563). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#17  ---
CVirtualOrder::Load | Order#18  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.04,24,0.10,0.30,330,7500.00,2400.00,24000,3)
CVirtualOrder::Load | Order#19 EURUSD 0.04 BUY STOP in 2027.02.23 08:02 at 1.08586 (1.01086, 1.10990). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#20  ---
CVirtualOrder::Load | Order#21  ---
CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.05,18,0.20,0.40,220,19500.00,1480.00,6000,3)
CVirtualOrder::Load | Order#22 EURUSD 0.05 BUY STOP in 2027.02.23 08:02 at 1.08476 (0.88976, 1.09960). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#23  ---
CVirtualOrder::Load | Order#24  ---
CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.05,128,0.70,0.30,550,3000.00,170.00,42000,3)
CVirtualOrder::Load | Order#25 EURUSD 0.05 BUY STOP in 2027.02.23 08:02 at 1.08806 (1.05806, 1.08980). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#26  ---
CVirtualOrder::Load | Order#27  ---

CVirtualAdvisor::Save | OK at 2027.02.23 08:19:48 to SimpleVolumes-27182.csv

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

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


Визуализация

Настала очередь визуализации виртуальных позиций и отложенных ордеров. С первого взгляда кажется вполне естественным выполнить её в виде расширения класса виртуальной позиции CVirtualOrder. Тут уже есть вся необходимая информация о визуализируемом объекте, он лучше всех знает, когда нужно себя перерисовать. Собственно, первая черновая реализация была сделана именно так. Но дальше начали всплывать весьма неприятные моменты, за пазухой которых прослеживались свёрнутые плакаты с явно длинными списками вопросов, которые они в любой момент готовы будут развернуть со словами: "Ах вы так? Тогда мы — вот так!"

Одним из таких невинных вопросов был такой: "И на какой же график вы планируете меня визуализировать?" Простой ответ — на текущий — годился только для случая, когда советник будет работать на одном символе, причём он будет совпадать с символом графика, на котором этот советник запущен. Как только символов становится несколько, то отображать все виртуальные позиции на графике одного символа становится очень неудобно. На графике возникает каша, которая никакой дополнительной ясности не вносит, а только запутывает.

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

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

Так что создадим новый базовый класс для всех объектов, которые так или иначе будут связаны с визуализацией чего-то на графиках:

//+------------------------------------------------------------------+
//| Базовый класс визуализации различных объектов                    |
//+------------------------------------------------------------------+
class CInterface {
protected:
   static ulong      s_magic;       // Magic эксперта
   bool              m_isActive;    // Интерфейс активен?
   bool              m_isChanged;   // Есть ли изменения у объекта?
public:
   CInterface();                    // Конструктор
   virtual void      Redraw() = 0;  // Отрисовка на графике изменённых объектов
   virtual void      Changed() {    // Установка флага наличия изменений
      m_isChanged = true;
   }
};

ulong CInterface::s_magic = 0;

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CInterface::CInterface() :
   m_isActive(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)),
   m_isChanged(true) {}

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

На основе этого класса создадим два новых класса:

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

Состав класса CVirtualChartOrder будет примерно таким:

//+------------------------------------------------------------------+
//| Класс графической виртуальной позиции                            |
//+------------------------------------------------------------------+
class CVirtualChartOrder : public CInterface {
   CVirtualOrder*    m_order;          // Связанная виртуальная позиция (ордер)
   CChart            m_chart;          // Объект графика для отображения

   // Объекты на графике для отображения виртуальной позиции
   CChartObjectHLine m_openLine;       // Линия цены открытия

   long              FindChart();      // Поиск/открытие нужного графика
public:
   CVirtualChartOrder(CVirtualOrder* p_order);     // Конструктор
   ~CVirtualChartOrder();                          // Деструктор

   bool              operator==(const ulong id) {  // Оператор сравнения по Id
      return m_order.Id() == id;
   }

   void              Show();    // Показ виртуальной позиции (ордера)
   void              Hide();    // Скрытие виртуальной позиции (ордера)

   virtual void      Redraw() override;   // Перерисовка виртуальной позиции (ордера)
};

Метод перерисовки Redraw() будет проверять, надо ли её выполнять, и при необходимости вызывать методы показа или скрытия виртуальной позиции с графика:

//+------------------------------------------------------------------+
//| Перерисовка виртуальной позиции (ордера)                         |
//+------------------------------------------------------------------+
void CVirtualChartOrder::Redraw() {
   if(m_isChanged) {
      if(m_order.IsOpen()) {
         Show();
      } else {
         Hide();
      }
      m_isChanged = false;
   }
}

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

//+------------------------------------------------------------------+
//| Поиск графика для отображения                                    |
//+------------------------------------------------------------------+
long CVirtualChartOrder::FindChart() {
   if(m_chart.ChartId() == -1 || m_chart.Symbol() != m_order.Symbol()) {
      long currChart, prevChart = ChartFirst();
      int i = 0, limit = 1000;

      currChart = prevChart;

      while(i < limit) { // у нас наверняка не больше 1000 открытых графиков
         if(ChartSymbol(currChart) == m_order.Symbol()) {
            return currChart;
         }
         currChart = ChartNext(prevChart); // на основании предыдущего получим новый график
         if(currChart < 0)
            break;        // достигли конца списка графиков
         prevChart = currChart; // запомним идентификатор текущего графика для ChartNext()
         i++;
      }

      // Если подходящий график не найден, то откроем новый
      if(currChart == -1) {
         m_chart.Open(m_order.Symbol(), PERIOD_CURRENT);
      }
   }
   return m_chart.ChartId();
}

Метод Show() пока просто рисует одну горизонтальную линию, соответствующую цене открытия. Её цвет и вид будет определяться в зависимости от направления и типа позиции (ордера). Метод Hide() будет эту линию удалять. Дальнейшее обогащение этого класса будет происходить именно в этих методах.

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

Реализация класса CVirtualInterface может быть выполнена, например, так:

//+------------------------------------------------------------------+
//| Класс графического интерфейса советника                          |
//+------------------------------------------------------------------+
class CVirtualInterface : public CInterface {
protected:
// Статический указатель на единственный экземпляр данного класса
   static   CVirtualInterface *s_instance;

   CVirtualChartOrder *m_chartOrders[];   // Массив графических виртуальных позиций

//--- Частные методы
   CVirtualInterface();   // Закрытый конструктор

public:
   ~CVirtualInterface();  // Деструктор

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

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

   virtual void      Redraw() override;   // Отрисовка на графике изменённых объектов
};

В статическом методе создания единственного экземпляра Instance() добавим инициализацию статической переменной s_magic, если был передан ненулевой магический номер:

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

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

//+------------------------------------------------------------------+
//| Обработка изменения виртуальной позиции                          |
//+------------------------------------------------------------------+
void CVirtualInterface::Changed(CVirtualOrder *p_order) {
   // Запомним, что изменения есть у данной позиции
   int i;
   FIND(m_chartOrders, p_order.Id(), i);
   if(i != -1) {
      m_chartOrders[i].Changed();
      m_isChanged = true;
   }
}

И, наконец, в методе отрисовки интерфейса Redraw() мы будем вызывать в цикле методы отрисовки всех графических виртуальных позиций:

//+------------------------------------------------------------------+
//| Отрисовка на графике изменённых объектов                         |
//+------------------------------------------------------------------+
void CVirtualInterface::Redraw() {
   if(m_isActive && m_isChanged) {  // Если интерфейс активен и есть изменения
      // Запускаем перерисовку графических виртуальных позиций
      FOREACH(m_chartOrders, m_chartOrders[i].Redraw());
      m_isChanged = false;          // Сбрасываем флаг изменений
   }
}

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

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

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

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "") :
   ...
// Инициализируем интерфейс статическим интерфейсом
   m_interface(CVirtualInterface::Instance(p_magic)),
   ... {
   ...
};

//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   
... 
   delete m_interface;        // Удаляем интерфейс
}

В обработчике события OnTick добавим после всех операций вызов метода перерисовки интерфейса, так как это наименее важная часть обработки тика: 

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

// Запуск обработки в стратегиях
   CAdvisor::Tick();

// Корректировка рыночных объемов
   m_receiver.Correct();

// Сохранение состояния
   Save();
   
// Отрисовка интерфейса
   m_interface.Redraw();
}

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

//+------------------------------------------------------------------+
//| Класс перевода открытых объемов в рыночные позиции (получатель)  |
//+------------------------------------------------------------------+
class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualInterface
   *m_interface;                          // Объект интерфейса для показа состояния пользователю

   ...
};

//+------------------------------------------------------------------+
//| Закрытый конструктор                                             |
//+------------------------------------------------------------------+
CVirtualReceiver::CVirtualReceiver() :
   m_interface(CVirtualInterface::Instance()),
   ... {}

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

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

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

void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   m_interface.Changed(p_order);
   ...
}

//+------------------------------------------------------------------+
//| Обработка закрытия виртуальной позиции                           |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   m_interface.Changed(p_order);
   ...
   }
}

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

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

Рис. 1. Отображение виртуальных ордеров и позиций на графике

Рис. 1. Отображение виртуальных ордеров и позиций на графике

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


Красивый график

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

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

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

Вот два примера тестовых прогонов советника, в котором используется около 170 экземпляров стратегий, работающих на различных символах и таймфреймах. Период тестирования: с 2023-01-01 по 2024-02-23, данные этого периода не использовались при оптимизации и обучении. В настройках управления капиталом были поставлены параметры, предполагающие в качестве допустимой просадки в одном случае значение около 10%, а в другом случае — около 40%.


Рис. 2. Результаты тестирования с допустимой просадкой 10%


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

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


Заключение

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

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

Спасибо за интерес и до встречи в следующей части!

Прикрепленные файлы |
Advisor.mqh (4.3 KB)
Interface.mqh (3.22 KB)
Macros.mqh (2.3 KB)
Receiver.mqh (1.8 KB)
Strategy.mqh (1.74 KB)
VirtualAdvisor.mqh (13.35 KB)
VirtualOrder.mqh (38.33 KB)
VirtualReceiver.mqh (17.61 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (5)
fxsaber
fxsaber | 29 февр. 2024 в 23:46

Не использовал бы такую архитектуру.


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

Графические объекты в виртуалке - ну это снова все в одну кучу.

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

Yuriy Bykov
Yuriy Bykov | 1 мар. 2024 в 07:10

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

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

Графические объекты мне были нужны для контроля при разработке торговых стратегий, поэтому они были реализованы. Причём класс CVirtualOrder при этом вообще не изменялся. А вот добавить четыре новые строки кода в клаcc CVirtualReceiver действительно пришлось. Выбрал именно такой вариант среди разных возможных.

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

fxsaber
fxsaber | 1 мар. 2024 в 07:31
Yuriy Bykov #:

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

У меня, к сожалению, туго со временем, чтобы на примере частичного рефакторинга показать свое виденье.


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

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


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


Под "перезагрузкой" имеется в виду и ситуация, когда происходит пауза в работе советнике. Например, долгий OrderSend или reping. Поэтому нужно запрашивать ценовые данные с момента прошлого запроса. И их прогонять в виртуалке.

Yuriy Bykov
Yuriy Bykov | 3 мар. 2024 в 12:26
fxsaber #:

У меня, к сожалению, туго со временем, чтобы на примере частичного рефакторинга показать свое виденье.

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

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

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

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

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

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

fxsaber
fxsaber | 3 мар. 2024 в 12:31

Yuriy Bykov #:

должен быть реализован виртуальный тестер

Он у Вас уже почти есть: в уже реализованную виртуальную торговлю прокидывайте тики "Тестера".

Интеграция ML-моделей с тестером стратегий (Заключение): Реализация регрессионной модели для прогнозирования цен Интеграция ML-моделей с тестером стратегий (Заключение): Реализация регрессионной модели для прогнозирования цен
В данной статье описывается реализация регрессионной модели на основе дерева решений для прогнозирования цен финансовых активов. Мы уже провели подготовку данных, обучение и оценку модели, а также ее корректировку и оптимизацию. Однако важно отметить, что данная модель является лишь исследованием и не должна использоваться при реальной торговле.
Мультибот в MetaTrader (Часть II): улучшенный динамический шаблон Мультибот в MetaTrader (Часть II): улучшенный динамический шаблон
Развивая тему предыдущей статьи про мультибота, я решил создать более гибкий и функциональный шаблон, который обладает большими возможностями и может эффективно применяться как во фрилансе, так и использоваться в виде базы для разработки мультивалютных и мультипериодных советников с возможностью интеграции с внешними решениями.
Нейросети — это просто (Часть 79): Агрегирование запросов в контексте состояния (FAQ) Нейросети — это просто (Часть 79): Агрегирование запросов в контексте состояния (FAQ)
В предыдущей статье мы познакомились с одним из методом обнаружение объектов на изображении. Однако, обработка статического изображения несколько отличается от работы с динамическими временными рядами, к которым относится и динамика анализируемых нами цен. В данной статье я хочу предложить Вам познакомиться с методом обнаружения объектов на видео, что несколько ближе к решаемой нами задаче.
Базовый класс популяционных алгоритмов как основа эффективной оптимизации Базовый класс популяционных алгоритмов как основа эффективной оптимизации
Уникальная исследовательская попытка объединения разнообразных популяционных алгоритмов в единый класс с целью упрощения применения методов оптимизации. Этот подход не только открывает возможности для разработки новых алгоритмов, включая гибридные варианты, но и создает универсальный базовый стенд для тестирования. Этот стенд становится ключевым инструментом для выбора оптимального алгоритма в зависимости от конкретной задачи.