Библиотека для простого и быстрого создания программ для MetaTrader (Часть X): Совместимость с MQL4 - События открытия позиции и активации отложенных ордеров

Artyom Trishkin | 23 мая, 2019

Содержание


Тестовый советник

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

Для работы возьмём тестовый советник TestDoEasyPart08.mq5 из восьмой части описания библиотеки из папки \MQL5\Experts\TestDoEasy\Part08 и сохраним его под новым именем TestDoEasyPart10.mq4 в папке MetaTrader 4 \MQL4\Experts\TestDoEasy\Part10.

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


Перейдём к первой ошибке, которая говорит нам об отсутствии подключаемого файла


и исправим — файл будем подключать только для MQL5:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart08.mq5 |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2018, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
#ifdef __MQL5__
#include <Trade\Trade.mqh>
#endif 
//--- enums

Скомпилируем. 33 ошибки. Опять перейдём к самой первой ошибке, которая указывает нам об отсутствующем типе при объявлении объекта торгового класса CTrade — его нет в MQL4.

Делаем точно так же — используем директиву условной компиляции:

//--- global variables
CEngine        engine;
#ifdef __MQL5__
CTrade         trade;
#endif 
SDataButt      butt_data[TOTAL_BUTT];

Скомпилируем. Теперь объект trade класса CTrade стал неизвестным для MQL4, исправим точно так же:

//--- setting trade parameters
#ifdef __MQL5__
   trade.SetDeviationInPoints(slippage);
   trade.SetExpertMagicNumber(magic_number);
   trade.SetTypeFillingBySymbol(Symbol());
   trade.SetMarginMode();
   trade.LogLevel(LOG_LEVEL_NO);
#endif 
//---
   return(INIT_SUCCEEDED);
  }

И далее по всему коду советника просто все вхождения объектов trade заключим в директивы условной компиляции, но с использованием директивы #elseтам будет размещён код для MQL4. Посмотрим на примере самой первой ошибки с неизвестным типом trade после внесения предыдущей правки и компиляции:

      //--- Если нажата кнопка BUTT_BUY: Открыть позицию Buy
      if(button==EnumToString(BUTT_BUY))
        {
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY,0,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY,0,takeprofit);
         //--- Открываем позицию Buy
         #ifdef __MQL5__
            trade.Buy(lot,Symbol(),0,sl,tp);
         #else 
                                            
         #endif 
        }

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


И если поглядеть внимательно в код, то станет ясна причина замешательства компилятора:

//+------------------------------------------------------------------+
//| Возвращает флаг наличия объекта с префиксом                      |
//+------------------------------------------------------------------+
bool IsPresentObects(const string object_prefix)
  {
   for(int i=ObjectsTotal(0)-1;i>=0;i--)
      if(StringFind(ObjectName(0,i,0),object_prefix)>WRONG_VALUE)
         return true;
   return false;
  }
//+------------------------------------------------------------------+

В MQL5 данная функция имеет только одну форму вызова:

int  ObjectsTotal(
   long  chart_id,           // идентификатор графика
   int   sub_window=-1,      // индекс окна
   int   type=-1             // тип объекта     
   );

Где первый параметр — это идентификатор графика (текущего если 0)

В то время как в MQL4 с некоторых пор функция имеет две формы вызова. Первая такая же, как и в MQL5:

int  ObjectsTotal(
   long  chart_id,           // идентификатор графика
   int   sub_window=-1,      // индекс окна
   int   type=-1             // тип объекта     
   );

и вторая — устаревшая, с одним лишь параметром:

int  ObjectsTotal(
   int   type=EMPTY         // тип объекта     
   );

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

Решение простое — передадим вторым параметром номер подокна (0 = главное окно графика):

//+------------------------------------------------------------------+
//| Возвращает флаг наличия объекта с префиксом                      |
//+------------------------------------------------------------------+
bool IsPresentObects(const string object_prefix)
  {
   for(int i=ObjectsTotal(0,0)-1;i>=0;i--)
      if(StringFind(ObjectName(0,i,0),object_prefix)>WRONG_VALUE)
         return true;
   return false;
  }
//+------------------------------------------------------------------+

и

//+------------------------------------------------------------------+
//| Контроль состояния кнопок                                        |
//+------------------------------------------------------------------+
void PressButtonsControl(void)
  {
   int total=ObjectsTotal(0,0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
  }
//+------------------------------------------------------------------+

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

В файле DELib.mqh в конце листинга напишем все необходимые нам тестерные функции для MQL4:

#ifdef __MQL4__
//+------------------------------------------------------------------+
//| Временные функции MQL4 для тестера                               |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Открытие позиции Buy                                             |
//+------------------------------------------------------------------+
bool Buy(const double volume,const string symbol=NULL,const ulong magic=0,const double sl=0,const double tp=0,const string comment=NULL,const int deviation=2)
  {
   string sym=(symbol==NULL ? Symbol() : symbol);
   double price=0;
   ResetLastError();
   if(!SymbolInfoDouble(sym,SYMBOL_ASK,price))
     {
      Print(DFUN,TextByLanguage("Не удалось получить цену Ask. Ошибка ","Could not get Ask price. Error "),(string)GetLastError());
      return false;
     }
   if(!OrderSend(sym,ORDER_TYPE_BUY,volume,price,deviation,sl,tp,comment,(int)magic,0,clrBlue))
     {
      Print(DFUN,TextByLanguage("Не удалось открыть позицию Buy. Ошибка ","Failed to open a Buy position. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Установка отложенного ордера BuyLimit                            |
//+------------------------------------------------------------------+
bool BuyLimit(const double volume,const double price_set,const string symbol=NULL,const ulong magic=0,const double sl=0,const double tp=0,const string comment=NULL,const int deviation=2)
  {
   string sym=(symbol==NULL ? Symbol() : symbol);
   ResetLastError();
   if(!OrderSend(sym,ORDER_TYPE_BUY_LIMIT,volume,price_set,deviation,sl,tp,comment,(int)magic,0,clrBlue))
     {
      Print(DFUN,TextByLanguage("Не удалось установить ордер BuyLimit. Ошибка ","Could not place order BuyLimit. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Установка отложенного ордера BuyStop                             |
//+------------------------------------------------------------------+
bool BuyStop(const double volume,const double price_set,const string symbol=NULL,const ulong magic=0,const double sl=0,const double tp=0,const string comment=NULL,const int deviation=2)
  {
   string sym=(symbol==NULL ? Symbol() : symbol);
   ResetLastError();
   if(!OrderSend(sym,ORDER_TYPE_BUY_STOP,volume,price_set,deviation,sl,tp,comment,(int)magic,0,clrBlue))
     {
      Print(DFUN,TextByLanguage("Не удалось установить ордер BuyStop. Ошибка ","Could not place order BuyStop. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Открытие позиции Sell                                            |
//+------------------------------------------------------------------+
bool Sell(const double volume,const string symbol=NULL,const ulong magic=0,const double sl=0,const double tp=0,const string comment=NULL,const int deviation=2)
  {
   string sym=(symbol==NULL ? Symbol() : symbol);
   double price=0;
   ResetLastError();
   if(!SymbolInfoDouble(sym,SYMBOL_BID,price))
     {
      Print(DFUN,TextByLanguage("Не удалось получить цену Bid. Ошибка ","Could not get Bid price. Error "),(string)GetLastError());
      return false;
     }
   if(!OrderSend(sym,ORDER_TYPE_SELL,volume,price,deviation,sl,tp,comment,(int)magic,0,clrRed))
     {
      Print(DFUN,TextByLanguage("Не удалось открыть позицию Sell. Ошибка ","Failed to open a Sell position. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Установка отложенного ордера SellLimit                           |
//+------------------------------------------------------------------+
bool SellLimit(const double volume,const double price_set,const string symbol=NULL,const ulong magic=0,const double sl=0,const double tp=0,const string comment=NULL,const int deviation=2)
  {
   string sym=(symbol==NULL ? Symbol() : symbol);
   ResetLastError();
   if(!OrderSend(sym,ORDER_TYPE_SELL_LIMIT,volume,price_set,deviation,sl,tp,comment,(int)magic,0,clrRed))
     {
      Print(DFUN,TextByLanguage("Не удалось установить ордер SellLimit. Ошибка ","Could not place order SellLimit. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Установка отложенного ордера SellStop                            |
//+------------------------------------------------------------------+
bool SellStop(const double volume,const double price_set,const string symbol=NULL,const ulong magic=0,const double sl=0,const double tp=0,const string comment=NULL,const int deviation=2)
  {
   string sym=(symbol==NULL ? Symbol() : symbol);
   ResetLastError();
   if(!OrderSend(sym,ORDER_TYPE_SELL_STOP,volume,price_set,deviation,sl,tp,comment,(int)magic,0,clrRed))
     {
      Print(DFUN,TextByLanguage("Не удалось установить ордер SellStop. Ошибка ","Could not place order SellStop. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Закрытие позиции по тикету                                       |
//+------------------------------------------------------------------+
bool PositionClose(const ulong ticket,const double volume=0,const int deviation=2)
  {
   ResetLastError();
   if(!OrderSelect((int)ticket,SELECT_BY_TICKET))
     {
      Print(DFUN,TextByLanguage("Не удалось выбрать позицию. Ошибка ","Could not select position. Error "),(string)GetLastError());
      return false;
     }
   if(OrderCloseTime()>0)
     {
      Print(DFUN,TextByLanguage("Позиция уже закрыта","Position already closed"));
      return false;
     }
   ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderType();
   if(type>ORDER_TYPE_SELL)
     {
      Print(DFUN,TextByLanguage("Ошибка. Не позиция: ","Error. Not position: "),OrderTypeDescription(type)," #",ticket);
      return false;
     }
   double price=0;
   color  clr=clrNONE;
   if(type==ORDER_TYPE_BUY)
     {
      price=SymbolInfoDouble(OrderSymbol(),SYMBOL_BID);
      clr=clrBlue;
     }
   else
     {
      price=SymbolInfoDouble(OrderSymbol(),SYMBOL_ASK);
      clr=clrRed;
     }
   double vol=(volume==0 || volume>OrderLots() ? OrderLots() : volume);
   ResetLastError();
   if(!OrderClose((int)ticket,vol,price,deviation,clr))
     {
      Print(DFUN,TextByLanguage("Не удалось закрыть позицию. Ошибка ","Could not close position. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Закрытие позиции встречной                                       |
//+------------------------------------------------------------------+
bool PositionCloseBy(const ulong ticket,const ulong ticket_by)
  {
   ResetLastError();
   if(!OrderSelect((int)ticket,SELECT_BY_TICKET))
     {
      Print(DFUN,TextByLanguage("Не удалось выбрать позицию. Ошибка ","Could not select position. Error "),(string)GetLastError());
      return false;
     }
   if(OrderCloseTime()>0)
     {
      Print(DFUN,TextByLanguage("Позиция уже закрыта","Position already closed"));
      return false;
     }
   ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderType();
   if(type>ORDER_TYPE_SELL)
     {
      Print(DFUN,TextByLanguage("Ошибка. Не позиция: ","Error. Not position: "),OrderTypeDescription(type)," #",ticket);
      return false;
     }
   ResetLastError();
   if(!OrderSelect((int)ticket_by,SELECT_BY_TICKET))
     {
      Print(DFUN,TextByLanguage("Не удалось выбрать встречную позицию. Ошибка ","Could not select the opposite position. Error "),(string)GetLastError());
      return false;
     }
   if(OrderCloseTime()>0)
     {
      Print(DFUN,TextByLanguage("Встречная позиция уже закрыта","Opposite position already closed"));
      return false;
     }
   ENUM_ORDER_TYPE type_by=(ENUM_ORDER_TYPE)OrderType();
   if(type_by>ORDER_TYPE_SELL)
     {
      Print(DFUN,TextByLanguage("Ошибка. Встречная позиция не является позицией: ","Error. Opposite position is not a position: "),OrderTypeDescription(type_by)," #",ticket_by);
      return false;
     }
   color clr=(type==ORDER_TYPE_BUY ? clrBlue : clrRed);
   ResetLastError();
   if(!OrderCloseBy((int)ticket,(int)ticket_by,clr))
     {
      Print(DFUN,TextByLanguage("Не удалось закрыть позицию встречной. Ошибка ","Could not close position by opposite position. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Удаление отложенного ордера по тикету                            |
//+------------------------------------------------------------------+
bool PendingOrderDelete(const ulong ticket)
  {
   ResetLastError();
   if(!OrderSelect((int)ticket,SELECT_BY_TICKET))
     {
      Print(DFUN,TextByLanguage("Не удалось выбрать ордер. Ошибка ","Could not select order. Error "),(string)GetLastError());
      return false;
     }
   if(OrderCloseTime()>0)
     {
      Print(DFUN,TextByLanguage("Ордер уже удалён","Order already deleted"));
      return false;
     }
   ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderType();
   if(type<ORDER_TYPE_SELL || type>ORDER_TYPE_SELL_STOP)
     {
      Print(DFUN,TextByLanguage("Ошибка. Не ордер: ","Error. Not order: "),PositionTypeDescription((ENUM_POSITION_TYPE)type)," #",ticket);
      return false;
     }
   color clr=(type<ORDER_TYPE_SELL_LIMIT ? clrBlue : clrRed);
   ResetLastError();
   if(!OrderDelete((int)ticket,clr))
     {
      Print(DFUN,TextByLanguage("Не удалось удалить ордер. Ошибка ","Could not delete order. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Модификация позиции по тикету                                    |
//+------------------------------------------------------------------+
bool PositionModify(const ulong ticket,const double sl,const double tp)
  {
   ResetLastError();
   if(!OrderSelect((int)ticket,SELECT_BY_TICKET))
     {
      Print(DFUN,TextByLanguage("Не удалось выбрать позицию. Ошибка ","Could not select position. Error "),(string)GetLastError());
      return false;
     }
   ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderType();
   if(type>ORDER_TYPE_SELL)
     {
      Print(DFUN,TextByLanguage("Ошибка. Не позиция: ","Error. Not position: "),OrderTypeDescription(type)," #",ticket);
      return false;
     }
   if(OrderCloseTime()>0)
     {
      Print(DFUN,TextByLanguage("Ошибка. Для модификации выбрана закрытая позиция: ","Error. Closed position selected for modification: "),PositionTypeDescription((ENUM_POSITION_TYPE)type)," #",ticket);
      return false;
     }
   color clr=(type==ORDER_TYPE_BUY ? clrBlue : clrRed);
   ResetLastError();
   if(!OrderModify((int)ticket,OrderOpenPrice(),sl,tp,0,clr))
     {
      Print(DFUN,TextByLanguage("Не удалось модифицировать позицию. Ошибка ","Failed to modify position. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Модификация отложенного ордера по тикету                         |
//+------------------------------------------------------------------+
bool PendingOrderModify(const ulong ticket,const double price_set,const double sl,const double tp)
  {
   ResetLastError();
   if(!OrderSelect((int)ticket,SELECT_BY_TICKET))
     {
      Print(DFUN,TextByLanguage("Не удалось выбрать ордер. Ошибка ","Could not select order. Error "),(string)GetLastError());
      return false;
     }
   ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderType();
   if(type<ORDER_TYPE_SELL || type>ORDER_TYPE_SELL_STOP)
     {
      Print(DFUN,TextByLanguage("Ошибка. Не ордер: ","Error. Not order: "),PositionTypeDescription((ENUM_POSITION_TYPE)type)," #",ticket);
      return false;
     }
   if(OrderCloseTime()>0)
     {
      Print(DFUN,TextByLanguage("Ошибка. Для модификации выбран удалённый ордер: ","Error. Deleted order selected for modification: "),OrderTypeDescription(type)," #",ticket);
      return false;
     }
   color clr=(type<ORDER_TYPE_SELL_LIMIT ? clrBlue : clrRed);
   ResetLastError();
   if(!OrderModify((int)ticket,price_set,sl,tp,0,clr))
     {
      Print(DFUN,TextByLanguage("Не удалось модифицировать ордер. Ошибка ","Failed to order modify. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
#endif 

Функции эти будут временными — скоро напишем полноценные торговые классы для MQL5 и MQL4, и эти функции удалим из данного листинга.

Теперь нам необходимо везде, где мы оставили место в коде советника под вызов торговых функций для MQL4, дописать вызов только что написанных функций. Нажмём Ctrl+F и в поле ввода строки для поиска введём trade — таким образом мы быстро будем находить места в коде, где необходимо прописать вызовы торговых MQL4-функций.

Начиная от функции обработки нажатий кнопок PressButtonEvents() и до конца листинга введём в нужные места вызов торговых функций MQL4. Так как кода там много, и выбор нужной функции однозначен и не вызывает сомнений, то весь код здесь приводить не буду — он есть в прикреплённых к статье файлах. Посмотрим лишь на примере обработки нажатий двух кнопок — кнопки открытия позиции Buy и кнопки установки отложенного ордера BuyLimit:

//+------------------------------------------------------------------+
//| Обработка нажатий кнопок                                         |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   //--- Преобразуем имя кнопки в её строковый идентификатор
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- Если кнопка в нажатом состоянии
   if(ButtonState(button_name))
     {
      //--- Если нажата кнопка BUTT_BUY: Открыть позицию Buy
      if(button==EnumToString(BUTT_BUY))
        {
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY,0,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY,0,takeprofit);
         //--- Открываем позицию Buy
         #ifdef __MQL5__
            trade.Buy(lot,Symbol(),0,sl,tp);
         #else 
            Buy(lot,Symbol(),magic_number,sl,tp);
         #endif 
        }
      //--- Если нажата кнопка BUTT_BUY_LIMIT: Выставить BuyLimit
      else if(button==EnumToString(BUTT_BUY_LIMIT))
        {
         //--- Получаем корректную цену установки ордера относительно уровня StopLevel
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_pending);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,takeprofit);
         //--- Устанавливаем ордер BuyLimit
         #ifdef __MQL5__
            trade.BuyLimit(lot,price_set,Symbol(),sl,tp);
         #else 
            BuyLimit(lot,price_set,Symbol(),magic_number,sl,tp);
         #endif 
        }
      //--- Если нажата кнопка BUTT_BUY_STOP: Выставить BuyStop

При тестировании кода библиотеки, заметил странную особенность: события, которые MQL4 видит без доработки кода, отображались в журнале не сразу, а спустя некоторое время. Стал разбираться в причинах, и понял, что всё дело в счётчике таймера коллекций, работающего в таймере CEngine. У нас стоит минимальная задержка в 16 милисекунд для счётчика таймера коллекций, созданного нами в третьей части описания библиотеки при создании базового объекта библиотеки. Но так как мы при работе в тестере не работаем с таймером, и сразу из OnTick() советника вызываем обработчик OnTimer() библиотеки и работаем по тикам, то задержка в 16 милисекунд вытекает в задержку в 16 тиков. Для исправления этой неприятной особенности, пришлось немного доработать класс CEngine, введя в него метод, возвращающий флаг работы в тестере и обработку работы в тестере в обработчике OnTimer(), который в свою очередь вызывается из OnTick() советника при работе в тестере.

Для внесения изменений была создана приватная переменная-член класса и метод, возвращающий значение этой переменной:

//+------------------------------------------------------------------+
//| Класс-основа библиотеки                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
   CHistoryCollection   m_history;                       // Коллекция исторических ордеров и сделок
   CMarketCollection    m_market;                        // Коллекция рыночных ордеров и сделок
   CEventsCollection    m_events;                        // Коллекция событий
   CArrayObj            m_list_counters;                 // Список счётчиков таймера
   bool                 m_first_start;                   // Флаг первого запуска
   bool                 m_is_hedge;                      // Флаг хедж-счёта
   bool                 m_is_tester;                     // Флаг работы в тестере
   bool                 m_is_market_trade_event;         // Флаг торгового события на счёте
   bool                 m_is_history_trade_event;        // Флаг торгового события в истории счёта
   ENUM_TRADE_EVENT     m_acc_trade_event;               // Торговое событие на счёте
//--- Возвращает индекс счётчика по id
public:
   //--- Возвращает список рыночных (1) позиций, (2) отложенных ордеров и (3) маркет-ордеров
   CArrayObj*           GetListMarketPosition(void);
   CArrayObj*           GetListMarketPendings(void);
   CArrayObj*           GetListMarketOrders(void);
   //--- Возвращает список исторических (1) ордеров, (2) удалённых отложенных ордеров, (3) сделок, (4) всех маркет-ордеров позиции по её идентификатору
   CArrayObj*           GetListHistoryOrders(void);
   CArrayObj*           GetListHistoryPendings(void);
   CArrayObj*           GetListDeals(void);
   CArrayObj*           GetListAllOrdersByPosID(const ulong position_id);
//--- Сбрасывает последнее торговое событие
   void                 ResetLastTradeEvent(void)                       { this.m_events.ResetLastTradeEvent(); }
//--- Возвращает (1) последнее торговое событие, (2) флаг счёта-хедж, (3) флаг работы в тестере
   ENUM_TRADE_EVENT     LastTradeEvent(void)                      const { return this.m_acc_trade_event;       }
   bool                 IsHedge(void)                             const { return this.m_is_hedge;              }
   bool                 IsTester(void)                            const { return this.m_is_tester;             }
//--- Создаёт счётчик таймера
   void                 CreateCounter(const int id,const ulong frequency,const ulong pause);
//--- Таймер
   void                 OnTimer(void);
//--- Конструктор/Деструктор
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+

Значение этой переменной-флага работы в тестере устанавливается в конструкторе класса:

//+------------------------------------------------------------------+
//| CEngine конструктор                                              |
//+------------------------------------------------------------------+
CEngine::CEngine() : m_first_start(true),m_acc_trade_event(TRADE_EVENT_NO_EVENT)
  {
   this.m_list_counters.Sort();
   this.m_list_counters.Clear();
   this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE);
   this.m_is_hedge=#ifdef __MQL4__ true #else bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) #endif;
   this.m_is_tester=::MQLInfoInteger(MQL_TESTER);
   ::ResetLastError();
   #ifdef __MQL5__
      if(!::EventSetMillisecondTimer(TIMER_FREQUENCY))
         ::Print(DFUN,"Не удалось создать таймер. Ошибка: ","Could not create timer. Error: ",(string)::GetLastError());
   //---__MQL4__
   #else 
      if(!this.IsTester() && !::EventSetMillisecondTimer(TIMER_FREQUENCY))
         ::Print(DFUN,"Не удалось создать таймер. Ошибка: ","Could not create timer. Error: ",(string)::GetLastError());
   #endif 
  }
//+------------------------------------------------------------------+

В обработчике OnTimer() класса CEngine проверяем работу в тестере и, в зависимости от работы не в тестере или в тестере, работаем соответственно либо по счётчику таймера, либо по тику:

//+------------------------------------------------------------------+
//| CEngine таймер                                                   |
//+------------------------------------------------------------------+
void CEngine::OnTimer(void)
  {
//--- Таймер коллекций исторических ордеров и сделок и рыночных ордеров и позиций
   int index=this.CounterIndex(COLLECTION_COUNTER_ID);
   if(index>WRONG_VALUE)
     {
      CTimerCounter* counter=this.m_list_counters.At(index);
      if(counter!=NULL)
        {
         //--- Если это не тестер
         if(!this.IsTester())
           {
            //--- Если пауза завершилась - работаем с событиями коллекций
            if(counter.IsTimeDone())
               this.TradeEventsControl();
           }
         //--- Если тестер - работаем с событиями коллекций по тику
         else 
           {
            this.TradeEventsControl();
           }
        }
     }
  }
//+------------------------------------------------------------------+

Скомпилируем советник, запустим его в тестере и понажимаем на кнопки:


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

Ну что ж. Будем разбираться с ошибками.

Доработка

Первое, что поглядим — почему библиотека не видит удаление отложенного ордера.
Все события у нас отслеживаются в методе класса коллекции событий CEventsCollection::Refresh(). Нас интересуют события в истории счёта. Переходим к методу и смотрим код, отвечающий за отслеживание изменения в коллекции исторических ордеров и сделок MQL5:
     }
//--- Если событие в истории счёта
   if(is_history_event)
     {
      //--- Если увеличилось количество исторических ордеров
      if(new_history_orders>0)
        {
         //--- Получаем список только удалённых отложенных ордеров
         CArrayObj* list=this.GetListHistoryPendings(list_history);
         if(list!=NULL)
           {
            Print(DFUN);
            //--- Сортируем новый список по времени удаления ордера
            list.Sort(SORT_BY_ORDER_TIME_CLOSE_MSC);
            //--- Берём в цикле с конца списка количество ордеров, равное количеству новых удалённых отложенных ордеров (последние N событий)
            int total=list.Total(), n=new_history_orders;
            for(int i=total-1; i>=0 && n>0; i--,n--)
              {
               //--- Получаем ордер из списка, и если это удалённый отложенный ордер и у него нет идентификатора позиции, 
               //--- то это удаление ордера - устанавливаем торговое событие
               COrder* order=list.At(i);
               if(order!=NULL && order.Status()==ORDER_STATUS_HISTORY_PENDING && order.PositionID()==0)
                  this.CreateNewEvent(order,list_history,list_market);
              }
           }
        }
      //--- Если увеличилось количество сделок

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

//+------------------------------------------------------------------+
//| Закрытый параметрический конструктор                             |
//+------------------------------------------------------------------+
COrder::COrder(ENUM_ORDER_STATUS order_status,const ulong ticket)
  {
//--- Сохранение целочисленных свойств
   this.m_ticket=ticket;
   this.m_long_prop[ORDER_PROP_STATUS]                               = order_status;
   this.m_long_prop[ORDER_PROP_MAGIC]                                = this.OrderMagicNumber();
   this.m_long_prop[ORDER_PROP_TICKET]                               = this.OrderTicket();
   this.m_long_prop[ORDER_PROP_TIME_OPEN]                            = (long)(ulong)this.OrderOpenTime();
   this.m_long_prop[ORDER_PROP_TIME_CLOSE]                           = (long)(ulong)this.OrderCloseTime();
   this.m_long_prop[ORDER_PROP_TIME_EXP]                             = (long)(ulong)this.OrderExpiration();
   this.m_long_prop[ORDER_PROP_TYPE]                                 = this.OrderType();
   this.m_long_prop[ORDER_PROP_STATE]                                = this.OrderState();
   this.m_long_prop[ORDER_PROP_DIRECTION]                            = this.OrderTypeByDirection();
   this.m_long_prop[ORDER_PROP_POSITION_ID]                          = this.OrderPositionID();
   this.m_long_prop[ORDER_PROP_REASON]                               = this.OrderReason();
   this.m_long_prop[ORDER_PROP_DEAL_ORDER_TICKET]                    = this.DealOrderTicket();
   this.m_long_prop[ORDER_PROP_DEAL_ENTRY]                           = this.DealEntry();
   this.m_long_prop[ORDER_PROP_POSITION_BY_ID]                       = this.OrderPositionByID();
   this.m_long_prop[ORDER_PROP_TIME_OPEN_MSC]                        = this.OrderOpenTimeMSC();
   this.m_long_prop[ORDER_PROP_TIME_CLOSE_MSC]                       = this.OrderCloseTimeMSC();
   this.m_long_prop[ORDER_PROP_TIME_UPDATE]                          = (long)(ulong)this.PositionTimeUpdate();
   this.m_long_prop[ORDER_PROP_TIME_UPDATE_MSC]                      = (long)(ulong)this.PositionTimeUpdateMSC();
   
//--- Сохранение вещественных свойств
   this.m_double_prop[this.IndexProp(ORDER_PROP_PRICE_OPEN)]         = this.OrderOpenPrice();
   this.m_double_prop[this.IndexProp(ORDER_PROP_PRICE_CLOSE)]        = this.OrderClosePrice();
   this.m_double_prop[this.IndexProp(ORDER_PROP_PROFIT)]             = this.OrderProfit();
   this.m_double_prop[this.IndexProp(ORDER_PROP_COMMISSION)]         = this.OrderCommission();
   this.m_double_prop[this.IndexProp(ORDER_PROP_SWAP)]               = this.OrderSwap();
   this.m_double_prop[this.IndexProp(ORDER_PROP_VOLUME)]             = this.OrderVolume();
   this.m_double_prop[this.IndexProp(ORDER_PROP_SL)]                 = this.OrderStopLoss();
   this.m_double_prop[this.IndexProp(ORDER_PROP_TP)]                 = this.OrderTakeProfit();
   this.m_double_prop[this.IndexProp(ORDER_PROP_VOLUME_CURRENT)]     = this.OrderVolumeCurrent();
   this.m_double_prop[this.IndexProp(ORDER_PROP_PRICE_STOP_LIMIT)]   = this.OrderPriceStopLimit();
   
//--- Сохранение строковых свойств
   this.m_string_prop[this.IndexProp(ORDER_PROP_SYMBOL)]             = this.OrderSymbol();
   this.m_string_prop[this.IndexProp(ORDER_PROP_COMMENT)]            = this.OrderComment();
   this.m_string_prop[this.IndexProp(ORDER_PROP_EXT_ID)]             = this.OrderExternalID();
   
//--- Сохранение дополнительных целочисленных свойств
   this.m_long_prop[ORDER_PROP_PROFIT_PT]                            = this.ProfitInPoints();
   this.m_long_prop[ORDER_PROP_TICKET_FROM]                          = this.OrderTicketFrom();
   this.m_long_prop[ORDER_PROP_TICKET_TO]                            = this.OrderTicketTo();
   this.m_long_prop[ORDER_PROP_CLOSE_BY_SL]                          = this.OrderCloseByStopLoss();
   this.m_long_prop[ORDER_PROP_CLOSE_BY_TP]                          = this.OrderCloseByTakeProfit();
   this.m_long_prop[ORDER_PROP_GROUP_ID]                             = 0;
   
//--- Сохранение дополнительных вещественных свойств
   this.m_double_prop[this.IndexProp(ORDER_PROP_PROFIT_FULL)]        = this.ProfitFull();
  }
//+------------------------------------------------------------------+

Это делает метод OrderPositionID(), переходим к нему и видим, что для MQL4 в идентификатор сразу записывается тикет:

//+------------------------------------------------------------------+
//| Возвращает идентификатор позиции                                 |
//+------------------------------------------------------------------+
long COrder::OrderPositionID(void) const
  {
#ifdef __MQL4__
   return ::OrderTicket();
#else
   long res=0;
   switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS))
     {
      case ORDER_STATUS_MARKET_POSITION   : res=::PositionGetInteger(POSITION_IDENTIFIER);            break;
      case ORDER_STATUS_MARKET_ORDER      :
      case ORDER_STATUS_MARKET_PENDING    : res=::OrderGetInteger(ORDER_POSITION_ID);                 break;
      case ORDER_STATUS_HISTORY_PENDING   :
      case ORDER_STATUS_HISTORY_ORDER     : res=::HistoryOrderGetInteger(m_ticket,ORDER_POSITION_ID); break;
      case ORDER_STATUS_DEAL              : res=::HistoryDealGetInteger(m_ticket,DEAL_POSITION_ID);   break;
      default                             : res=0;                                                    break;
     }
   return res;
#endif
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Возвращает идентификатор позиции                                 |
//+------------------------------------------------------------------+
long COrder::OrderPositionID(void) const
  {
#ifdef __MQL4__
   return 0;
#else
   long res=0;
   switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS))
     {
      case ORDER_STATUS_MARKET_POSITION   : res=::PositionGetInteger(POSITION_IDENTIFIER);            break;
      case ORDER_STATUS_MARKET_ORDER      :
      case ORDER_STATUS_MARKET_PENDING    : res=::OrderGetInteger(ORDER_POSITION_ID);                 break;
      case ORDER_STATUS_HISTORY_PENDING   :
      case ORDER_STATUS_HISTORY_ORDER     : res=::HistoryOrderGetInteger(m_ticket,ORDER_POSITION_ID); break;
      case ORDER_STATUS_DEAL              : res=::HistoryDealGetInteger(m_ticket,DEAL_POSITION_ID);   break;
      default                             : res=0;                                                    break;
     }
   return res;
#endif
  }
//+------------------------------------------------------------------+

Скомпилируем советник, запустим в тестере и установим, а затем удалим отложенный ордер:


Теперь событие удаления отложенного ордера отслеживается.

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

Вспоминаем, что всё начинается с обработчика OnTimer() класса CEngine:

//+------------------------------------------------------------------+
//| CEngine таймер                                                   |
//+------------------------------------------------------------------+
void CEngine::OnTimer(void)
  {
//--- Таймер коллекций исторических ордеров и сделок и рыночных ордеров и позиций
   int index=this.CounterIndex(COLLECTION_COUNTER_ID);
   if(index>WRONG_VALUE)
     {
      CTimerCounter* counter=this.m_list_counters.At(index);
      if(counter!=NULL)
        {
         //--- Если это не тестер
         if(!this.IsTester())
           {
            //--- Если пауза завершилась - работаем с событиями коллекций
            if(counter.IsTimeDone())
               this.TradeEventsControl();
           }
         //--- Если тестер - работаем с событиями коллекций по тику
         else 
           {
            this.TradeEventsControl();
           }
        }
     }
  }
//+------------------------------------------------------------------+

По коду видим, что контроль событий осуществляется в методе TradeEventsControl(), переходим к нему и видим, что при наличии любого события мы вызываем метод обновления событий класса-коллекции событий CEventsCollection::Refresh():

//+------------------------------------------------------------------+
//| Проверка торговых событий                                        |
//+------------------------------------------------------------------+
void CEngine::TradeEventsControl(void)
  {
//--- Инициализация кода и флагов торговых событий
   this.m_is_market_trade_event=false;
   this.m_is_history_trade_event=false;
//--- Обновление списков 
   this.m_market.Refresh();
   this.m_history.Refresh();
//--- Действия при первом запуске
   if(this.IsFirstStart())
     {
      this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;
      return;
     }
//--- Проверка изменений в рыночном состоянии и в истории счёта 
   this.m_is_market_trade_event=this.m_market.IsTradeEvent();
   this.m_is_history_trade_event=this.m_history.IsTradeEvent();

//--- Если есть любое событие, отправляем списки, флаги и количество новых ордеров и сделок в коллекцию событий и обновляем коллекцию событий
   int change_total=0;
   CArrayObj* list_changes=this.m_market.GetListChanges();
   if(list_changes!=NULL)
      change_total=list_changes.Total();
   if(this.m_is_history_trade_event || this.m_is_market_trade_event || change_total>0)
     {
      this.m_events.Refresh(this.m_history.GetList(),this.m_market.GetList(),list_changes,
                            this.m_is_history_trade_event,this.m_is_market_trade_event,
                            this.m_history.NewOrders(),this.m_market.NewPendingOrders(),
                            this.m_market.NewMarketOrders(),this.m_history.NewDeals());
      //--- Получаем последнее торговое событие на счёте
      this.m_acc_trade_event=this.m_events.GetLastTradeEvent();
     }
  }
//+------------------------------------------------------------------+

Здесь мы отправляем в метод списки исторической и рыночной коллекций, флаги изменения в этих коллекциях, количество новых исторических ордеров и новых активных рыночных ордеров и позиций, а также количество новых сделок. Но если внимательно поглядеть, то мы увидим, что вместо количества новых рыночных позиций, в метод передаётся количество новых маркет-ордеров, которые мы нигде в библиотеке ещё не используем. Это моя ошибка — ведь создавалось всё изначально для MQL5, а количество новых позиций нужно отправлять в метод для MQL4, ведь в MQL5 новые позиции мы определяем по количеству сделок. Ну и вышло, что я ошибся, "на глазок" заполняя передаваемые данные в метод для MQL4. Теперь понятно почему метод не видит новых рыночных позиций.
Исправим. А заодно решим вот ещё какую проблему:
В MQL4, к сожалению, нет штатной возможности узнать от какого ордера была порождена позиция, как это легко можно сделать в MQL5. Но. У нас уже готов и работает список контрольных ордеров для отслеживания изменений свойств ордеров и позиций — их модификации. И этот список мы ещё нигде не очищали от ненужных данных. Вот именно он нам и поможет отслеживать ордер, на основании которого была открыта позиция, а заодно и идентифицировать событие — либо это открытие маркет-ордером, либо это срабатывание отложенного ордера.

В коллекцию рыночных ордеров и позиций (класс CMarketCollection в файле MarketCollection.mqh)
добавим публичный метод, возвращающий список контрольных ордеров:

public:
//--- Возвращает список (1) всех отложенных ордеров и открытых позиций, (2) контрольных ордеров и позиций
   CArrayObj*        GetList(void)                                                                       { return &this.m_list_all_orders;                                       }
   CArrayObj*        GetListChanges(void)                                                                { return &this.m_list_changed;                                          }
   CArrayObj*        GetListControl(void)                                                                { return &this.m_list_control;                                          }
//--- Возвращает список ордеров и позиций со временем открытия в диапазоне от begin_time до end_time
   CArrayObj*        GetListByTime(const datetime begin_time=0,const datetime end_time=0);
//--- Возвращает список ордеров и позиций по выбранному (1) double, (2) integer и (3) string свойству, удовлетворяющему сравниваемому условию
   CArrayObj*        GetList(ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode);  }
   CArrayObj*        GetList(ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL)  { return CSelect::ByOrderProperty(this.GetList(),property,value,mode);  }
   CArrayObj*        GetList(ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode);  }
//--- Возвращает количество (1) новых маркет-ордеров, (2) новых отложенных ордеров, (3) новых позиций, (4) флаг произошедшего торгового события (5) величину изменённого объёма
   int               NewMarketOrders(void)                                                         const { return this.m_new_market_orders;                                      }
   int               NewPendingOrders(void)                                                        const { return this.m_new_pendings;                                           }
   int               NewPositions(void)                                                            const { return this.m_new_positions;                                          }
   bool              IsTradeEvent(void)                                                            const { return this.m_is_trade_event;                                         }
   double            ChangedVolumeValue(void)                                                      const { return this.m_change_volume_value;                                    }
//--- Конструктор
                     CMarketCollection(void);
//--- Обновляет список отложенных ордеров и позиций
   void              Refresh(void);
  };
//+------------------------------------------------------------------+
Чтобы использовать данные из этого списка, нам нужно его передать в метод Refresh() класса CEventsCollection.

Для этого допишем все необходимые и описанные выше изменения:

//+------------------------------------------------------------------+
//| Проверка торговых событий                                        |
//+------------------------------------------------------------------+
void CEngine::TradeEventsControl(void)
  {
//--- Инициализация кода и флагов торговых событий
   this.m_is_market_trade_event=false;
   this.m_is_history_trade_event=false;
//--- Обновление списков 
   this.m_market.Refresh();
   this.m_history.Refresh();
//--- Действия при первом запуске
   if(this.IsFirstStart())
     {
      this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;
      return;
     }
//--- Проверка изменений в рыночном состоянии и в истории счёта 
   this.m_is_market_trade_event=this.m_market.IsTradeEvent();
   this.m_is_history_trade_event=this.m_history.IsTradeEvent();

//--- Если есть любое событие, отправляем списки, флаги и количество новых ордеров и сделок в коллекцию событий и обновляем коллекцию событий
   int change_total=0;
   CArrayObj* list_changes=this.m_market.GetListChanges();
   if(list_changes!=NULL)
      change_total=list_changes.Total();
   if(this.m_is_history_trade_event || this.m_is_market_trade_event || change_total>0)
     {
      this.m_events.Refresh(this.m_history.GetList(),this.m_market.GetList(),list_changes,this.m_market.GetListControl(),
                            this.m_is_history_trade_event,this.m_is_market_trade_event,                                  
                            this.m_history.NewOrders(),this.m_market.NewPendingOrders(),                                 
                            this.m_market.NewPositions(),this.m_history.NewDeals());                                     
      //--- Получаем последнее торговое событие на счёте
      this.m_acc_trade_event=this.m_events.GetLastTradeEvent();
     }
  }
//+------------------------------------------------------------------+

Здесь: в методе TradeEventsControl() класса CEngine добавили передачу ещё одного списка — списка контрольных ордеров в метод Refresh() класса CEventsCollection, и исправили ошибочную передачу в этот метод количества новых маркет-ордеров на передачу количества новых позиций.

Внесём исправления в определение метода Refresh() в теле класса CEventsCollection:

public:
//--- Выбирает события из коллекции со временем в диапазоне от begin_time до end_time
   CArrayObj        *GetListByTime(const datetime begin_time=0,const datetime end_time=0);
//--- Возвращает полный список-коллекцию событий "как есть"
   CArrayObj        *GetList(void)                                                                       { return &this.m_list_events;                                           }
//--- Возвращает список по выбранному (1) целочисленному, (2) вещественному и (3) строковому свойству, удовлетворяющему сравниваемому критерию
   CArrayObj        *GetList(ENUM_EVENT_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL)  { return CSelect::ByEventProperty(this.GetList(),property,value,mode);  }
   CArrayObj        *GetList(ENUM_EVENT_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByEventProperty(this.GetList(),property,value,mode);  }
   CArrayObj        *GetList(ENUM_EVENT_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByEventProperty(this.GetList(),property,value,mode);  }
//--- Обновляет список событий
   void              Refresh(CArrayObj* list_history,
                             CArrayObj* list_market,
                             CArrayObj* list_changes,
                             CArrayObj* list_control,
                             const bool is_history_event,
                             const bool is_market_event,
                             const int  new_history_orders,
                             const int  new_market_pendings,
                             const int  new_market_positions,
                             const int  new_deals);
//--- Устанавливает идентификатор графика управляющей программы
   void              SetChartID(const long id)        { this.m_chart_id=id;         }
//--- Возвращает последнее торговое событие на счёте
   ENUM_TRADE_EVENT  GetLastTradeEvent(void)    const { return this.m_trade_event;  }
//--- Сбрасывает последнее торговое событие
   void              ResetLastTradeEvent(void)        { this.m_trade_event=TRADE_EVENT_NO_EVENT;   }
//--- Конструктор
                     CEventsCollection(void);
  };
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Обновляет список событий                                         |
//+------------------------------------------------------------------+
void CEventsCollection::Refresh(CArrayObj* list_history,
                                CArrayObj* list_market,
                                CArrayObj* list_changes,
                                CArrayObj* list_control,
                                const bool is_history_event,
                                const bool is_market_event,
                                const int  new_history_orders,
                                const int  new_market_pendings,
                                const int  new_market_positions,
                                const int  new_deals)
  {

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

//+------------------------------------------------------------------+
//| Коллекция событий счёта                                          |
//+------------------------------------------------------------------+
class CEventsCollection : public CListObj
  {
private:
   CListObj          m_list_events;                   // Список событий
   bool              m_is_hedge;                      // Флаг хедж-счёта
   long              m_chart_id;                      // Идентификатор графика управляющей программы
   int               m_trade_event_code;              // Код торгового события
   ENUM_TRADE_EVENT  m_trade_event;                   // Торговое событие на счёте
   CEvent            m_event_instance;                // Объект-событие для поиска по свойству
   MqlTick           m_tick;                          // Структура последнего тика
   ulong             m_position_id;                   // Идентификатор позиции (MQL4)  
   ENUM_ORDER_TYPE   m_type_first;                    // Тип открывающего ордера (MQL4)
   
//--- Создаёт торговое событие в зависимости от (1) статуса и (2) типа изменения ордера
   void              CreateNewEvent(COrder* order,CArrayObj* list_history,CArrayObj* list_market);
   void              CreateNewEvent(COrderControl* order);
//--- Создаёт событие для (1) хеджевого счёта, (2) неттингового счёта
   void              NewDealEventHedge(COrder* deal,CArrayObj* list_history,CArrayObj* list_market);
   void              NewDealEventNetto(COrder* deal,CArrayObj* list_history,CArrayObj* list_market);
//--- Выбирает из списка и возвращает список (1) рыночных отложенных ордеров, (2) открытых позиций
   CArrayObj*        GetListMarketPendings(CArrayObj* list);
   CArrayObj*        GetListPositions(CArrayObj* list);
//--- Выбирает из списка и возвращает список исторических (1) удалённых отложенных ордеров, (2) сделок, (3) всех закрывающих ордеров 
   CArrayObj*        GetListHistoryPendings(CArrayObj* list);
   CArrayObj*        GetListDeals(CArrayObj* list);
   CArrayObj*        GetListCloseByOrders(CArrayObj* list);
//--- Возвращает список (1) всех ордеров позиции по её идентификатору, (2) всех сделок позиции по её идентификатору
//--- (3) всех сделок на вход в рынок по идентификатору позиции, (4) всех сделок на выход из рынка по идентификатору позиции,
//--- (5) всех сделок на разворот позиции по идентификатору позиции
   CArrayObj*        GetListAllOrdersByPosID(CArrayObj* list,const ulong position_id);
   CArrayObj*        GetListAllDealsByPosID(CArrayObj* list,const ulong position_id);
   CArrayObj*        GetListAllDealsInByPosID(CArrayObj* list,const ulong position_id);
   CArrayObj*        GetListAllDealsOutByPosID(CArrayObj* list,const ulong position_id);
   CArrayObj*        GetListAllDealsInOutByPosID(CArrayObj* list,const ulong position_id);
//--- Возвращает суммарный объём всех сделок (1) IN, (2) OUT позиции по её идентификатору
   double            SummaryVolumeDealsInByPosID(CArrayObj* list,const ulong position_id);
   double            SummaryVolumeDealsOutByPosID(CArrayObj* list,const ulong position_id);
//--- Возвращает (1) первый, (2) последний и (3) закрывающий ордер из списка всех ордеров позиции,
//--- (4) ордер по тикету, (5) рыночную позицию по идентификатору,
//--- (6) последнюю и (7) предпоследнюю сделку InOut по идентификатору позиции
   COrder*           GetFirstOrderFromList(CArrayObj* list,const ulong position_id);
   COrder*           GetLastOrderFromList(CArrayObj* list,const ulong position_id);
   COrder*           GetCloseByOrderFromList(CArrayObj* list,const ulong position_id);
   COrder*           GetHistoryOrderByTicket(CArrayObj* list,const ulong order_ticket);
   COrder*           GetPositionByID(CArrayObj* list,const ulong position_id);
//--- Возвращает тип открывающего ордера по тикету позиции (MQL4)
   ENUM_ORDER_TYPE   GetTypeFirst(CArrayObj* list,const ulong ticket);
//--- Возвращает флаг наличия объекта-события в списке событий
   bool              IsPresentEventInList(CEvent* compared_event);
//--- Обработчик события изменения существующего ордера/позиции
   void              OnChangeEvent(CArrayObj* list_changes,const int index);

public:

Напишем за пределами тела класса реализацию метода получения списка открытых позиций:

//+------------------------------------------------------------------+
//| Выбирает из списка только рыночные позиции                       |
//+------------------------------------------------------------------+
CArrayObj* CEventsCollection::GetListPositions(CArrayObj *list)
  {
   if(list.Type()!=COLLECTION_MARKET_ID)
     {
      Print(DFUN,TextByLanguage("Ошибка. Список не является списком рыночной коллекции","Error. The list is not a list of the market collection"));
      return NULL;
     }
   CArrayObj* list_positions=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_POSITION,EQUAL);
   return list_positions;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает тип открывающего ордера по тикету позиции (MQL4)      |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE CEventsCollection::GetTypeFirst(CArrayObj* list,const ulong ticket)
  {
   if(list==NULL)
      return WRONG_VALUE;
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      COrderControl* ctrl=list.At(i);
      if(ctrl==NULL)
         continue;
      if(ctrl.Ticket()==ticket)                   
         return (ENUM_ORDER_TYPE)ctrl.TypeOrder();
     }
   return WRONG_VALUE;
  }
//+------------------------------------------------------------------+

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

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

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

//+------------------------------------------------------------------+
//| Обновляет список событий                                         |
//+------------------------------------------------------------------+
void CEventsCollection::Refresh(CArrayObj* list_history,
                                CArrayObj* list_market,
                                CArrayObj* list_changes,
                                CArrayObj* list_control,
                                const bool is_history_event,
                                const bool is_market_event,
                                const int  new_history_orders,
                                const int  new_market_pendings,
                                const int  new_market_positions,
                                const int  new_deals)
  {
//--- Если списки пустые - выход
   if(list_history==NULL || list_market==NULL)
      return;
//--- Если событие в рыночном окружении
   if(is_market_event)
     {
      //--- если было изменение свойств ордера
      int total_changes=list_changes.Total();
      if(total_changes>0)
        {
         for(int i=total_changes-1;i>=0;i--)
           {
            this.OnChangeEvent(list_changes,i);
           }
        }
      //--- если увеличилось количество установленных отложенных ордеров
      if(new_market_pendings>0)
        {
         //--- Получаем список только установленных отложенных ордеров
         CArrayObj* list=this.GetListMarketPendings(list_market);
         if(list!=NULL)
           {
            //--- Сортируем новый список по времени установки ордера
            list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
            //--- Берём в цикле с конца списка количество ордеров, равное количеству новых установленных ордеров (последние N событий)
            int total=list.Total(), n=new_market_pendings;
            for(int i=total-1; i>=0 && n>0; i--,n--)
              {
               //--- Получаем ордер из списка, и если это отложенный ордер - устанавливаем торговое событие
               COrder* order=list.At(i);
               if(order!=NULL && order.Status()==ORDER_STATUS_MARKET_PENDING)
                  this.CreateNewEvent(order,list_history,list_market);
              }
           }
        }
      #ifdef __MQL4__
      //--- Если увеличилось количество позиций
      if(new_market_positions>0)
        {
         //--- Получаем список открытых позиций
         CArrayObj* list=this.GetListPositions(list_market);
         if(list!=NULL)
           {
            //--- Сортируем новый список по времени открытия позиции
            list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
            //--- Берём в цикле с конца списка количество позиций, равное количеству новых открытых позиций (последние N событий)
            int total=list.Total(), n=new_market_positions;
            for(int i=total-1; i>=0 && n>0; i--,n--)
              {
               //--- Получаем позицию из списка, и если это позиция - ищем данные открывающего ордера и устанавливаем торговое событие
               COrder* position=list.At(i);
               if(position!=NULL && position.Status()==ORDER_STATUS_MARKET_POSITION)
                 {
                  //--- Находим ордер и устанавливаем (1) тип ордера, на основании которого была открыта позиция и (2) идентификатор позиции 
                  this.m_type_first=this.GetTypeFirst(list_control,position.Ticket());
                  this.m_position_id=position.Ticket();
                  this.CreateNewEvent(position,list_history,list_market);
                 }
              }
           }
        }
      #endif 
     }
//--- Если событие в истории счёта
   if(is_history_event)
     {
      //--- Если увеличилось количество исторических ордеров
      if(new_history_orders>0)
        {
         //--- Получаем список только удалённых отложенных ордеров
         CArrayObj* list=this.GetListHistoryPendings(list_history);
         if(list!=NULL)
           {
            //--- Сортируем новый список по времени удаления ордера
            list.Sort(SORT_BY_ORDER_TIME_CLOSE_MSC);
            //--- Берём в цикле с конца списка количество ордеров, равное количеству новых удалённых отложенных ордеров (последние N событий)
            int total=list.Total(), n=new_history_orders;
            for(int i=total-1; i>=0 && n>0; i--,n--)
              {
               //--- Получаем ордер из списка, и если это удалённый отложенный ордер и у него нет идентификатора позиции, 
               //--- то это удаление ордера - устанавливаем торговое событие
               COrder* order=list.At(i);
               if(order!=NULL && order.Status()==ORDER_STATUS_HISTORY_PENDING && order.PositionID()==0)
                  this.CreateNewEvent(order,list_history,list_market);
              }
           }
        }
      //--- Если увеличилось количество сделок
      if(new_deals>0)
        {
         //--- Получаем список только сделок
         CArrayObj* list=this.GetListDeals(list_history);
         if(list!=NULL)
           {
            //--- Сортируем новый список по времени совершения сделки
            list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
            //--- Берём в цикле с конца списка количество сделок, равное количеству новых сделок (последние N событий)
            int total=list.Total(), n=new_deals;
            for(int i=total-1; i>=0 && n>0; i--,n--)
              {
               //--- Получаем сделку из списка и устанавливаем торговое событие
               COrder* order=list.At(i);
               if(order!=NULL)
                  this.CreateNewEvent(order,list_history,list_market);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

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

//--- Открыта позиция (__MQL4__)
   if(status==ORDER_STATUS_MARKET_POSITION)
     {
      //--- Установим код торгового события "позиция открыта"
      this.m_trade_event_code=TRADE_EVENT_FLAG_POSITION_OPENED;
      //--- Установим причину "запрос исполнен полностью""
      ENUM_EVENT_REASON reason=EVENT_REASON_DONE;
      //--- Если открывающий ордер - отложенный
      if(this.m_type_first>ORDER_TYPE_SELL && this.m_type_first<ORDER_TYPE_BALANCE)
        {
         //--- установим причину "срабатывание отложенного ордера"
         reason=EVENT_REASON_ACTIVATED_PENDING;
         //--- добавим к коду события флаг активации отложенного ордера
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_ACTIVATED;
        }
      CEvent* event=new CEventPositionOpen(this.m_trade_event_code,order.Ticket());
      if(event!=NULL)
        {
         event.SetProperty(EVENT_PROP_TIME_EVENT,order.TimeOpenMSC());                             // Время события
         event.SetProperty(EVENT_PROP_REASON_EVENT,reason);                                        // Причина события (из перечисления ENUM_EVENT_REASON)
         event.SetProperty(EVENT_PROP_TYPE_DEAL_EVENT,this.m_type_first);                          // Тип сделки события
         event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket());                           // Тикет сделки события
         event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,this.m_type_first);                         // Тип ордера, на основании которого открыта сделка события (последний ордер позиции)
         event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,this.m_type_first);                      // Тип ордера, на основании которого открыта сделка позиции (первый ордер позиции)
         event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order.Ticket());                          // Тикет ордера, на основании которого открыта сделка события (последний ордер позиции)
         event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order.Ticket());                       // Тикет ордера, на основании которого открыта сделка позиции (первый ордер позиции)
         event.SetProperty(EVENT_PROP_POSITION_ID,this.m_position_id);                             // Идентификатор позиции
         event.SetProperty(EVENT_PROP_POSITION_BY_ID,order.PositionByID());                        // Идентификатор встречной позиции
         event.SetProperty(EVENT_PROP_MAGIC_BY_ID,0);                                              // Магический номер встречной позиции
            
         event.SetProperty(EVENT_PROP_TYPE_ORD_POS_BEFORE,order.TypeOrder());                      // Тип ордера позиции до смены направления
         event.SetProperty(EVENT_PROP_TICKET_ORD_POS_BEFORE,order.Ticket());                       // Тикет ордера позиции до смены направления
         event.SetProperty(EVENT_PROP_TYPE_ORD_POS_CURRENT,order.TypeOrder());                     // Тип ордера текущей позиции
         event.SetProperty(EVENT_PROP_TICKET_ORD_POS_CURRENT,order.Ticket());                      // Тикет ордера текущей позиции
      
         event.SetProperty(EVENT_PROP_PRICE_OPEN_BEFORE,order.PriceOpen());                        // Цена установки ордера до модификации
         event.SetProperty(EVENT_PROP_PRICE_SL_BEFORE,order.StopLoss());                           // Цена StopLoss до модификации
         event.SetProperty(EVENT_PROP_PRICE_TP_BEFORE,order.TakeProfit());                         // Цена TakeProfit до модификации
         event.SetProperty(EVENT_PROP_PRICE_EVENT_ASK,this.m_tick.ask);                            // Цена Ask в момент события
         event.SetProperty(EVENT_PROP_PRICE_EVENT_BID,this.m_tick.bid);                            // Цена Bid в момент события
         
         event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic());                                  // Магический номер ордера/сделки/позиции
         event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order.TimeOpenMSC());                    // Время ордера, на основании которого открыта сделка позиции (первый ордер позиции)
         event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PriceOpen());                              // Цена, на которой произошло событие
         event.SetProperty(EVENT_PROP_PRICE_OPEN,order.PriceOpen());                               // Цена открытия ордера/сделки/позиции
         event.SetProperty(EVENT_PROP_PRICE_CLOSE,order.PriceClose());                             // Цена закрытия ордера/сделки/позиции
         event.SetProperty(EVENT_PROP_PRICE_SL,order.StopLoss());                                  // Цена StopLoss позиции
         event.SetProperty(EVENT_PROP_PRICE_TP,order.TakeProfit());                                // Цена TakeProfit позиции
         event.SetProperty(EVENT_PROP_VOLUME_ORDER_INITIAL,order.Volume());                        // Запрашиваемый объём ордера
         event.SetProperty(EVENT_PROP_VOLUME_ORDER_EXECUTED,order.Volume()-order.VolumeCurrent()); // Исполненный объём ордера
         event.SetProperty(EVENT_PROP_VOLUME_ORDER_CURRENT,order.VolumeCurrent());                 // Оставшийся (неисполненный) объём ордера
         event.SetProperty(EVENT_PROP_VOLUME_POSITION_EXECUTED,order.Volume());                    // Исполненный объём позиции
         event.SetProperty(EVENT_PROP_PROFIT,order.Profit());                                      // Профит
         event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol());                                      // Символ ордера
         event.SetProperty(EVENT_PROP_SYMBOL_BY_ID,order.Symbol());                                // Символ встречной позиции
         //--- Установка идентификатора графика управляющей программы и расшифровка кода события и установка типа события
         event.SetChartID(this.m_chart_id);
         event.SetTypeEvent();
         //--- Если объекта-события нет в списке - добавляем
         if(!this.IsPresentEventInList(event))
           {
            this.m_list_events.InsertSort(event);
            //--- Отправляем сообщение о событии и устанавливаем значение последнего торгового события
            event.SendEvent();
            this.m_trade_event=event.TradeEvent();
           }
         //--- Если это событие уже есть в списке - удаляем новый объект-событие и выводим отладочное сообщение
         else
           {
            ::Print(DFUN_ERR_LINE,TextByLanguage("Такое событие уже есть в списке","This event is already in the list."));
            delete event;
           }
        }
     }
//--- Новая сделка (__MQL5__)

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


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

Проверим внесённые изменения. Скомпилируем советник TestDoEasyPart10.mq4, запустим его в тестере, откроем-закроем позиции, установим отложенные ордера и дождёмся активации одного из них, а также проверим выставление стоп-уровней и работу тралов (модификация позиций и отложенных ордеров). Все события, которые библиотека уже "видит" для MQL4, будут отображены в журнале тестера:


Если внимательно понаблюдать за журналом тестера, то видно, что закрытие позиций библиотека ещё не видит. Но при срабатывании отложенного ордера BuyLimit #3 в журнале видим запись о том, что активирован отложенный ордер [BuyLimit #3], породив позицию Buy #3 — события активации отложенных ордеров теперь библиотека видит, и к тому же знает от какого ордера произошла позиция. Так же можно заметить незначительное упущение в функции модификации — цвет значка модифицированного трейлингом отложенного ордера BuyStop #1 меняется на красный. Но все события модификации ордеров и позиций библиотека видит.

Добавим исправления в торговые MQL4-функции для тестера в файле DELib.mqh. Просто сделаем ещё одну функцию, которая будет возвращать тип позиции Buy или Sell в зависимости от типа отложенного ордера, переданного в неё, и заменим в строках выбора цвета стрелки проверку типа ордера на проверку типа ордера по направлению:

//+------------------------------------------------------------------+
//| Модификация отложенного ордера по тикету                         |
//+------------------------------------------------------------------+
bool PendingOrderModify(const ulong ticket,const double price_set,const double sl,const double tp)
  {
   ResetLastError();
   if(!OrderSelect((int)ticket,SELECT_BY_TICKET))
     {
      Print(DFUN,TextByLanguage("Не удалось выбрать ордер. Ошибка ","Could not select order. Error "),(string)GetLastError());
      return false;
     }
   ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderType();
   if(type<ORDER_TYPE_BUY_LIMIT || type>ORDER_TYPE_SELL_STOP)
     {
      Print(DFUN,TextByLanguage("Ошибка. Не ордер: ","Error. Not order: "),PositionTypeDescription((ENUM_POSITION_TYPE)type)," #",ticket);
      return false;
     }
   if(OrderCloseTime()>0)
     {
      Print(DFUN,TextByLanguage("Ошибка. Для модификации выбран удалённый ордер: ","Error. Deleted order selected for modification: "),OrderTypeDescription(type)," #",ticket);
      return false;
     }
   color clr=(TypeByPendingDirection(type)==ORDER_TYPE_BUY ? clrBlue : clrRed);
   ResetLastError();
   if(!OrderModify((int)ticket,price_set,sl,tp,0,clr))
     {
      Print(DFUN,TextByLanguage("Не удалось модифицировать ордер. Ошибка ","Failed to order modify. Error "),(string)GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Возвращает тип по направлению отложенного ордера                 |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE TypeByPendingDirection(const ENUM_ORDER_TYPE type)
  {
   if(type==ORDER_TYPE_BUY_LIMIT  || type==ORDER_TYPE_BUY_STOP)  return ORDER_TYPE_BUY;
   if(type==ORDER_TYPE_SELL_LIMIT || type==ORDER_TYPE_SELL_STOP) return ORDER_TYPE_SELL;
   return WRONG_VALUE;
  }
//+------------------------------------------------------------------+

Что дальше

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

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

К содержанию

Статьи этой серии:

Часть 1. Концепция, организация данных.
Часть 2. Коллекция исторических ордеров и сделок.
Часть 3. Коллекция рыночных ордеров и позиций, организация поиска.
Часть 4. Торговые события. Концепция.
Часть 5. Классы и коллекция торговых событий. Отправка событий в программу.
Часть 6. События на счёте с типом неттинг.
Часть 7. События срабатывания StopLimit-ордеров, подготовка функционала для регистрации событий модификации ордеров и позиций.
Часть 8. События модификации ордеров и позиций.
Часть 9. Совместимость с MQL4 - Подготовка данных.