Библиотека для простого и быстрого создания программ для MetaTrader (Часть III): Коллекция рыночных ордеров и позиций, поиск и фильтрация

Artyom Trishkin | 13 марта, 2019

Содержание

Организация поиска
Базовый объект Engine — основа библиотеки
Объекты активных рыночных ордеров и позиций
Коллекция активных рыночных ордеров и позиций
Что дальше

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


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

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


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

Организация поиска

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

Итак, в папке библиотеки Collections создайте новый класс CSelect. Базовый класс задавать не нужно. После завершения работы Мастера MQL в папке Collections будет создан новый файл Select.mqh:

//+------------------------------------------------------------------+
//|                                                       Select.mqh |
//|                        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"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CSelect
  {
private:

public:
                     CSelect();
                    ~CSelect();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CSelect::CSelect()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CSelect::~CSelect()
  {
  }
//+------------------------------------------------------------------+

Для осуществления поиска требуется задать все его режимы. Для этого в файле Defines.mqh создадим перечисление, описывающее режимы сравнения объектов во время осуществления поиска:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        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"
//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define COUNTRY_LANG   ("Russian")              // Язык страны
#define DFUN           (__FUNCTION__+": ")      // "Описание функции"
#define END_TIME       (D'31.12.3000 23:59:59') // Конечная дата для запросов данных истории счёта
//+------------------------------------------------------------------+
//| Поиск                                                            |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Данные для поиска                                                |
//+------------------------------------------------------------------+
enum ENUM_COMPARER_TYPE
  {
   EQUAL,                                                   // Равно
   MORE,                                                    // Больше
   LESS,                                                    // Меньше
   NO_EQUAL,                                                // Не равно
   EQUAL_OR_MORE,                                           // Больше или равно
   EQUAL_OR_LESS                                            // Меньше или равно
  };
//+------------------------------------------------------------------+

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

Для проведения сравнения необходимо создать такой метод: объявим шаблонный статический метод сравнения двух величин.
//+------------------------------------------------------------------+
//|                                                       Select.mqh |
//|                        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"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Arrays\ArrayObj.mqh>
#include "..\Objects\Order.mqh"
//+------------------------------------------------------------------+
//| Список-хранилище                                                 |
//+------------------------------------------------------------------+
CArrayObj   ListStorage; // Объект-хранилище для хранения сортированных списков коллекций
//+------------------------------------------------------------------+
//| Класс для выборки объектов, удовлетворяющих критерию             |
//+------------------------------------------------------------------+
class CSelect
  {
private:
   //--- Метод сравнения двух величин
   template<typename T>
   static bool       CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode);
public:

  };
//+------------------------------------------------------------------+

Реализация метода сравнения:

//+------------------------------------------------------------------+
//| Метод сравнения двух величин                                     |
//+------------------------------------------------------------------+
template<typename T>
bool CSelect::CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode)
  {
   return
     (
      mode==EQUAL && value1==value2          ?  true  :
      mode==NO_EQUAL && value1!=value2       ?  true  :
      mode==MORE && value1>value2            ?  true  :
      mode==LESS && value1<value2            ?  true  :
      mode==EQUAL_OR_MORE && value1>=value2  ?  true  :
      mode==EQUAL_OR_LESS && value1<=value2  ?  true  :  false
     );
  }
//+------------------------------------------------------------------+

В метод передаются два значения одного и того же типа и режим, по которому необходимо провести сравнение.
А далее простое сравнение в зависимости от метода сравнения на (равно/не равно/больше/меньше/больше или равно/меньше или равно), и возврат результата.

Теперь создадим несколько методов для осуществления поиска по списку. В публичной секции класса CSelect объявим три статических метода для поиска ордера по заданному критерию:

//+------------------------------------------------------------------+
//| Класс для выборки объектов, удовлетворяющих критерию             |
//+------------------------------------------------------------------+
class CSelect
  {
private:
   //--- Метод сравнения двух величин
   template<typename T>
   static bool       CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode);
public:
   //--- Возвращает список ордеров, у которых одно из (1) целочисленных, (2) вещественных и (3) строковых свойств удовлетворяет заданному критерию
   static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode);
   static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode);
   static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode);
  };
//+------------------------------------------------------------------+

И сразу за пределами тела класса напишем их реализацию:

//+------------------------------------------------------------------+
//| Возвращает список ордеров, у которых одно из целочисленных       |
//| свойств удовлетворяет заданному критерию                         |
//+------------------------------------------------------------------+
CArrayObj *CSelect::ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode)
  {
   if(list_source==NULL) return NULL;
   CArrayObj *list=new CArrayObj();
   if(list==NULL) return NULL;
   list.FreeMode(false);
   ListStorage.Add(list);
   int total=list_source.Total();
   for(int i=0; i<total; i++)
     {
      COrder *order=list_source.At(i);
      if(!order.SupportProperty(property)) continue;
      long order_prop=order.GetProperty(property);
      if(CompareValues(order_prop,value,mode)) list.Add(order);
     }
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает список ордеров, у которых одно из вещественных        |
//| свойств удовлетворяет заданному критерию                         |
//+------------------------------------------------------------------+
CArrayObj *CSelect::ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode)
  {
   if(list_source==NULL) return NULL;
   CArrayObj *list=new CArrayObj();
   if(list==NULL) return NULL;
   list.FreeMode(false);
   ListStorage.Add(list);
   for(int i=0; i<list_source.Total(); i++)
     {
      COrder *order=list_source.At(i);
      if(!order.SupportProperty(property)) continue;
      double order_prop=order.GetProperty(property);
      if(CompareValues(order_prop,value,mode)) list.Add(order);
     }
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает список ордеров, у которых одно из строковых           |
//| свойств удовлетворяет заданному критерию                         |
//+------------------------------------------------------------------+
CArrayObj *CSelect::ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode)
  {
   if(list_source==NULL) return NULL;
   CArrayObj *list=new CArrayObj();
   if(list==NULL) return NULL;
   list.FreeMode(false);
   ListStorage.Add(list);
   for(int i=0; i<list_source.Total(); i++)
     {
      COrder *order=list_source.At(i);
      if(!order.SupportProperty(property)) continue;
      string order_prop=order.GetProperty(property);
      if(CompareValues(order_prop,value,mode)) list.Add(order);
     }
   return list;
  }
//+------------------------------------------------------------------+

Разберём на примере поиска по строковым критериям:

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

//+------------------------------------------------------------------+
//| Класс для выборки объектов, удовлетворяющих критерию             |
//+------------------------------------------------------------------+
class CSelect
  {
private:
   //--- Метод сравнения двух величин
   template<typename T>
   static bool       CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode);
public:
   //--- Возвращает список ордеров, у которых одно из (1) целочисленных, (2) вещественных и (3) строковых свойств удовлетворяет заданному критерию
   static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode);
   static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode);
   static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode);
   //--- Возвращает индекс ордера в списке с максимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства
   static int        FindOrderMax(CArrayObj* list_source,ENUM_ORDER_PROP_INTEGER property);
   static int        FindOrderMax(CArrayObj* list_source,ENUM_ORDER_PROP_DOUBLE property); 
   static int        FindOrderMax(CArrayObj* list_source,ENUM_ORDER_PROP_STRING property); 
   //--- Возвращает индекс ордера в списке с минимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства
   static int        FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_INTEGER property);
   static int        FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_DOUBLE property); 
   static int        FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_STRING property); 
  };
//+------------------------------------------------------------------+

и их реализацию:

//+------------------------------------------------------------------+
//| Возвращает индекс ордера в списке                                |
//| с максимальным значением целочисленного свойства                 |
//+------------------------------------------------------------------+
int CSelect::FindOrderMax(CArrayObj *list_source,ENUM_ORDER_PROP_INTEGER property)
  {
   if(list_source==NULL) return WRONG_VALUE;
   int index=0;
   COrder *max_order=NULL;
   int total=list_source.Total();
   if(total==0) return WRONG_VALUE;
   for(int i=1; i<total; i++)
     {
      COrder *order=list_source.At(i);
      long order1_prop=order.GetProperty(property);
      max_order=list_source.At(index);
      long order2_prop=max_order.GetProperty(property);
      if(CompareValues(order1_prop,order2_prop,MORE)) index=i;
     }
   return index;
  }
//+------------------------------------------------------------------+
//| Возвращает индекс ордера в списке                                |
//| с максимальным значением вещественного свойства                  |
//+------------------------------------------------------------------+
int CSelect::FindOrderMax(CArrayObj *list_source,ENUM_ORDER_PROP_DOUBLE property)
  {
   if(list_source==NULL) return WRONG_VALUE;
   int index=0;
   COrder *max_order=NULL;
   int total=list_source.Total();
   if(total==0) return WRONG_VALUE;
   for(int i=1; i<total; i++)
     {
      COrder *order=list_source.At(i);
      double order1_prop=order.GetProperty(property);
      max_order=list_source.At(index);
      double order2_prop=max_order.GetProperty(property);
      if(CompareValues(order1_prop,order2_prop,MORE)) index=i;
     }
   return index;
  }
//+------------------------------------------------------------------+
//| Возвращает индекс ордера в списке                                |
//| с максимальным значением строкового свойства                     |
//+------------------------------------------------------------------+
int CSelect::FindOrderMax(CArrayObj *list_source,ENUM_ORDER_PROP_STRING property)
  {
   if(list_source==NULL) return WRONG_VALUE;
   int index=0;
   COrder *max_order=NULL;
   int total=list_source.Total();
   if(total==0) return WRONG_VALUE;
   for(int i=1; i<total; i++)
     {
      COrder *order=list_source.At(i);               
      string order1_prop=order.GetProperty(property);
      max_order=list_source.At(index);                   
      string order2_prop=max_order.GetProperty(property);
      if(CompareValues(order1_prop,order2_prop,MORE)) index=i;
     }
   return index;
  }
//+------------------------------------------------------------------+

Разберём на примере поиска индекса ордера с максимальным строковым значением:

Аналогично устроены методы, возвращающие индекс ордера с минимальным значением заданного свойства:

//+------------------------------------------------------------------+
//| Возвращает индекс ордера в списке                                |
//| с минимальным значением целочисленного свойства                  |
//+------------------------------------------------------------------+
int CSelect::FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_INTEGER property)
  {
   int index=0;
   COrder* min_order=NULL;
   int total=list_source.Total();
   if(total==0) return WRONG_VALUE;
   for(int i=1; i<total; i++){
      COrder* order=list_source.At(i);
      long order1_prop=order.GetProperty(property);
      min_order=list_source.At(index);
      long order2_prop=min_order.GetProperty(property);
      if(CompareValues(order1_prop,order2_prop,LESS)) index=i;
      }
   return index;
  }
//+------------------------------------------------------------------+
//| Возвращает индекс ордера в списке                                |
//| с минимальным значением вещественного свойства                   |
//+------------------------------------------------------------------+
int CSelect::FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_DOUBLE property)
  {
   int index=0;
   COrder* min_order=NULL;
   int total=list_source.Total();
   if(total== 0) return WRONG_VALUE;
   for(int i=1; i<total; i++){
      COrder* order=list_source.At(i);
      double order1_prop=order.GetProperty(property);
      min_order=list_source.At(index);
      double order2_prop=min_order.GetProperty(property);
      if(CompareValues(order1_prop,order2_prop,LESS)) index=i;
      }
   return index;
  }
//+------------------------------------------------------------------+
//| Возвращает индекс ордера в списке                                |
//| с минимальным значением строкового свойства                      |
//+------------------------------------------------------------------+
int CSelect::FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_STRING property)
  {
   int index=0;
   COrder* min_order=NULL;
   int total=list_source.Total();
   if(total==0) return WRONG_VALUE;
   for(int i=1; i<total; i++){
      COrder* order=list_source.At(i);
      string order1_prop=order.GetProperty(property);
      min_order=list_source.At(index);
      string order2_prop=min_order.GetProperty(property);
      if(CompareValues(order1_prop,order2_prop,LESS)) index=i;
      }
   return index;
  }
//+------------------------------------------------------------------+

Теперь можно в класс-коллекцию исторических ордеров добавить выборку из списка-коллекции по времени и по заданным критериям.

Сначала добавим в файл Defines.mqh варианты сортировки по времени:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        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"

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define COUNTRY_LANG   ("Russian")              // Язык страны
#define DFUN           (__FUNCTION__+": ")      // "Описание функции"
#define END_TIME       (D'31.12.3000 23:59:59') // Конечная дата для запросов данных истории счёта
//+------------------------------------------------------------------+
//| Поиск                                                            |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Данные для поиска и сортировки                                   |
//+------------------------------------------------------------------+
enum ENUM_COMPARER_TYPE
  {
   EQUAL,                                                   // Равно
   MORE,                                                    // Больше
   LESS,                                                    // Меньше
   NO_EQUAL,                                                // Не равно
   EQUAL_OR_MORE,                                           // Больше или равно
   EQUAL_OR_LESS                                            // Меньше или равно
  };
//+------------------------------------------------------------------+
//| Возможные варианты выбора по времени                             |
//+------------------------------------------------------------------+
enum ENUM_SELECT_BY_TIME
  {
   SELECT_BY_TIME_OPEN,                                     // По времени открытия
   SELECT_BY_TIME_CLOSE,                                    // По времени закрытия
   SELECT_BY_TIME_OPEN_MSC,                                 // По времени открытия в милисекундах
   SELECT_BY_TIME_CLOSE_MSC,                                // По времени закрытия в милисекундах
  };
//+------------------------------------------------------------------+

К файлу HistoryCollection.mqh нужно подключить класс CSelect. Для этого заменим строку подключения сервисных функций на строку подключения файла класса CSelect:

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Arrays\ArrayObj.mqh>
#include "..\DELib.mqh"
#include "..\Objects\HistoryOrder.mqh"
#include "..\Objects\HistoryPending.mqh"
#include "..\Objects\HistoryDeal.mqh"
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Arrays\ArrayObj.mqh>
#include "Select.mqh"
#include "..\Objects\HistoryOrder.mqh"
#include "..\Objects\HistoryPending.mqh"
#include "..\Objects\HistoryDeal.mqh"
//+------------------------------------------------------------------+

Теперь вместо файла с сервисными функциями у нас подключен файл класса CSelect. Дело в том, что в файл Select.mqh мы подключали Order.mqh, а к файлу Order.mqh у нас уже подключен файл сервисных функций.

Объявим в публичной секции класса CHistoryCollection метод выбора ордеров из коллекции по заданному времени в диапазоне дат, а в приватную секцию добавим объект абстрактного ордера COrder — это будет ордер-образец для поиска значений:
//+------------------------------------------------------------------+
//| Коллекция исторических ордеров и сделок                          |
//+------------------------------------------------------------------+
class CHistoryCollection
  {
private:
   CArrayObj         m_list_all_orders;      // Список всех исторических ордеров и сделок
   COrder            m_order_instance;       // Объект-ордер для поиска по свойству
   bool              m_is_trade_event;       // Флаг торгового события
   int               m_index_order;          // Индекс последнего добавленного ордера в коллекцию из списка истории терминала (MQL4, MQL5)
   int               m_index_deal;           // Индекс последней добавленной сделки в коллекцию из списка истории терминала (MQL5)
   int               m_delta_order;          // Разница в количестве ордеров по сравнению с прошлой проверкой
   int               m_delta_deal;           // Разница в количестве сделок по сравнению с прошлой проверкой
public:
   //--- Возвращает полный список-коллекцию "как есть"
   CArrayObj        *GetList(void) { return &m_list_all_orders;  }
   //--- Выбирает ордера из коллекции со временем в диапазоне от begin_time до end_time
   CArrayObj        *GetListByTime(const datetime begin_time=0,const datetime end_time=0,
                                   const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE);
   //--- Конструктор
                     CHistoryCollection();
   //--- Обновляет список ордеров, заполняет данные о количестве новых и устанавливает флаг торгового события
   void              Refresh(void);
  };
//+------------------------------------------------------------------+

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

public:
   //--- Устанавливает (1) целочисленное, (2) вещественное и (3) строковое свойство ордера
   void              SetProperty(ENUM_ORDER_PROP_INTEGER property,long value) { m_long_prop[property]=value;                     }
   void              SetProperty(ENUM_ORDER_PROP_DOUBLE property,long value)  { m_long_prop[property]=value                    }
   void              SetProperty(ENUM_ORDER_PROP_STRING property,long value)  { m_long_prop[property]=value                    }
   //--- Возвращает из массива свойств (1) целочисленное, (2) вещественное и (3) строковое свойство ордера
   long              GetProperty(ENUM_ORDER_PROP_INTEGER property)      const { return m_long_prop[property];                    }
   double            GetProperty(ENUM_ORDER_PROP_DOUBLE property)       const { return m_double_prop[this.IndexProp(property)];  }
   string            GetProperty(ENUM_ORDER_PROP_STRING property)       const { return m_string_prop[this.IndexProp(property)];  }

   //--- Возвращает флаг поддержания ордером данного свойства
   virtual bool      SupportProperty(ENUM_ORDER_PROP_INTEGER property)        { return true; }
   virtual bool      SupportProperty(ENUM_ORDER_PROP_DOUBLE property)         { return true; }
   virtual bool      SupportProperty(ENUM_ORDER_PROP_STRING property)         { return true; }

   //--- Сравнивает объекты COrder между собой по всем возможным свойствам
   virtual int       Compare(const CObject *node,const int mode=0) const;

//+------------------------------------------------------------------+

В файл HistoryCollection.mqh добавим реализацию метода выобора ордеров из коллекции в диапазоне дат:

//+------------------------------------------------------------------+
//| Выбирает ордера из коллекции со временем                         |
//| в диапазоне от begin_time, до end_time                           |
//+------------------------------------------------------------------+
CArrayObj *CHistoryCollection::GetListByTime(const datetime begin_time=0,const datetime end_time=0,
                                             const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE)
  {
   ENUM_ORDER_PROP_INTEGER property=
     (
      select_time_mode==SELECT_BY_TIME_CLOSE       ?  ORDER_PROP_TIME_CLOSE      : 
      select_time_mode==SELECT_BY_TIME_OPEN        ?  ORDER_PROP_TIME_OPEN       :
      select_time_mode==SELECT_BY_TIME_CLOSE_MSC   ?  ORDER_PROP_TIME_CLOSE_MSC  : 
      ORDER_PROP_TIME_OPEN_MSC
     );

   CArrayObj *list=new CArrayObj();
   if(list==NULL)
     {
      ::Print(DFUN+TextByLanguage("Ошибка создания временного списка","Error creating temporary list"));
      return NULL;
     }
   datetime begin=begin_time,end=(end_time==0 ? END_TIME : end_time);
   if(begin_time>end_time) begin=0;
   list.FreeMode(false); 
   ListStorage.Add(list);
   //---
   m_order_instance.SetProperty(property,begin);
   int index_begin=m_list_all_orders.SearchGreatOrEqual(&m_order_instance);
   if(index_begin==WRONG_VALUE)
      return list;
   m_order_instance.SetProperty(property,end);
   int index_end=m_list_all_orders.SearchLessOrEqual(&m_order_instance);
   if(index_end==WRONG_VALUE)
      return list;
   for(int i=index_begin; i<=index_end; i++)
      list.Add(m_list_all_orders.At(i));
   return list;
  }
//+------------------------------------------------------------------+

Итого, что тут имеем:

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

//+------------------------------------------------------------------+
//| Коллекция исторических ордеров и сделок                          |
//+------------------------------------------------------------------+
class CHistoryCollection
  {
private:
   CArrayObj         m_list_all_orders;      // Список всех исторических ордеров и сделок
   COrder            m_order_instance;       // Объект-ордер для поиска по свойству
   bool              m_is_trade_event;       // Флаг торгового события
   int               m_index_order;          // Индекс последнего добавленного ордера в коллекцию из списка истории терминала (MQL4, MQL5)
   int               m_index_deal;           // Индекс последней добавленной сделки в коллекцию из списка истории терминала (MQL5)
   int               m_delta_order;          // Разница в количестве ордеров по сравнению с прошлой проверкой
   int               m_delta_deal;           // Разница в количестве сделок по сравнению с прошлой проверкой
public:
   //--- Возвращает полный список-коллекцию "как есть"
   CArrayObj        *GetList(void) { return &m_list_all_orders;  }
   //--- Выбирает ордера из коллекции со временем в диапазоне от begin_time до end_time
   CArrayObj        *GetListByTime(const datetime begin_time=0,const datetime end_time=0,
                                   const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE);
   //--- Возвращает список по выбранному (1) целочисленному, (2) вещественному и (3) строковому свойству, удовлетворяющему сравниваемому критерию
   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_DOUBLE property,double 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); }
   //--- Конструктор
                     CHistoryCollection();
   //--- Обновляет список ордеров, заполняет данные о количестве новых и устанавливает флаг торгового события
   void              Refresh(void);
  };
//+------------------------------------------------------------------+

В метод передаётся искомое свойство ордера, значение, с которым нужно сравнить и режим сравнения (больше/меньше/равно/не равно/больше или равно/меньше или равно). И далее при помощи ранее описанных методов класса CSelect возвращается список, отфильтрованный по требуемым свойствам, значениям и методу сравнения.

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

Возьмём тестовый советник из второй части TestDoEasyPart02.mq5 и сохраним его в папке MQL5\Experts\TestDoEasy, в новой подпапке Part03 под именем TestDoEasyPart03_1.mq5. Допишем к его input-параметрам выбор начала и конца диапазона дат и изменим код в обработчике OnInit(), где будем запрашивать историю в диапазоне дат:

//+------------------------------------------------------------------+
//|                                           TestDoEasyPart03_1.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\Collections\HistoryCollection.mqh>
//--- enums
enum ENUM_TYPE_ORDERS
  {
   TYPE_ORDER_MARKET,   // Market-orders
   TYPE_ORDER_PENDING,  // Pending orders
   TYPE_ORDER_DEAL      // Deals
  };
//--- input parameters
input ENUM_TYPE_ORDERS  InpOrderType   =  TYPE_ORDER_DEAL;  // Show type:
input datetime          InpTimeBegin   =  0;                // Start date of required range
input datetime          InpTimeEnd     =  END_TIME;         // End date of required range  
//--- global variables
CHistoryCollection history;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- обновляем историю
   history.Refresh();
//--- получаем список-коллекцию в диапазоне дат
   CArrayObj* list=history.GetListByTime(InpTimeBegin,InpTimeEnd,SELECT_BY_TIME_CLOSE);
   if(list==NULL)
     {
      Print("Could not get collection list");
      return INIT_FAILED;
     }
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем ордер из списка
      COrder* order=list.At(i);
      if(order==NULL) continue;
      //--- если это сделка
      if(order.Status()==ORDER_STATUS_DEAL && InpOrderType==TYPE_ORDER_DEAL)
         order.Print();
      //--- если это исторический маркет-ордер
      if(order.Status()==ORDER_STATUS_HISTORY_ORDER && InpOrderType==TYPE_ORDER_MARKET)
         order.Print();
      //--- если это удалённый отложенный ордер
      if(order.Status()==ORDER_STATUS_HISTORY_PENDING && InpOrderType==TYPE_ORDER_PENDING)
         order.Print();
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

Теперь вместо полного списка получаем список, выбранный по заданному диапазону дат методом GetListByTime(). Скомпилируем и запустим советник с настройками по умолчанию. В журнал будут выведены все сделки за историю счёта:


Нажмём F7 и введём в настройках конечную дату требуемого диапазона. Я сделал так: зашёл в историю счёта, посмотрел когда было пополнение и дату следующей (первой после пополнения счёта) сделки


и выбрал такой диапазон, чтобы первая сделка в него не попала: от 2018.01.22 до 2018.02.01.
В итоге в журнал попала только одна сделка — пополнение баланса:


Теперь сохраним советник TestDoEasyPart03_1.mq5 под именем TestDoEasyPart03_2mq5. Удалим входные параметры и изменим способ получения данных о сделках:

//+------------------------------------------------------------------+
//|                                           TestDoEasyPart03_2.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\Collections\HistoryCollection.mqh>
//--- global variables
CHistoryCollection history;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- обновляем историю
   history.Refresh();
//--- получаем из списка-коллекции только сделки
   CArrayObj* list=history.GetList(ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL);
//--- фильтруем полученный список по балансовым операциям
   list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,DEAL_TYPE_BALANCE,EQUAL);
   if(list==NULL)
     {
      Print("Could not get collection list");
      return INIT_FAILED;
     }
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем ордер из списка
      COrder* order=list.At(i);
      if(order==NULL) continue;
      order.Print();
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

Сначала получаем список всех сделок (выделяем из списка статусы ордеров с типом сделка), затем фильтруем полученный список по типу "балансовая операция". В обоих случаях используем режим сравнения "Равно".
В итоге в журнал выводится только одна сделка — балансовая операция "Начисление баланса":


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

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

//+------------------------------------------------------------------+
//|                                           TestDoEasyPart03_3.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\Collections\HistoryCollection.mqh>
//--- global variables
CHistoryCollection history;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- обновляем историю
   history.Refresh();
//--- получаем из списка-коллекции только сделки
   CArrayObj* list=history.GetList(ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL);
   if(list==NULL)
     {
      Print(TextByLanguage("Не удалось получить список","Could not get list"));
      return INIT_FAILED;
     }
//--- Получаем индекс сделки с наибольшим профитом (первое пополнение баланса)
   int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT);
   if(index!=WRONG_VALUE)
     {
      //--- получаем сделку из списка по индексу
      COrder* order=list.At(index);
      if(order!=NULL)
         order.Print();
     }
   else
      Print(TextByLanguage("Не найден индекс ордера с максимальным значением профита","Order index with maximum profit value not found"));
//--- Получаем индекс сделки с наименьшим профитом
   index=CSelect::FindOrderMin(list,ORDER_PROP_PROFIT);
   if(index!=WRONG_VALUE)
     {
      //--- получаем сделку из списка по индексу
      COrder* order=list.At(index);
      if(order!=NULL)
         order.Print();
     }
   else
      Print(TextByLanguage("Не найден индекс ордера с минимальным значением профита","Order index with minimum profit value not found"));
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

По окончании его работы в журнал будут выведены две сделки — с наибольшей (пополнение баланса) и наименьшей прибылью.



Базовый объект Engine основа библиотеки

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

В корневом каталоге библиотеки создадим новый класс CEngine на основе базового CObject и подключим к нему класс исторической коллекции:

//+------------------------------------------------------------------+
//|                                                       Engine.mqh |
//|                        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"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Collections\HistoryCollection.mqh"
//+------------------------------------------------------------------+
//| Класс-основа библиотеки                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
//--- Коллекция исторических ордеров и сделок
   CHistoryCollection   m_history;
public:
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+
//| CEngine конструктор                                              |
//+------------------------------------------------------------------+
CEngine::CEngine()
  {
  }
//+------------------------------------------------------------------+
//| CEngine деструктор                                               |
//+------------------------------------------------------------------+
CEngine::~CEngine()
  {
  }
//+------------------------------------------------------------------+

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

В первую очередь в публичной секции класса создадим обработчик OnTimer(), а за пределами тела класса его реализацию — в нём будут обновляться все коллекции:
//+------------------------------------------------------------------+
//|                                                       Engine.mqh |
//|                        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"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Collections\HistoryCollection.mqh"
//+------------------------------------------------------------------+
//| Класс-основа библиотеки                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
//--- Коллекция исторических ордеров и сделок
   CHistoryCollection   m_history;
public:
//--- Таймер
   void                 OnTimer(void);
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+
//| CEngine конструктор                                              |
//+------------------------------------------------------------------+
CEngine::CEngine()
  {
  }
//+------------------------------------------------------------------+
//| CEngine деструктор                                               |
//+------------------------------------------------------------------+
CEngine::~CEngine()
  {
  }
//+------------------------------------------------------------------+
//| CEngine таймер                                                   |
//+------------------------------------------------------------------+
void CEngine::OnTimer(void)
  {
   
  }
//+------------------------------------------------------------------+

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

Сначала добавим в файл Defines.mqh новые макроподстановки, в которых пропишем частоту общего таймера библиотеки в милисекундах и паузу счётчика таймера коллекций в милисекундах, шаг приращения счётчика таймера коллекций, а также идентификатор счётчика таймера обновления исторических ордеров и сделок (счётчиков может стать несколько в процессе разработки библиотеки, и у каждого должен быть свой идентификатор)
//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define COUNTRY_LANG             ("Russian")                // Язык страны
#define DFUN                     (__FUNCTION__+": ")        // "Описание функции"
#define END_TIME                 (D'31.12.3000 23:59:59')   // Конечная дата для запросов данных истории счёта
#define TIMER_FREQUENCY          (16                      // Минимальная частота таймера библиотеки в милисекундах
#define COLLECTION_PAUSE         (250)                      // Пауза таймера коллекции ордеров и сделок в милисекундах
#define COLLECTION_COUNTER_STEP  (16                      // Шаг приращения счётчика таймера коллекции ордеров и сделок
#define COLLECTION_COUNTER_ID    (1)                        // Идентификатор счётчика таймера коллекции ордеров и сделок
//+------------------------------------------------------------------+

В корневом каталоге библиотеки создадим новую папку Services, а в ней новый класс CTimerCounter. И сразу же перенесём в эту папку и наш файл сервисных функций DELib.mqh — ему там как раз самое место.

Затем, после переноса DELib.mqh в новую папку, в файле Order.mqh изменим адрес файла сервисных функций:

вместо адреса

//+------------------------------------------------------------------+
//|                                                        Order.mqh |
//|                        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"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Object.mqh>
#include "..\DELib.mqh"
//+------------------------------------------------------------------+

впишем адрес:

//+------------------------------------------------------------------+
//|                                                        Order.mqh |
//|                        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"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Object.mqh>
#include "..\Services\DELib.mqh"
//+------------------------------------------------------------------+

Теперь рассмотрим класс счётчика таймера. Класс простой, так что сразу посмотрим его листинг и разберём его работу:

//+------------------------------------------------------------------+
//|                                                 TimerCounter.mqh |
//|                        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"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Object.mqh>
#include "DELib.mqh"
//+------------------------------------------------------------------+
//| Класс счётчик таймера                                            |
//+------------------------------------------------------------------+
class CTimerCounter : public CObject
  {
private:  
   int               m_counter_id;   
   ulong             m_counter;      
   ulong             m_counter_step; 
   ulong             m_counter_pause;
public:
   //--- Возвращает флаг завершения ожидания
   bool              IsTimeDone(void);
   //--- Устанавливает параметры счётчика
   void              SetParams(const ulong step,const ulong pause)         { this.m_counter_step=step; this.m_counter_pause=pause;  }
   //--- Возвращает id счётчика
   virtual  int      Type(void)                                      const { return this.m_counter_id;                              }
   //--- Сравнивает объекты-счётчики между собой
   virtual int       Compare(const CObject *node,const int mode=0)   const;
   //--- Конструктор
                     CTimerCounter(const int id);
  };
//+------------------------------------------------------------------+
//| CTimerCounter конструктор                                        |
//+------------------------------------------------------------------+
CTimerCounter::CTimerCounter(const int id) : m_counter(0),m_counter_step(16),m_counter_pause(16)
  {
   this.m_counter_id=id;
  }
//+------------------------------------------------------------------+
//| CTimerCounter возвращает флаг окончание паузы                    |
//+------------------------------------------------------------------+
bool CTimerCounter::IsTimeDone(void)
  {
   if(this.m_counter>=ULONG_MAX)
      this.m_counter=0;
   if(this.m_counter<this.m_counter_pause)
     {
      this.m_counter+=this.m_counter_step;
      return false;
     }
   this.m_counter=0;
   return true;
  }
//+------------------------------------------------------------------+
//| Сравнивает объекты CTimerCounter между собой по id               |
//+------------------------------------------------------------------+
int CTimerCounter::Compare(const CObject *node,const int mode=0) const
  {
   const CTimerCounter *counter_compared=node;
   int value_compared=counter_compared.Type();
   int value_current=this.Type();
   return(value_current>value_compared ? 1 : value_current<value_compared ? -1 : 0);
   return 0;
  }
  
//+------------------------------------------------------------------+

Так как мы переместили DELib.mqh в ту же папку, в которой расположен класс-счётчик, то и подключаем его напрямую из этой же папки. К файлу DELib.mqh подключен Defines.mqh, так что все макроподстановки класс будет видеть.

Метод, возвращающий флаг окончания паузы устроен просто:

Метод, возвращающий идентификатор счётчика Type(), сделан виртуальным. В поставке класса CObject есть виртуальный метод, возвращающий тип объекта:

   //--- method of identifying the object
   virtual int       Type(void)                                    const { return(0);      }

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

   virtual  int      Type(void)                                    const { return this.m_counter_id; }

Виртуальный метод сравнения двух объектов-счётчиков прост:

//+------------------------------------------------------------------+
//| Сравнивает объекты CTimerCounter между собой по id               |
//+------------------------------------------------------------------+
int CTimerCounter::Compare(const CObject *node,const int mode=0) const
  {
   const CTimerCounter *counter_compared=node;
   int value_compared=counter_compared.Type();
   int value_current=this.Type();
   return(value_current>value_compared ? 1 : value_current<value_compared ? -1 : 0);
   return 0;
  }
//+------------------------------------------------------------------+

Получаем ссылку на объект-источник, берём из источника его id, берём id текущего счётчика, далее — возврат результата простого сравнения на больше/меньше/равно.


Продолжим наполнять класс CEngine. Подключим класс счётчика таймера к файлу Engine.mqh:

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Collections\HistoryCollection.mqh"
#include "Services\TimerCounter.mqh"
//+------------------------------------------------------------------+

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

В конструкторе класса инициализируем милисекундный таймер, поставим флаг сортированного списка и создадим счётчик таймера обновления коллекции исторических ордеров и сделок. В деструкторе класса пропишем уничтожение таймера:
//+------------------------------------------------------------------+
//| Класс-основа библиотеки                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
//--- Список счётчиков таймера
   CArrayObj            m_list_counters;           
//--- Коллекция исторических ордеров и сделок
   CHistoryCollection   m_history;
//--- Возвращает индекс счётчика по id
   int                  CounterIndex(const int id) const;
public:
//--- Создаёт счётчик таймера
   void                 CreateCounter(const int counter_id,const ulong frequency,const ulong pause);
//--- Таймер
   void                 OnTimer(void);
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+
//| CEngine конструктор                                              |
//+------------------------------------------------------------------+
CEngine::CEngine()
  {
   ::EventSetMillisecondTimer(TIMER_FREQUENCY);
   this.m_list_counters.Sort();
   this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_MIN_PAUSE);
  }
//+------------------------------------------------------------------+
//| CEngine деструктор                                               |
//+------------------------------------------------------------------+
CEngine::~CEngine()
  {
   ::EventKillTimer();
  }
//+------------------------------------------------------------------+

Реализация метода, возвращающего индекс счётчика по его идентификатору:

//+------------------------------------------------------------------+
//| Возвращает индекс счётчика в списке по id                        |
//+------------------------------------------------------------------+
int CEngine::CounterIndex(const int id) const
  {
   int total=this.m_list_counters.Total();
   for(int i=0;i<total;i++)
     {
      CTimerCounter* counter=this.m_list_counters.At(i);
      if(counter==NULL) continue;
      if(counter.Type()==id) 
         return i;
     }
   return WRONG_VALUE;
  }
//+------------------------------------------------------------------+

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

Рассмотрим метод создания счётчика таймера:

//+------------------------------------------------------------------+
//| Создаёт счётчик таймера                                          |
//+------------------------------------------------------------------+
void CEngine::CreateCounter(const int id,const ulong step,const ulong pause)
  {
   if(this.CounterIndex(id)>WRONG_VALUE)
     {
      ::Print(TextByLanguage("Ошибка. Уже создан счётчик с идентификатором ","Error. Already created a counter with id "),(string)id);
      return;
     }
   m_list_counters.Sort();
   CTimerCounter* counter=new CTimerCounter(id);
   if(counter==NULL)
      ::Print(TextByLanguage("Не удалось создать счётчик таймера ","Failed to create timer counter "),(string)id);
   counter.SetParams(step,pause);
   if(this.m_list_counters.Search(counter)==WRONG_VALUE)
      this.m_list_counters.Add(counter);
   else
     {
      string t1=TextByLanguage("Ошибка. Счётчик с идентификатором ","Error. Counter with ID ")+(string)id;
      string t2=TextByLanguage(", шагом ",", step ")+(string)step;
      string t3=TextByLanguage(" и паузой "," and pause ")+(string)pause;
      ::Print(t1+t2+t3+TextByLanguage(" уже существует"," already exists"));
      delete counter;
     }
  }
//+------------------------------------------------------------------+

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

Затем выполняется поиск точно такого же счётчика в списке и, если его там нету, то новый счётчик добавляется к списку.

В противном случае формируется сообщение, включающее в себя все параметры и выводится в журнал. Затем объект-счётчик удаляется — он не нужен — точно такой уже есть.

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

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

//+------------------------------------------------------------------+
//| Коллекция исторических ордеров и сделок                          |
//+------------------------------------------------------------------+
class CHistoryCollection
  {
private:
   CArrayObj         m_list_all_orders;      // Список всех исторических ордеров и сделок
   COrder            m_order_instance;       // Объект-ордер для поиска по свойству
   bool              m_is_trade_event;       // Флаг торгового события
   int               m_index_order;          // Индекс последнего добавленного ордера в коллекцию из списка истории терминала (MQL4, MQL5)
   int               m_index_deal;           // Индекс последней добавленной сделки в коллекцию из списка истории терминала (MQL5)
   int               m_delta_order;          // Разница в количестве ордеров по сравнению с прошлой проверкой
   int               m_delta_deal;           // Разница в количестве сделок по сравнению с прошлой проверкой
public:
   //--- Выбирает ордера из коллекции со временем в диапазоне от begin_time до end_time
   CArrayObj        *GetListByTime(const datetime begin_time=0,const datetime end_time=0,
                                   const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE);
   //--- Возвращает полный список-коллекцию "как есть"
   CArrayObj        *GetList(void)                                                                       { return &m_list_all_orders;                                            }
   //--- Возвращает список по выбранному (1) целочисленному, (2) вещественному и (3) строковому свойству, удовлетворяющему сравниваемому критерию
   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_DOUBLE property,double 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) новых сделок
   int               NewOrders(void)                                                                     { return m_delta_order; }
   int               NewDeals(void                                                                     { return m_delta_deal;  }
   
   //--- Конструктор
                     CHistoryCollection();
   //--- Обновляет список ордеров, заполняет данные о количестве новых и устанавливает флаг торгового события
   void              Refresh(void);
  };
//+------------------------------------------------------------------+

Методы просто возвращают значения соответствующих переменных-членов класса.

И теперь в 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.IsTimeDone())
        {
         this.m_history.Refresh();
         if(this.m_history.NewOrders()>0)
           {
            Print(DFUN,TextByLanguage("Изменилось количество исторических ордеров: NewOrders=","The number of historical orders has changed: NewOrders="),this.m_history.NewOrders());
           }
         if(this.m_history.NewDeals()>0)
           {
            Print(DFUN,TextByLanguage("Изменилось количество сделок: NewDeals=","The number of deals has changed: NewDeals="),this.m_history.NewOrders());
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

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

Для проверки сделаем простой советник. В папке терминала Experts\TestDoEasy\Part3 создадим советник под именем TestDoEasyPart03_4.mq5 с таймером. Для создания шаблона советника с таймером, на второй странице Мастера MQL установите флажок на чекбоксе таймера:

Жмём "Далее" до конца работы мастера. В итоге будет создан пустой шаблон советника, к которому подключим главный файл библиотеки, создадим объект класса библиотеки и в таймере советника вызовем таймер библиотеки.

//+------------------------------------------------------------------+
//|                                           TestDoEasyPart03_4.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>
//--- global variables
CEngine        engine;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
   engine.OnTimer();
  }
//+------------------------------------------------------------------+

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

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

  1. об изменении количества ордеров (был выставлен открывающий маркет-ордер) и
  2. об изменении количества сделок (маркет-ордер сработал и породил сделку "Вход в рынок").

Теперь если закрыть позицию, то в журнале так же появятся две записи:

  1. о появлении закрывающего маркет-ордера и
  2. о появлении новой сделки (закрывающий маркет-ордер сработал и породил сделку "Выход из рынка")

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

  1. вообще ничего не показывать в журнале при первом запуске
  2. показать сообщения о состоянии счёта при первом запуске
Так как сбор и вывод необходимой статистики мы будем делать в последующих частях описания, то пока воспользуемся первым вариантом "вообще ничего не показывать в журнале при первом запуске" и просто сделаем заглушку для первого запуска:

В приватной секции класса CEngine добавим флаг первого запуска и метод, проверяющий и сбрасывающий этот флаг:

//+------------------------------------------------------------------+
//| Класс-основа библиотеки                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
   CHistoryCollection   m_history;                       // Коллекция исторических ордеров и сделок
   CArrayObj            m_list_counters;                 // Список счётчиков таймера
   bool                 m_first_start;                   // Флаг первого запуска
//--- Возвращает индекс счётчика по id
   int                  CounterIndex(const int id) const;
//--- Возвращает флаг первого запуска
   bool                 IsFirstStart(void);
public:
//--- Создаёт счётчик таймера
   void                 CreateCounter(const int id,const ulong frequency,const ulong pause);
//--- Таймер
   void                 OnTimer(void);
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+

и за пределами тела класса напишем реализацию метода проверки и сброса флага первого запуска:

//+------------------------------------------------------------------+
//| Возвращает флаг первого запуска, сбрасывает флаг                 |
//+------------------------------------------------------------------+
bool CEngine::IsFirstStart(void)
  {
   if(this.m_first_start)
     {
      this.m_first_start=false;
      return true;             
     }
   return false;
  }
//+------------------------------------------------------------------+

Тут всё предельно просто: если флаг установлен, то сбрасываем его и возвращаем true, иначе — возвращаем false.
Теперь в конструкторе класса в списке инициализации нужно установить флаг, чтобы при первом запуске он всегда был во "взведённом состоянии".

//+------------------------------------------------------------------+
//| CEngine конструктор                                              |
//+------------------------------------------------------------------+
CEngine::CEngine() : m_first_start(true)
  {
   ::EventSetMillisecondTimer(TIMER_FREQUENCY);
   this.m_list_counters.Sort();
   this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE);
  }
//+------------------------------------------------------------------+

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

Проверить это можно, запустив тестовый советник из расположения: MQL5\Experts\TestDoEasy\Part2\TestDoEasyPart03_4.mq5 и убедиться, что при первом запуске в журнал экспертов не будут выводиться сообщения о добавлении в историю счёта ордеров и сделок.

Объекты активных рыночных ордеров и позиций

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

В папке библиотеки Objects создадим новый класс CMarketPosition на базе абстрактного ордера библиотеки COrder — это будет объект-рыночная позиция:


После нажатия на кнопку "Готово" будет создан шаблон класса с именем MarketPosition.mqh, в который мы сразу же добавим подключение класса COrder:

//+------------------------------------------------------------------+
//|                                               MarketPosition.mqh |
//|                        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"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Order.mqh"
//+------------------------------------------------------------------+
//| Рыночная позиция                                                 |
//+------------------------------------------------------------------+
class CMarketPosition : public COrder
  {
private:

public:
                     CMarketPosition();
                    ~CMarketPosition();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CMarketPosition::CMarketPosition()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CMarketPosition::~CMarketPosition()
  {
  }
//+------------------------------------------------------------------+

Изменим конструктор так, чтобы в него передавался тикет позиции, и в его списке инициализации установим классу-родителю (COrder) статус "рыночная позиция" и отправим в него же тикет. Объявим три виртуальных метода, возвращающие флаг поддержания позицией целочисленного, вещественного и строкового свойств:

//+------------------------------------------------------------------+
//| Рыночная позиция                                                 |
//+------------------------------------------------------------------+
class CMarketPosition : public COrder
  {
public:
   //--- Конструктор
                     CMarketPosition(const ulong ticket=0) : COrder(ORDER_STATUS_MARKET_POSITION,ticket) {}
   //--- Поддерживаемые свойства позиции (1) вещественные, (2) целочисленные
   virtual bool      SupportProperty(ENUM_ORDER_PROP_INTEGER property);
   virtual bool      SupportProperty(ENUM_ORDER_PROP_DOUBLE property);
   virtual bool      SupportProperty(ENUM_ORDER_PROP_STRING property);
  };
//+------------------------------------------------------------------+

и за пределами тела класса впишем реализацию этих методов:

//+------------------------------------------------------------------+
//| Возвращает истину, если позиция поддерживает переданное          |
//| целочисленное свойство, возвращает ложь в противном случае       |
//+------------------------------------------------------------------+
bool CMarketPosition::SupportProperty(ENUM_ORDER_PROP_INTEGER property)
  {
   if(property==ORDER_PROP_TIME_CLOSE     || 
      property==ORDER_PROP_TIME_CLOSE_MSC ||
      property==ORDER_PROP_TIME_EXP       ||
      property==ORDER_PROP_POSITION_BY_ID ||
      property==ORDER_PROP_DEAL_ORDER     ||
      property==ORDER_PROP_DEAL_ENTRY     ||
      property==ORDER_PROP_CLOSE_BY_SL    ||
      property==ORDER_PROP_CLOSE_BY_TP
     #ifdef __MQL5__                      ||
      property==ORDER_PROP_TICKET_FROM    ||
      property==ORDER_PROP_TICKET_TO
     #endif 
     ) return false;
   return true;
}
//+------------------------------------------------------------------+
//| Возвращает истину, если позиция поддерживает переданное          |
//| вещественное свойство, возвращает ложь в противном случае        |
//+------------------------------------------------------------------+
bool CMarketPosition::SupportProperty(ENUM_ORDER_PROP_DOUBLE property)
  {
   if(property==ORDER_PROP_PRICE_CLOSE || property==ORDER_PROP_PRICE_STOP_LIMIT) return false;
   return true;
  }
//+------------------------------------------------------------------+
//| Возвращает истину, если позиция поддерживает переданное          |
//| строковое свойство, возвращает ложь в противном случае           |
//+------------------------------------------------------------------+
bool CMarketPosition::SupportProperty(ENUM_ORDER_PROP_STRING property)
  {
   if(property==ORDER_PROP_EXT_ID) return false;
   return true;
  }
//+------------------------------------------------------------------+

Здесь всё аналогично созданию объектов исторических ордеров и сделок, процесс создания которых был описан во второй части описания библиотеки.

Теперь аналогично создадим объект рыночного отложенного ордера. Создадим новый класс CMarketPending на базе абстрактного ордера библиотеки COrder в папке Objects и впишем в него уже знакомые нам изменения созданного Мастером MQL шаблона класса:

//+------------------------------------------------------------------+
//|                                                MarketPending.mqh |
//|                        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"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Order.mqh"
//+------------------------------------------------------------------+
//| Рыночный отложенный ордер                                        |
//+------------------------------------------------------------------+
class CMarketPending : public COrder
  {
public:
   //--- Конструктор
                     CMarketPending(const ulong ticket=0) : COrder(ORDER_STATUS_MARKET_PENDING,ticket) {}
   //--- Поддерживаемые свойства ордера (1) вещественные, (2) целочисленные
   virtual bool      SupportProperty(ENUM_ORDER_PROP_DOUBLE property);
   virtual bool      SupportProperty(ENUM_ORDER_PROP_INTEGER property);
  };
//+------------------------------------------------------------------+
//| Возвращает истину, если ордер поддерживает переданное            |
//| целочисленное свойство, возвращает ложь в противном случае       |
//+------------------------------------------------------------------+
bool CMarketPending::SupportProperty(ENUM_ORDER_PROP_INTEGER property)
  {
   if(property==ORDER_PROP_PROFIT_PT         ||
      property==ORDER_PROP_DEAL_ORDER        ||
      property==ORDER_PROP_DEAL_ENTRY        ||
      property==ORDER_PROP_TIME_UPDATE       ||
      property==ORDER_PROP_TIME_CLOSE        ||
      property==ORDER_PROP_TIME_CLOSE_MSC    ||
      property==ORDER_PROP_TIME_UPDATE_MSC   ||
      property==ORDER_PROP_TICKET_FROM       ||
      property==ORDER_PROP_TICKET_TO         ||
      property==ORDER_PROP_CLOSE_BY_SL       ||
      property==ORDER_PROP_CLOSE_BY_TP
     ) return false;
   return true;
  }
//+------------------------------------------------------------------+
//| Возвращает истину, если ордер поддерживает переданное            |
//| вещественное свойство, возвращает ложь в противном случае        |
//+------------------------------------------------------------------+
bool CMarketPending::SupportProperty(ENUM_ORDER_PROP_DOUBLE property)
  {
   if(property==ORDER_PROP_COMMISSION  ||
      property==ORDER_PROP_SWAP        ||
      property==ORDER_PROP_PROFIT      ||
      property==ORDER_PROP_PROFIT_FULL ||
      property==ORDER_PROP_PRICE_CLOSE
     ) return false;
   return true;
  }
//+------------------------------------------------------------------+

Здесь в конструкторе класса, в его списке инициализации в базовый COrder передаём статус "отложенный ордер".

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

Коллекция активных рыночных ордеров и позиций

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

Для этого:

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

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

Итак, начнём. В папке библиотеки Collections создадим новый класс CMarketCollection. Для этого правой кнопкой щёлкнем по папке Collection и выберем "Новый файл". В открывшемся мастере MQL выберем "Новый класс" и нажмём "Далее".


Введём название класса CMarketCollection и нажмём кнопку "Готово" Будет создан шаблон класса под именем MarketCollection.mqh:

//+------------------------------------------------------------------+
//|                                             MarketCollection.mqh |
//|                        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"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CMarketCollection
  {
private:

public:
                     CMarketCollection();
                    ~CMarketCollection();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CMarketCollection::CMarketCollection()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CMarketCollection::~CMarketCollection()
  {
  }
//+------------------------------------------------------------------+

Займёмся его наполнением.

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

//+------------------------------------------------------------------+
//|                                             MarketCollection.mqh |
//|                        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"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Arrays\ArrayObj.mqh>
#include "Select.mqh"
#include "..\Objects\MarketPending.mqh" 
#include "..\Objects\MarketPosition.mqh"
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//|                                             MarketCollection.mqh |
//|                        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"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Arrays\ArrayObj.mqh>
#include "Select.mqh"
#include "..\Objects\MarketPending.mqh" 
#include "..\Objects\MarketPosition.mqh"
//+------------------------------------------------------------------+
//| Коллекция рыночных ордеров и позиций                             |
//+------------------------------------------------------------------+
class CMarketCollection
  {
private:
   struct MqlDataCollection
     {
      long           hash_sum_acc;           // Хэш-сумма всех ордеров и позиций на счёте
      int            total_pending;          // Количество отложенных ордеров на счёте
      int            total_positions;        // Количество позиций на счёте
      double         total_volumes;          // Общий объём ордеров и позиций на счёте
     };
   MqlDataCollection m_struct_curr_market;   // Текущие данные рыночных ордеров и позиций на счёте
   MqlDataCollection m_struct_prev_market;   // Прошлые данные рыночных ордеров и позиций на счёте
public:
                     CMarketCollection();
                    ~CMarketCollection();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CMarketCollection::CMarketCollection()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CMarketCollection::~CMarketCollection()
  {
  }
//+------------------------------------------------------------------+

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

Рассмотрим тикет. Добавление/удаление отложенного ордера изменит общую сумму тикетов на счёте, срабатывание отложенного ордера не изменит общую сумму тикетов на счёте.

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

Значит смотрим ещё одно свойство позиции — время её изменения в милисекундах: открытие новой позиции изменит общее время изменения позиции, частичное закрытие изменит время изменения позиции, добавление объёма на неттинговом счёте изменит общее время изменения позиции.

Что из всего этого нам подойдёт для однозначного определения произошедшего изменения на счёте? Тикет+время изменения позиции. Проверим:

  • Открыли позицию — сумма тикетов изменилась + сумма времени изменения позиции изменилась есть изменение
  • Закрыли позицию — сумма тикетов изменилась + сумма времени изменения позиции изменилась есть изменение
  • Выставили отложенный ордер — сумма тикетов изменилась + сумма времени изменения позиции не измениласьесть изменение
  • Удалили отложенный ордер — сумма тикетов изменилась + сумма времени изменения позиции не изменилась есть изменение
  • Отложенный ордер активировался — сумма тикетов не изменилась + сумма времени изменения позиции изменилась есть изменение
  • Частичное закрытие позиции — сумма тикетов изменилась + сумма времени изменения позиции изменилась есть изменение
  • Добавление объёма к позиции — сумма тикетов не изменилась + сумма времени изменения позиции изменилась есть изменение
Таким образом, для хэш-суммы будем использовать тикет+время изменения позиции в милисекундах.

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

//+------------------------------------------------------------------+
//| Коллекция рыночных ордеров и позиций                             |
//+------------------------------------------------------------------+
class CMarketCollection
  {
private:
   struct MqlDataCollection
     {
      long           hash_sum_acc;           // Хэш-сумма всех ордеров и позиций на счёте
      int            total_pending;          // Количество отложенных ордеров на счёте
      int            total_positions;        // Количество позиций на счёте
      double         total_volumes;          // Общий объём ордеров и позиций на счёте
     };
   MqlDataCollection m_struct_curr_market;   // Текущие данные рыночных ордеров и позиций на счёте
   MqlDataCollection m_struct_prev_market;   // Прошлые данные рыночных ордеров и позиций на счёте
   CArrayObj         m_list_all_orders;      // Список отложенных ордеров и позиций на счёте
   bool              m_is_trade_event;       // Флаг торгового события
   bool              m_is_change_volume;     // Флаг изменения общего объёма
   double            m_change_volume_value;  // Величина изменения общего объёма
   int               m_new_positions;        // Количество новых позиций
   int               m_new_pendings;         // Количество новых отложенных ордеров
public:
   //--- Конструктор
                     CMarketCollection(void);
   //--- Обновляет список отложенных ордеров и позиций
   void              Refresh(void);
  };
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CMarketCollection::CMarketCollection(void) : m_is_trade_event(false),m_is_change_volume(false),m_change_volume_value(0)
  {
   m_list_all_orders.Sort(SORT_BY_ORDER_TIME_OPEN);
   ::ZeroMemory(this.m_struct_prev_market);
   this.m_struct_prev_market.hash_sum_acc=WRONG_VALUE;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Коллекция рыночных ордеров и позиций                             |
//+------------------------------------------------------------------+
class CMarketCollection
  {
private:
   struct MqlDataCollection
     {
      long           hash_sum_acc;           // Хэш-сумма всех ордеров и позиций на счёте
      int            total_pending;          // Количество отложенных ордеров на счёте
      int            total_positions;        // Количество позиций на счёте
      double         total_volumes;          // Общий объём ордеров и позиций на счёте
     };
   MqlDataCollection m_struct_curr_market;   // Текущие данные рыночных ордеров и позиций на счёте
   MqlDataCollection m_struct_prev_market;   // Прошлые данные рыночных ордеров и позиций на счёте
   CArrayObj         m_list_all_orders;      // Список отложенных ордеров и позиций на счёте
   bool              m_is_trade_event;       // Флаг торгового события
   bool              m_is_change_volume;     // Флаг изменения общего объёма
   double            m_change_volume_value;  // Величина изменения общего объёма
   int               m_new_positions;        // Количество новых позиций
   int               m_new_pendings;         // Количество новых отложенных ордеров
   //--- Сохраняет текущие значения состояния данных счёта как прошлые
   void              SavePrevValues(void)             { this.m_struct_prev_market=this.m_struct_curr_market;   }
public:
   //--- Возвращает количество (1) новых отложенных ордеров, (2) новых позиций, (3) флаг произошедшего торгового события
   int               NewOrders(void)    const         { return this.m_new_pendings;                            }
   int               NewPosition(void)  const         { return this.m_new_positions;                           }
   bool              IsTradeEvent(void) const         { return this.m_is_trade_event;                          }
   //--- Конструктор
                     CMarketCollection(void);
   //--- Обновляет список отложенных ордеров и позиций
   void              Refresh(void);
  };
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Обновляет список ордеров                                         |
//+------------------------------------------------------------------+
void CMarketCollection::Refresh(void)
  {
   ::ZeroMemory(this.m_struct_curr_market);
   this.m_is_trade_event=false;            
   this.m_is_change_volume=false;          
   this.m_new_pendings=0;                  
   this.m_new_positions=0;                 
   this.m_change_volume_value=0;           
   m_list_all_orders.Clear();              
#ifdef __MQL4__
   int total=::OrdersTotal();
   for(int i=0; i<total; i++)
     {
      if(!::OrderSelect(i,SELECT_BY_POS)) continue;
      long ticket=::OrderTicket();
      ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::OrderType();
      if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL)
        {
         CMarketPosition *position=new CMarketPosition(ticket);
         if(position==NULL) continue;
         if(this.m_list_all_orders.InsertSort(position))
           {
            this.m_struct_market.hash_sum_acc+=ticket;
            this.m_struct_market.total_volumes+=::OrderLots();
            this.m_struct_market.total_positions++;
           }
         else
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить позицию в список","Failed to add position to the list"));
            delete position;
           }
        }
      else
        {
         CMarketPending *order=new CMarketPending(ticket);
         if(order==NULL) continue;
         if(this.m_list_all_orders.InsertSort(order))
           {
            this.m_struct_market.hash_sum_acc+=ticket;
            this.m_struct_market.total_volumes+=::OrderLots();
            this.m_struct_market.total_pending++;
           }
         else
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to the list"));
            delete order;
           }
        }
     }
//--- MQ5
#else    
//--- Позиции
   int total_positions=::PositionsTotal();
   for(int i=0; i<total_positions; i++)
     {
      ulong ticket=::PositionGetTicket(i);
      if(ticket==0) continue;
      CMarketPosition *position=new CMarketPosition(ticket);
      if(position==NULL) continue;
      if(this.m_list_all_orders.InsertSort(position))
        {
         this.m_struct_curr_market.hash_sum_acc+=(long)::PositionGetInteger(POSITION_TIME_UPDATE_MSC);
         this.m_struct_curr_market.total_volumes+=::PositionGetDouble(POSITION_VOLUME);
         this.m_struct_curr_market.total_positions++;
        }
      else
        {
         ::Print(DFUN,TextByLanguage("Не удалось добавить позицию в список","Failed to add position to the list"));
         delete position;
        }
     }
//--- Ордера
   int total_orders=::OrdersTotal();
   for(int i=0; i<total_orders; i++)
     {
      ulong ticket=::OrderGetTicket(i);
      if(ticket==0) continue;
      CMarketPending *order=new CMarketPending(ticket);
      if(order==NULL) continue;
      if(this.m_list_all_orders.InsertSort(order))
        {
         this.m_struct_curr_market.hash_sum_acc+=(long)ticket;
         this.m_struct_curr_market.total_volumes+=::OrderGetDouble(ORDER_VOLUME_INITIAL);
         this.m_struct_curr_market.total_pending++;
        }
      else
        {
         ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to the list"));
         delete order;
        }
     }
#endif 
//--- Первый запуск
   if(this.m_struct_prev_market.hash_sum_acc==WRONG_VALUE)
     {
      this.SavePrevValues();                              
     }                                                    
//--- Если хэш-сумма всех ордеров и позиций изменилась
   if(this.m_struct_curr_market.hash_sum_acc!=this.m_struct_prev_market.hash_sum_acc)
     {
      this.m_new_pendings=this.m_struct_curr_market.total_pending-this.m_struct_prev_market.total_pending;
      this.m_new_positions=this.m_struct_curr_market.total_positions-this.m_struct_prev_market.total_positions;
      this.m_change_volume_value=::NormalizeDouble(this.m_struct_curr_market.total_volumes-this.m_struct_prev_market.total_volumes,4);
      this.m_is_change_volume=(this.m_change_volume_value!=0 ? true : false);
      this.m_is_trade_event=true;
      this.SavePrevValues();
     }
  }
//+------------------------------------------------------------------+

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

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

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

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

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

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

Затем проверяется на принадлежность к MQL4 или к MQL5.
Так как на данном этапе мы делаем код на MQL5, то разберём именно его:

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

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

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

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

Метод SavePrevValues() просто копирует структуру с текущими значениями в структуру с прошлыми значениями.

Чтобы проверить работу метода обновления списка рыночных ордеров и позиций и его совместную работу с методом обновления списка исторических ордеров и сделок, используем последний тестовый советник из папки Part03 с именем TestDoEasyPart03_4.mq5:

//+------------------------------------------------------------------+
//|                                           TestDoEasyPart03_4.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>
//--- global variables
CEngine        engine;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
   engine.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 && counter.IsTimeDone())
        {
         //--- Обновление списков 
         this.m_market.Refresh(); 
         this.m_history.Refresh();
         //--- Действия при первом запуске
         if(this.IsFirstStart())
           {
            return;
           }
         //--- Проверка изменения рыночного состояния
         if(this.m_market.IsTradeEvent())
           {
            Print(DFUN,TextByLanguage("Новое торговое событие на счёте","New trading event on the account"));
           }
         //--- Проверка изменения в истории счёта
         if(this.m_history.IsTradeEvent())
           {
            Print(DFUN,TextByLanguage("Новое торговое событие в истории счёта","New trading event in the account history"));
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

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

2019.02.28 17:36:24.678 CEngine::OnTimer: New trading event on the account
2019.02.28 17:36:24.678 CEngine::OnTimer: New trading event in the account history

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

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

Что дальше

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

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

К содержанию