preview
Мониторинг торговли с помощью Push-уведомлений — пример сервиса в MetaTrader 5

Мониторинг торговли с помощью Push-уведомлений — пример сервиса в MetaTrader 5

MetaTrader 5Примеры | 24 июля 2024, 16:50
498 4
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

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

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

Чтобы проанализировать свою стратегию, нужно нажать "Отчет \ Обзор" в контекстном меню раздела торговой истории или "Отчеты" в меню "Вид" ( либо просто по комбинации клавиш Alt+E):




Подробнее об отчётах в терминале MetaTrader 5 можно почитать в статье "Новый отчет в MetaTrader: 5 самых важных показателей торговли".

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

Наша программа должна запускаться в момент запуска терминала, отслеживать смену аккаунта или торгового счёта, наступление дня и времени для создания и отправки отчётов. Для таких целей нам подойдёт тип программы "Сервис".

Как следует из справки, Сервис — программа, которая в отличие от индикаторов, советников и скриптов для своей работы не требует привязки к графику. Как и скрипты, сервисы не обрабатывают никаких событий, кроме события запуска. Для запуска сервиса в его коде обязательно должна быть функция-обработчик OnStart. Сервисы не принимают никаких других событий кроме Start, но могут сами отправлять графикам пользовательские события с помощью EventChartCustom. Сервисы хранятся в директории <каталог_терминала>\MQL5\Services.

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

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


Структура проекта

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

  • Программа сервис. Имеет доступ к данным всех аккаунтов, которые были активными за всё время беспрерывной работы сервиса. Из данных всех аккаунтов программа получает списки закрытых позиций и объединяет их в один общий список. В зависимости от настроек, сервис может использовать данные о закрытых позициях только текущего активного аккаунта, либо с текущего и каждого из ранее использованных в торговле в клиентском терминале аккаунтов.
    Из данных о закрытых позициях, полученных из списка аккаунтов, создаётся статистика торговли за требуемые торговые периоды и отправляется в Push уведомлениях на смартфон пользователя. Дополнительно торговая статистика в табличном виде выводится в журнал терминала "Эксперты".
  • Коллекция аккаунтов. Включает в себя список аккаунтов, к которым был подключен терминал за время беспрерывной работы сервиса. Коллекция аккаунтов даёт доступ к любому аккаунту в списке и ко всем закрытым позициям всех аккаунтов. Списки доступны в программе сервисе, и на их основании сервис делает выборки и создаёт статистику.
  • Класс объекта аккаунта. Хранит в себе данные одного аккаунта со списком (коллекцией) всех закрытых позиций, транзакции которых были проведены на этом аккаунте за время беспрерывной работы сервиса. Даёт доступ к свойствам аккаунта, к созданию и обновлению списка закрытых позиций этого аккаунта и возвращает списки закрытых позиций по различным критериям выборки.
  • Класс коллекция исторических позиций. Содержит список объектов позиций, даёт доступ к свойствам закрытых позиций, к созданию и обновлению списка позиций. Возвращает список закрытых позиций.
  • Класс объекта позиции. Хранит свойства закрытой позиции и предоставляет к ним доступ. В объекте организован функционал для сравнения двух объектов по различным свойствам, что даёт возможность для создания списков позиций по различным критериям выборки. Содержит список сделок этой позиции и предоставляет к ним доступ.
  • Класс объекта сделки. Хранит свойства одной сделки и предоставляет к ним доступ. В объекте организован функционал для сравнения двух объектов по различным свойствам, что даёт возможность для создания списков сделок по различным критериям выборки.


Концепцию воссоздания закрытой позиции из списка исторических сделок мы рассматривали в статье "Как просматривать сделки прямо на графике и не утонуть в торговой истории". По списку сделок определяется принадлежность каждой сделки к какой-либо позиции по идентификатору позиции (PositionID), записанному в свойствах сделки. Создаётся объект позиции, в который найденные сделки помещаются в список сделок. Здесь будем делать таким же образом. Но для организации построения объектов сделок и позиций будем использовать совсем иную, давно проверенную концепцию, где у каждого объекта есть одинаковые по реализации методы доступа к свойствам для их установки и получения. Такая концепция позволяет создавать объекты в едином общем ключе, хранить их в списках, фильтровать и сортировать по любому из свойств объектов и получать новые списки в разрезе указанного свойства.

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

  1. структура свойств объектов "(Часть I): Концепция, организация данных, первые результаты"
  2. структура списков объектов "(Часть II): Коллекция исторических ордеров и сделок" и
  3. методы фильтрации объектов в списках по свойствам "(Часть III): Коллекция рыночных ордеров и позиций, поиск и фильтрация"

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

Если вкратце, то у каждого объекта, создаваемого в проекте, будет набор собственных свойств, как, впрочем, и у любого объекта или сущности в MQL5. Только в MQL5 для получения свойств существуют стандартные функции, а у объектов проекта это будут методы для получения целочисленных, вещественных и строковых свойств, прописанные прямо в классе каждого объекта. А далее, все эти объекты будут храниться в списках — динамических массивах указателей на объекты CObject Стандартной Библиотеки. И именно классы Стандартной Библиотеки позволяют нам с минимальными затратами создавать сложные проекты. В данном случае — базу данных по закрытым позициям всех аккаунтов, на которых велась торговля, с возможностью получения списков объектов, отсортированных и выбранных по любому требуемому свойству.

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

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


    Класс сделки

    В каталоге терминала \MQL5\Services\ создадим новую папку AccountReporter\, а в ней новый файл Deal.mqh класса CDeal.
    Класс должен быть унаследован от базового класса Стандартной Библиотеки CObject, а его файл подключен к вновь создаваемому классу:

    //+------------------------------------------------------------------+
    //|                                                         Deal.mqh |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #include <Object.mqh>
    
    //+------------------------------------------------------------------+
    //| Класс сделки                                                     |
    //+------------------------------------------------------------------+
    class CDeal : public CObject
      {
      }
    


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

    //+------------------------------------------------------------------+
    //|                                                         Deal.mqh |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #include <Object.mqh>
    
    //--- Перечисление целочисленных свойств сделки
    enum ENUM_DEAL_PROPERTY_INT
      {
       DEAL_PROP_TICKET = 0,               // Тикет сделки
       DEAL_PROP_ORDER,                    // Ордер, на основание которого выполнена сделка
       DEAL_PROP_TIME,                     // Время совершения сделки
       DEAL_PROP_TIME_MSC,                 // Время совершения сделки в миллисекундах
       DEAL_PROP_TYPE,                     // Тип сделки
       DEAL_PROP_ENTRY,                    // Направление сделки
       DEAL_PROP_MAGIC,                    // Magic number сделки
       DEAL_PROP_REASON,                   // Причина или источник проведения сделки
       DEAL_PROP_POSITION_ID,              // Идентификатор позиции
       DEAL_PROP_SPREAD,                   // Spread при совершении сделки
      };
      
    //--- Перечисление вещественных свойств сделки
    enum ENUM_DEAL_PROPERTY_DBL
      {
       DEAL_PROP_VOLUME = DEAL_PROP_SPREAD+1,// Объем сделки
       DEAL_PROP_PRICE,                    // Цена сделки
       DEAL_PROP_COMMISSION,               // Комиссия
       DEAL_PROP_SWAP,                     // Накопленный своп при закрытии
       DEAL_PROP_PROFIT,                   // Финансовый результат сделки
       DEAL_PROP_FEE,                      // Оплата за проведение сделки
       DEAL_PROP_SL,                       // Уровень Stop Loss
       DEAL_PROP_TP,                       // Уровень Take Profit
      };
      
    //--- Перечисление строковых свойств сделки
    enum ENUM_DEAL_PROPERTY_STR
      {
       DEAL_PROP_SYMBOL = DEAL_PROP_TP+1,  // Символ, по которому произведена сделка
       DEAL_PROP_COMMENT,                  // Комментарий к сделке
       DEAL_PROP_EXTERNAL_ID,              // Идентификатор сделки во внешней торговой системе 
      };
      
    //+------------------------------------------------------------------+
    //| Класс сделки                                                     |
    //+------------------------------------------------------------------+
    class CDeal : public CObject
      {
    private:
       MqlTick           m_tick;                                      // Структура тика сделки
       long              m_lprop[DEAL_PROP_SPREAD+1];                 // Массив для хранения целочисленных свойств
       double            m_dprop[DEAL_PROP_TP-DEAL_PROP_SPREAD];      // Массив для хранения вещественных свойств
       string            m_sprop[DEAL_PROP_EXTERNAL_ID-DEAL_PROP_TP]; // Массив для хранения строковых свойств
    
    //--- Возвращает индекс массива, по которому фактически расположено (1) double-свойство и (2) string-свойство сделки
       int               IndexProp(ENUM_DEAL_PROPERTY_DBL property)   const { return(int)property-DEAL_PROP_SPREAD-1; }
       int               IndexProp(ENUM_DEAL_PROPERTY_STR property)   const { return(int)property-DEAL_PROP_TP-1;     }
       
    //--- Получает (1) тик сделки, (2) спред минутного бара сделки
       bool              GetDealTick(const int amount=20);
       int               GetSpreadM1(void);
    
    //--- Возвращает время с миллисекундами
       string            TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const;
       
    protected:
    //--- Дополнительные свойства
       int               m_digits;                                    // Digits символа
       double            m_point;                                     // Point символа
       double            m_bid;                                       // Bid при совершении сделки
       double            m_ask;                                       // Ask при совершении сделки
       
    public:
    //--- Установка свойств
    //--- Устанавливает (1) целочисленное, (2) вещественное и (3) строковое свойство сделки
       void              SetProperty(ENUM_DEAL_PROPERTY_INT property,long   value){ this.m_lprop[property]=value;                 }
       void              SetProperty(ENUM_DEAL_PROPERTY_DBL property,double value){ this.m_dprop[this.IndexProp(property)]=value; }
       void              SetProperty(ENUM_DEAL_PROPERTY_STR property,string value){ this.m_sprop[this.IndexProp(property)]=value; }
    
    //--- Целочисленные свойства
       void              SetTicket(const long ticket)              { this.SetProperty(DEAL_PROP_TICKET, ticket);                  }  // Тикет
       void              SetOrder(const long order)                { this.SetProperty(DEAL_PROP_ORDER, order);                    }  // Ордер
       void              SetTime(const datetime time)              { this.SetProperty(DEAL_PROP_TIME, time);                      }  // Время
       void              SetTimeMsc(const long value)              { this.SetProperty(DEAL_PROP_TIME_MSC, value);                 }  // Время в миллисекундах
       void              SetTypeDeal(const ENUM_DEAL_TYPE type)    { this.SetProperty(DEAL_PROP_TYPE, type);                      }  // Тип
       void              SetEntry(const ENUM_DEAL_ENTRY entry)     { this.SetProperty(DEAL_PROP_ENTRY, entry);                    }  // Направление
       void              SetMagic(const long magic)                { this.SetProperty(DEAL_PROP_MAGIC, magic);                    }  // Magic number
       void              SetReason(const ENUM_DEAL_REASON reason)  { this.SetProperty(DEAL_PROP_REASON, reason);                  }  // Причина или источник проведения сделки
       void              SetPositionID(const long id)              { this.SetProperty(DEAL_PROP_POSITION_ID, id);                 }  // Идентификатор позиции
    
    //--- Вещественные свойства
       void              SetVolume(const double volume)            { this.SetProperty(DEAL_PROP_VOLUME, volume);                  }  // Объем
       void              SetPrice(const double price)              { this.SetProperty(DEAL_PROP_PRICE, price);                    }  // Цена
       void              SetCommission(const double value)         { this.SetProperty(DEAL_PROP_COMMISSION, value);               }  // Комиссия
       void              SetSwap(const double value)               { this.SetProperty(DEAL_PROP_SWAP, value);                     }  // Накопленный своп при закрытии
       void              SetProfit(const double value)             { this.SetProperty(DEAL_PROP_PROFIT, value);                   }  // Финансовый результат
       void              SetFee(const double value)                { this.SetProperty(DEAL_PROP_FEE, value);                      }  // Оплата за проведение сделки
       void              SetSL(const double value)                 { this.SetProperty(DEAL_PROP_SL, value);                       }  // Уровень Stop Loss
       void              SetTP(const double value)                 { this.SetProperty(DEAL_PROP_TP, value);                       }  // Уровень Take Profit
    
    //--- Строковые свойства
       void              SetSymbol(const string symbol)            { this.SetProperty(DEAL_PROP_SYMBOL,symbol);                   }  // Имя символа
       void              SetComment(const string comment)          { this.SetProperty(DEAL_PROP_COMMENT,comment);                 }  // Комментарий
       void              SetExternalID(const string ext_id)        { this.SetProperty(DEAL_PROP_EXTERNAL_ID,ext_id);              }  // Идентификатор сделки во внешней торговой системе
    
    //--- Получение свойств
    //--- Возвращает из массива свойств (1) целочисленное, (2) вещественное и (3) строковое свойство сделки
       long              GetProperty(ENUM_DEAL_PROPERTY_INT property) const { return this.m_lprop[property];                      }
       double            GetProperty(ENUM_DEAL_PROPERTY_DBL property) const { return this.m_dprop[this.IndexProp(property)];      }
       string            GetProperty(ENUM_DEAL_PROPERTY_STR property) const { return this.m_sprop[this.IndexProp(property)];      }
    
    //--- Целочисленные свойства
       long              Ticket(void)                        const { return this.GetProperty(DEAL_PROP_TICKET);                   }  // Тикет
       long              Order(void)                         const { return this.GetProperty(DEAL_PROP_ORDER);                    }  // Ордер
       datetime          Time(void)                          const { return (datetime)this.GetProperty(DEAL_PROP_TIME);           }  // Время
       long              TimeMsc(void)                       const { return this.GetProperty(DEAL_PROP_TIME_MSC);                 }  // Время в миллисекундах
       ENUM_DEAL_TYPE    TypeDeal(void)                      const { return (ENUM_DEAL_TYPE)this.GetProperty(DEAL_PROP_TYPE);     }  // Тип
       ENUM_DEAL_ENTRY   Entry(void)                         const { return (ENUM_DEAL_ENTRY)this.GetProperty(DEAL_PROP_ENTRY);   }  // Направление
       long              Magic(void)                         const { return this.GetProperty(DEAL_PROP_MAGIC);                    }  // Magic number
       ENUM_DEAL_REASON  Reason(void)                        const { return (ENUM_DEAL_REASON)this.GetProperty(DEAL_PROP_REASON); }  // Причина или источник проведения сделки
       long              PositionID(void)                    const { return this.GetProperty(DEAL_PROP_POSITION_ID);              }  // Идентификатор позиции
       
    //--- Вещественные свойства
       double            Volume(void)                        const { return this.GetProperty(DEAL_PROP_VOLUME);                   }  // Объем
       double            Price(void)                         const { return this.GetProperty(DEAL_PROP_PRICE);                    }  // Цена
       double            Commission(void)                    const { return this.GetProperty(DEAL_PROP_COMMISSION);               }  // Комиссия
       double            Swap(void)                          const { return this.GetProperty(DEAL_PROP_SWAP);                     }  // Накопленный своп при закрытии
       double            Profit(void)                        const { return this.GetProperty(DEAL_PROP_PROFIT);                   }  // Финансовый результат
       double            Fee(void)                           const { return this.GetProperty(DEAL_PROP_FEE);                      }  // Оплата за проведение сделки
       double            SL(void)                            const { return this.GetProperty(DEAL_PROP_SL);                       }  // Уровень Stop Loss
       double            TP(void)                            const { return this.GetProperty(DEAL_PROP_TP);                       }  // Уровень Take Profit
       
    //--- Строковые свойства
       string            Symbol(void)                        const { return this.GetProperty(DEAL_PROP_SYMBOL);                   }  // Имя символа
       string            Comment(void)                       const { return this.GetProperty(DEAL_PROP_COMMENT);                  }  // Комментарий
       string            ExternalID(void)                    const { return this.GetProperty(DEAL_PROP_EXTERNAL_ID);              }  // Идентификатор сделки во внешней торговой системе
       
    //--- Дополнительные свойства
       double            Bid(void)                           const { return this.m_bid;                                           }  // Bid при совершении сделки
       double            Ask(void)                           const { return this.m_ask;                                           }  // Ask при совершении сделки
       int               Spread(void)                        const { return (int)this.GetProperty(DEAL_PROP_SPREAD);              }  // Spread при совершении сделки
       
    //--- Возвращает описание (1) типа сделки, (2) способа изменения позиции, (3) причины проведения сделки
       string            TypeDescription(void)   const;
       string            EntryDescription(void)  const;
       string            ReasonDescription(void) const;
       
    //--- Возвращает описание сделки
       string            Description(void);
    
    //--- Распечатывает в журнал свойства сделки
       void              Print(void);
       
    //--- Сравнивает два объекта между собой по указанному в mode свойству
       virtual int       Compare(const CObject *node, const int mode=0) const;
       
    //--- Конструкторы/деструктор
                         CDeal(void){}
                         CDeal(const ulong ticket);
                        ~CDeal();
      };
    

    Рассмотрим реализацию методов класса.

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

    //+------------------------------------------------------------------+
    //| Конструктор                                                      |
    //+------------------------------------------------------------------+
    CDeal::CDeal(const ulong ticket)
      {
    //--- Сохранение свойств
    //--- Целочисленные свойства
       this.SetTicket((long)ticket);                                                    // Тикет сделки
       this.SetOrder(::HistoryDealGetInteger(ticket, DEAL_ORDER));                      // Ордер
       this.SetTime((datetime)::HistoryDealGetInteger(ticket, DEAL_TIME));              // Время совершения сделки
       this.SetTimeMsc(::HistoryDealGetInteger(ticket, DEAL_TIME_MSC));                 // Время совершения сделки в миллисекундах
       this.SetTypeDeal((ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE));    // Тип
       this.SetEntry((ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY));     // Направление
       this.SetMagic(::HistoryDealGetInteger(ticket, DEAL_MAGIC));                      // Magic number
       this.SetReason((ENUM_DEAL_REASON)::HistoryDealGetInteger(ticket, DEAL_REASON));  // Причина или источник проведения сделки
       this.SetPositionID(::HistoryDealGetInteger(ticket, DEAL_POSITION_ID));           // Идентификатор позиции
       
    //--- Вещественные свойства
       this.SetVolume(::HistoryDealGetDouble(ticket, DEAL_VOLUME));                     // Объем
       this.SetPrice(::HistoryDealGetDouble(ticket, DEAL_PRICE));                       // Цена
       this.SetCommission(::HistoryDealGetDouble(ticket, DEAL_COMMISSION));             // Комиссия
       this.SetSwap(::HistoryDealGetDouble(ticket, DEAL_SWAP));                         // Накопленный своп при закрытии
       this.SetProfit(::HistoryDealGetDouble(ticket, DEAL_PROFIT));                     // Финансовый результат
       this.SetFee(::HistoryDealGetDouble(ticket, DEAL_FEE));                           // Оплата за проведение сделки
       this.SetSL(::HistoryDealGetDouble(ticket, DEAL_SL));                             // Уровень Stop Loss
       this.SetTP(::HistoryDealGetDouble(ticket, DEAL_TP));                             // Уровень Take Profit
    
    //--- Строковые свойства
       this.SetSymbol(::HistoryDealGetString(ticket, DEAL_SYMBOL));                     // Имя символа
       this.SetComment(::HistoryDealGetString(ticket, DEAL_COMMENT));                   // Комментарий
       this.SetExternalID(::HistoryDealGetString(ticket, DEAL_EXTERNAL_ID));            // Идентификатор сделки во внешней торговой системе
    
    //--- Дополнительные параметры
       this.m_digits = (int)::SymbolInfoInteger(this.Symbol(), SYMBOL_DIGITS);
       this.m_point  = ::SymbolInfoDouble(this.Symbol(), SYMBOL_POINT);
       
    //--- Параметры для расчёта спреда
       this.m_bid = 0;
       this.m_ask = 0;
       this.SetProperty(DEAL_PROP_SPREAD, 0);
       
    //--- Если исторический тик и значение Point символа удалось получить
       if(this.GetDealTick() && this.m_point!=0)
         {
          //--- запишем значения цен Bid и Ask и рассчитаем и сохраним значение спреда
          this.m_bid=this.m_tick.bid;
          this.m_ask=this.m_tick.ask;
          int  spread=(int)::fabs((this.m_ask-this.m_bid)/this.m_point);
          this.SetProperty(DEAL_PROP_SPREAD, spread);
         }
    //--- Если исторический тик получить не удалось, возьмём значение спреда минутного бара, на котором была сделка
       else
          this.SetProperty(DEAL_PROP_SPREAD, this.GetSpreadM1());
      }
    

    Сохраняем в массивах свойств класса свойства сделки, Digits и Point символа, по которому была проведена сделка — для осуществления расчётов и вывода информации о сделке. Далее получаем исторический тик по времени проведения сделки. Тем самым мы даём доступ к ценам Bid и Ask в момент совершения сделки, а отсюда и возможность расчёта спреда.


    Метод, сравнивающий два объекта между собой по указанному свойству:

    //+------------------------------------------------------------------+
    //| Сравнивает два объекта между собой по указанному свойству        |
    //+------------------------------------------------------------------+
    int CDeal::Compare(const CObject *node,const int mode=0) const
      {
       const CDeal * obj = node;
       switch(mode)
         {
          case DEAL_PROP_TICKET      :  return(this.Ticket() > obj.Ticket()          ?  1  :  this.Ticket() < obj.Ticket()           ? -1  :  0);
          case DEAL_PROP_ORDER       :  return(this.Order() > obj.Order()            ?  1  :  this.Order() < obj.Order()             ? -1  :  0);
          case DEAL_PROP_TIME        :  return(this.Time() > obj.Time()              ?  1  :  this.Time() < obj.Time()               ? -1  :  0);
          case DEAL_PROP_TIME_MSC    :  return(this.TimeMsc() > obj.TimeMsc()        ?  1  :  this.TimeMsc() < obj.TimeMsc()         ? -1  :  0);
          case DEAL_PROP_TYPE        :  return(this.TypeDeal() > obj.TypeDeal()      ?  1  :  this.TypeDeal() < obj.TypeDeal()       ? -1  :  0);
          case DEAL_PROP_ENTRY       :  return(this.Entry() > obj.Entry()            ?  1  :  this.Entry() < obj.Entry()             ? -1  :  0);
          case DEAL_PROP_MAGIC       :  return(this.Magic() > obj.Magic()            ?  1  :  this.Magic() < obj.Magic()             ? -1  :  0);
          case DEAL_PROP_REASON      :  return(this.Reason() > obj.Reason()          ?  1  :  this.Reason() < obj.Reason()           ? -1  :  0);
          case DEAL_PROP_POSITION_ID :  return(this.PositionID() > obj.PositionID()  ?  1  :  this.PositionID() < obj.PositionID()   ? -1  :  0);
          case DEAL_PROP_SPREAD      :  return(this.Spread() > obj.Spread()          ?  1  :  this.Spread() < obj.Spread()           ? -1  :  0);
          case DEAL_PROP_VOLUME      :  return(this.Volume() > obj.Volume()          ?  1  :  this.Volume() < obj.Volume()           ? -1  :  0);
          case DEAL_PROP_PRICE       :  return(this.Price() > obj.Price()            ?  1  :  this.Price() < obj.Price()             ? -1  :  0);
          case DEAL_PROP_COMMISSION  :  return(this.Commission() > obj.Commission()  ?  1  :  this.Commission() < obj.Commission()   ? -1  :  0);
          case DEAL_PROP_SWAP        :  return(this.Swap() > obj.Swap()              ?  1  :  this.Swap() < obj.Swap()               ? -1  :  0);
          case DEAL_PROP_PROFIT      :  return(this.Profit() > obj.Profit()          ?  1  :  this.Profit() < obj.Profit()           ? -1  :  0);
          case DEAL_PROP_FEE         :  return(this.Fee() > obj.Fee()                ?  1  :  this.Fee() < obj.Fee()                 ? -1  :  0);
          case DEAL_PROP_SL          :  return(this.SL() > obj.SL()                  ?  1  :  this.SL() < obj.SL()                   ? -1  :  0);
          case DEAL_PROP_TP          :  return(this.TP() > obj.TP()                  ?  1  :  this.TP() < obj.TP()                   ? -1  :  0);
          case DEAL_PROP_SYMBOL      :  return(this.Symbol() > obj.Symbol()          ?  1  :  this.Symbol() < obj.Symbol()           ? -1  :  0);
          case DEAL_PROP_COMMENT     :  return(this.Comment() > obj.Comment()        ?  1  :  this.Comment() < obj.Comment()         ? -1  :  0);
          case DEAL_PROP_EXTERNAL_ID :  return(this.ExternalID() > obj.ExternalID()  ?  1  :  this.ExternalID() < obj.ExternalID()   ? -1  :  0);
          default                    :  return(-1);
         }
      }
    

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


    Метод, возвращающий описание типа сделки:

    //+------------------------------------------------------------------+
    //| Возвращает описание типа сделки                                  |
    //+------------------------------------------------------------------+
    string CDeal::TypeDescription(void) const
      {
       switch(this.TypeDeal())
         {
          case DEAL_TYPE_BUY                     :  return "Buy";
          case DEAL_TYPE_SELL                    :  return "Sell";
          case DEAL_TYPE_BALANCE                 :  return "Balance";
          case DEAL_TYPE_CREDIT                  :  return "Credit";
          case DEAL_TYPE_CHARGE                  :  return "Additional charge";
          case DEAL_TYPE_CORRECTION              :  return "Correction";
          case DEAL_TYPE_BONUS                   :  return "Bonus";
          case DEAL_TYPE_COMMISSION              :  return "Additional commission";
          case DEAL_TYPE_COMMISSION_DAILY        :  return "Daily commission";
          case DEAL_TYPE_COMMISSION_MONTHLY      :  return "Monthly commission";
          case DEAL_TYPE_COMMISSION_AGENT_DAILY  :  return "Daily agent commission";
          case DEAL_TYPE_COMMISSION_AGENT_MONTHLY:  return "Monthly agent commission";
          case DEAL_TYPE_INTEREST                :  return "Interest rate";
          case DEAL_TYPE_BUY_CANCELED            :  return "Canceled buy deal";
          case DEAL_TYPE_SELL_CANCELED           :  return "Canceled sell deal";
          case DEAL_DIVIDEND                     :  return "Dividend operations";
          case DEAL_DIVIDEND_FRANKED             :  return "Franked (non-taxable) dividend operations";
          case DEAL_TAX                          :  return "Tax charges";
          default                                :  return "Unknown: "+(string)this.TypeDeal();
         }
      }
    

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


    Метод, возвращающий описание способа изменения позиции:

    //+------------------------------------------------------------------+
    //| Возвращает описание способа изменения позиции                    |
    //+------------------------------------------------------------------+
    string CDeal::EntryDescription(void) const
      {
       switch(this.Entry())
         {
          case DEAL_ENTRY_IN      :  return "Entry In";
          case DEAL_ENTRY_OUT     :  return "Entry Out";
          case DEAL_ENTRY_INOUT   :  return "Reverse";
          case DEAL_ENTRY_OUT_BY  :  return "Close a position by an opposite one";
          default                 :  return "Unknown: "+(string)this.Entry();
         }
      }
    


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

    //+------------------------------------------------------------------+
    //| Возвращает описание причины проведения сделки                    |
    //+------------------------------------------------------------------+
    string CDeal::ReasonDescription(void) const
      {
       switch(this.Reason())
         {
          case DEAL_REASON_CLIENT          :  return "Terminal";
          case DEAL_REASON_MOBILE          :  return "Mobile";
          case DEAL_REASON_WEB             :  return "Web";
          case DEAL_REASON_EXPERT          :  return "EA";
          case DEAL_REASON_SL              :  return "SL";
          case DEAL_REASON_TP              :  return "TP";
          case DEAL_REASON_SO              :  return "SO";
          case DEAL_REASON_ROLLOVER        :  return "Rollover";
          case DEAL_REASON_VMARGIN         :  return "Var. Margin";
          case DEAL_REASON_SPLIT           :  return "Split";
          case DEAL_REASON_CORPORATE_ACTION:  return "Corp. Action";
          default                          :  return "Unknown reason "+(string)this.Reason();
         }
      }
    


    Метод, возвращающий описание сделки:

    //+------------------------------------------------------------------+
    //| Возвращает описание сделки                                       |
    //+------------------------------------------------------------------+
    string CDeal::Description(void)
      {
       return(::StringFormat("Deal: %-9s %.2f %-4s #%I64d at %s", this.EntryDescription(), this.Volume(), this.TypeDescription(), this.Ticket(), this.TimeMscToString(this.TimeMsc())));
      }
    


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

    //+------------------------------------------------------------------+
    //| Распечатывает в журнал свойства сделки                           |
    //+------------------------------------------------------------------+
    void CDeal::Print(void)
      {
       ::Print(this.Description());
      }
    


    Метод, возвращающий время с миллисекундами:

    //+------------------------------------------------------------------+
    //| Возвращает время с миллисекундами                                |
    //+------------------------------------------------------------------+
    string CDeal::TimeMscToString(const long time_msc, int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const
      {
       return(::TimeToString(time_msc/1000, flags) + "." + ::IntegerToString(time_msc %1000, 3, '0'));
      }
    

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

    Метод, получающий тик сделки:

    //+------------------------------------------------------------------+
    //| Получает тик сделки                                              |
    //| https://www.mql5.com/ru/forum/42122/page47#comment_37205238      |
    //+------------------------------------------------------------------+
    bool CDeal::GetDealTick(const int amount=20)
      {
       MqlTick ticks[];        // Сюда будем получать тики
       int attempts = amount;  // Количество попыток получения тиков
       int offset = 500;       // Начальное смещение времени для попытки
       int copied = 0;         // Количество скопированных тиков
       
    //--- До тех пор, пока не скопирован тик и не закончилось количество попыток копирования
    //--- пытаемся получить тик, на каждой итерации увеличивая вдвое начальное смещение времени (расширяем диапазон времени "from_msc")
       while(!::IsStopped() && (copied<=0) && (attempts--)!=0)
          copied = ::CopyTicksRange(this.Symbol(), ticks, COPY_TICKS_INFO, this.TimeMsc()-(offset <<=1), this.TimeMsc());
        
    //--- Если тик скопировать удалось (он последний в массиве тиков) - записываем его в переменную m_tick
       if(copied>0)
          this.m_tick=ticks[copied-1];
    
    //--- Возвращаем флаг того, что тик скопирован
       return(copied>0);
      }
    

    Логика метода расписана в комментариях к коду. После получения тика, из него берутся значения цен Ask и Bid и рассчитывается размер спреда как (Ask - Bid) / Point.

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

    //+------------------------------------------------------------------+
    //| Получает спред минутного бара сделки                             |
    //+------------------------------------------------------------------+
    int CDeal::GetSpreadM1(void)
      {
       int array[1]={};
       int bar=::iBarShift(this.Symbol(), PERIOD_M1, this.Time());
       if(bar==WRONG_VALUE)
          return 0;
       return(::CopySpread(this.Symbol(), PERIOD_M1, bar, 1, array)==1 ? array[0] : 0);
      }
    

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


    Класс исторической позиции

    В папке терминала \MQL5\Services\AccountReporter\ создадим новый файл Position.mqh класса CPosition.

    Класс должен быть унаследован от класса базового объекта Стандартной Библиотеки CObject:

    //+------------------------------------------------------------------+
    //|                                                     Position.mqh |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
      
    //+------------------------------------------------------------------+
    //| Класс позиции                                                    |
    //+------------------------------------------------------------------+
    class CPosition : public CObject
      {
      }
    


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

    //+------------------------------------------------------------------+
    //|                                                     Position.mqh |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #include "Deal.mqh"
    #include <Arrays\ArrayObj.mqh>
    
    //+------------------------------------------------------------------+
    //| Класс позиции                                                    |
    //+------------------------------------------------------------------+
    class CPosition : public CObject
      {
      }
    


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

    //+------------------------------------------------------------------+
    //|                                                     Position.mqh |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #include "Deal.mqh"
    #include <Arrays\ArrayObj.mqh>
    
    //--- Перечисление целочисленных свойств позиции
    enum ENUM_POSITION_PROPERTY_INT
      {
       POSITION_PROP_TICKET = 0,        // Тикет позиции
       POSITION_PROP_TIME,              // Время открытия позиции
       POSITION_PROP_TIME_MSC,          // Время открытия позиции в миллисекундах
       POSITION_PROP_TIME_UPDATE,       // Время изменения позиции
       POSITION_PROP_TIME_UPDATE_MSC,   // Время изменения позиции в миллисекундах
       POSITION_PROP_TYPE,              // Тип позиции
       POSITION_PROP_MAGIC,             // Magic number позиции
       POSITION_PROP_IDENTIFIER,        // Идентификатор позиции
       POSITION_PROP_REASON,            // Причина открытия позиции
       POSITION_PROP_ACCOUNT_LOGIN,     // Номер счёта
       POSITION_PROP_TIME_CLOSE,        // Время закрытия позиции
       POSITION_PROP_TIME_CLOSE_MSC,    // Время закрытия позиции в миллисекундах
      };
      
    //--- Перечисление вещественных свойств позиции
    enum ENUM_POSITION_PROPERTY_DBL
      {
       POSITION_PROP_VOLUME = POSITION_PROP_TIME_CLOSE_MSC+1,// Объем позиции
       POSITION_PROP_PRICE_OPEN,        // Цена позиции
       POSITION_PROP_SL,                // Stop Loss для открытой позиции
       POSITION_PROP_TP,                // Take Profit для открытой позиции
       POSITION_PROP_PRICE_CURRENT,     // Текущая цена по символу
       POSITION_PROP_SWAP,              // Накопленный своп
       POSITION_PROP_PROFIT,            // Текущая прибыль
       POSITION_PROP_CONTRACT_SIZE,     // Размер торгового контракта символа
       POSITION_PROP_PRICE_CLOSE,       // Цена закрытия позиции
       POSITION_PROP_COMMISSIONS,       // Накопленная комиссия
       POSITION_PROP_FEE,               // Накопленная оплата за сделки
      };
    
    //--- Перечисление строковых свойств позиции
    enum ENUM_POSITION_PROPERTY_STR
      {
       POSITION_PROP_SYMBOL = POSITION_PROP_FEE+1,// Символ, по которому открыта позиция
       POSITION_PROP_COMMENT,           // Комментарий к позиции
       POSITION_PROP_EXTERNAL_ID,       // Идентификатор позиции во внешней системе
       POSITION_PROP_CURRENCY_PROFIT,   // Валюта прибыли символа позиции
       POSITION_PROP_ACCOUNT_CURRENCY,  // Валюта депозита аккаунта
       POSITION_PROP_ACCOUNT_SERVER,    // Имя сервера
      };
      
    //+------------------------------------------------------------------+
    //| Класс позиции                                                    |
    //+------------------------------------------------------------------+
    class CPosition : public CObject
      {
    private:
       long              m_lprop[POSITION_PROP_TIME_CLOSE_MSC+1];                    // Массив для хранения целочисленных свойств
       double            m_dprop[POSITION_PROP_FEE-POSITION_PROP_TIME_CLOSE_MSC];    // Массив для хранения вещественных свойств
       string            m_sprop[POSITION_PROP_ACCOUNT_SERVER-POSITION_PROP_FEE];    // Массив для хранения строковых свойств
    
    //--- Возвращает индекс массива, по которому фактически расположено (1) double-свойство и (2) string-свойство ордера
       int               IndexProp(ENUM_POSITION_PROPERTY_DBL property)   const { return(int)property-POSITION_PROP_TIME_CLOSE_MSC-1;}
       int               IndexProp(ENUM_POSITION_PROPERTY_STR property)   const { return(int)property-POSITION_PROP_FEE-1;           }
       
    protected:
       CArrayObj         m_list_deals;        // Список сделок позиции
       CDeal             m_temp_deal;         // Временный объект-сделка для поиска по свойству в списке
       
    //--- Возвращает время с миллисекундами
       string            TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const;
    
    //--- Дополнительные свойства
       int               m_profit_pt;         // Прибыль в пунктах
       int               m_digits;            // Digits символа
       double            m_point;             // Значение одного пункта символа
       double            m_tick_value;        // Рассчитанная стоимость тика
       
    //--- Возвращает указатель на сделку (1) открытия, (2) закрытия
       CDeal            *GetDealIn(void)   const;
       CDeal            *GetDealOut(void)  const;
       
    public:
    //--- Возвращает список сделок
       CArrayObj        *GetListDeals(void)                              { return(&this.m_list_deals);                                  }
    
    //--- Установка свойств
    //--- Устанавливает (1) целочисленное, (2) вещественное и (3) строковое свойство
       void              SetProperty(ENUM_POSITION_PROPERTY_INT property,long   value)  { this.m_lprop[property]=value;                 }
       void              SetProperty(ENUM_POSITION_PROPERTY_DBL property,double value)  { this.m_dprop[this.IndexProp(property)]=value; }
       void              SetProperty(ENUM_POSITION_PROPERTY_STR property,string value)  { this.m_sprop[this.IndexProp(property)]=value; }
    
    //--- Целочисленные свойства
       void              SetTicket(const long ticket)                    { this.SetProperty(POSITION_PROP_TICKET, ticket);              }  // Тикет позиции
       void              SetTime(const datetime time)                    { this.SetProperty(POSITION_PROP_TIME, time);                  }  // Время открытия позиции
       void              SetTimeMsc(const long value)                    { this.SetProperty(POSITION_PROP_TIME_MSC, value);             }  // Время открытия позиции в миллисекундах с 01.01.1970 
       void              SetTimeUpdate(const datetime time)              { this.SetProperty(POSITION_PROP_TIME_UPDATE, time);           }  // Время изменения позиции
       void              SetTimeUpdateMsc(const long value)              { this.SetProperty(POSITION_PROP_TIME_UPDATE_MSC, value);      }  // Время изменения позиции в миллисекундах с 01.01.1970
       void              SetTypePosition(const ENUM_POSITION_TYPE type)  { this.SetProperty(POSITION_PROP_TYPE, type);                  }  // Тип позиции
       void              SetMagic(const long magic)                      { this.SetProperty(POSITION_PROP_MAGIC, magic);                }  // Magic number для позиции (смотри ORDER_MAGIC)
       void              SetID(const long id)                            { this.SetProperty(POSITION_PROP_IDENTIFIER, id);              }  // Идентификатор позиции
       void              SetReason(const ENUM_POSITION_REASON reason)    { this.SetProperty(POSITION_PROP_REASON, reason);              }  // Причина открытия позиции
       void              SetTimeClose(const datetime time)               { this.SetProperty(POSITION_PROP_TIME_CLOSE, time);            }  // Время закрытия
       void              SetTimeCloseMsc(const long value)               { this.SetProperty(POSITION_PROP_TIME_CLOSE_MSC, value);       }  // Время закрытия в миллисекундах
       void              SetAccountLogin(const long login)               { this.SetProperty(POSITION_PROP_ACCOUNT_LOGIN, login);        }  // Номер счёта
       
    //--- Вещественные свойства
       void              SetVolume(const double volume)                  { this.SetProperty(POSITION_PROP_VOLUME, volume);              }  // Объем позиции
       void              SetPriceOpen(const double price)                { this.SetProperty(POSITION_PROP_PRICE_OPEN, price);           }  // Цена позиции
       void              SetSL(const double value)                       { this.SetProperty(POSITION_PROP_SL, value);                   }  // Уровень Stop Loss для открытой позиции
       void              SetTP(const double value)                       { this.SetProperty(POSITION_PROP_TP, value);                   }  // Уровень Take Profit для открытой позиции
       void              SetPriceCurrent(const double price)             { this.SetProperty(POSITION_PROP_PRICE_CURRENT, price);        }  // Текущая цена по символу
       void              SetSwap(const double value)                     { this.SetProperty(POSITION_PROP_SWAP, value);                 }  // Накопленный своп
       void              SetProfit(const double value)                   { this.SetProperty(POSITION_PROP_PROFIT, value);               }  // Текущая прибыль
       void              SetPriceClose(const double price)               { this.SetProperty(POSITION_PROP_PRICE_CLOSE, price);          }  // Цена закрытия
       void              SetContractSize(const double value)             { this.SetProperty(POSITION_PROP_CONTRACT_SIZE, value);        }  // Размер торгового контракта символа
       void              SetCommissions(void);                                                                                             // Совокупная комиссия всех сделок
       void              SetFee(void);                                                                                                     // Совокупная оплату за проведение сделок
       
    //--- Строковые свойства
       void              SetSymbol(const string symbol)                  { this.SetProperty(POSITION_PROP_SYMBOL, symbol);              }  // Символ, по которому открыта позиция
       void              SetComment(const string comment)                { this.SetProperty(POSITION_PROP_COMMENT, comment);            }  // Комментарий к позиции
       void              SetExternalID(const string ext_id)              { this.SetProperty(POSITION_PROP_EXTERNAL_ID, ext_id);         }  // Идентификатор позиции во внешней системе (на бирже)
       void              SetAccountServer(const string server)           { this.SetProperty(POSITION_PROP_ACCOUNT_SERVER, server);      }  // Имя сервера
       void              SetAccountCurrency(const string currency)       { this.SetProperty(POSITION_PROP_ACCOUNT_CURRENCY, currency);  }  // Валюта депозита аккаунта
       void              SetCurrencyProfit(const string currency)        { this.SetProperty(POSITION_PROP_CURRENCY_PROFIT, currency);   }  // Валюта прибыли символа позиции
       
    //--- Получение свойств
    //--- Возвращает из массива свойств (1) целочисленное, (2) вещественное и (3) строковое свойство
       long              GetProperty(ENUM_POSITION_PROPERTY_INT property) const { return this.m_lprop[property];                        }
       double            GetProperty(ENUM_POSITION_PROPERTY_DBL property) const { return this.m_dprop[this.IndexProp(property)];        }
       string            GetProperty(ENUM_POSITION_PROPERTY_STR property) const { return this.m_sprop[this.IndexProp(property)];        }
    
    //--- Целочисленные свойства
       long              Ticket(void)                              const { return this.GetProperty(POSITION_PROP_TICKET);               }  // Тикет позиции
       datetime          Time(void)                                const { return (datetime)this.GetProperty(POSITION_PROP_TIME);       }  // Время открытия позиции
       long              TimeMsc(void)                             const { return this.GetProperty(POSITION_PROP_TIME_MSC);             }  // Время открытия позиции в миллисекундах с 01.01.1970 
       datetime          TimeUpdate(void)                          const { return (datetime)this.GetProperty(POSITION_PROP_TIME_UPDATE);}  // Время изменения позиции
       long              TimeUpdateMsc(void)                       const { return this.GetProperty(POSITION_PROP_TIME_UPDATE_MSC);      }  // Время изменения позиции в миллисекундах с 01.01.1970
       ENUM_POSITION_TYPE TypePosition(void)                       const { return (ENUM_POSITION_TYPE)this.GetProperty(POSITION_PROP_TYPE);}// Тип позиции
       long              Magic(void)                               const { return this.GetProperty(POSITION_PROP_MAGIC);                }  // Magic number для позиции (смотри ORDER_MAGIC)
       long              ID(void)                                  const { return this.GetProperty(POSITION_PROP_IDENTIFIER);           }  // Идентификатор позиции
       ENUM_POSITION_REASON Reason(void)                           const { return (ENUM_POSITION_REASON)this.GetProperty(POSITION_PROP_REASON);}// Причина открытия позиции
       datetime          TimeClose(void)                           const { return (datetime)this.GetProperty(POSITION_PROP_TIME_CLOSE); }  // Время закрытия
       long              TimeCloseMsc(void)                        const { return this.GetProperty(POSITION_PROP_TIME_CLOSE_MSC);       }  // Время закрытия в миллисекундах
       long              AccountLogin(void)                        const { return this.GetProperty(POSITION_PROP_ACCOUNT_LOGIN);        }  // Логин
       
    //--- Вещественные свойства
       double            Volume(void)                              const { return this.GetProperty(POSITION_PROP_VOLUME);               }  // Объем позиции
       double            PriceOpen(void)                           const { return this.GetProperty(POSITION_PROP_PRICE_OPEN);           }  // Цена позиции
       double            SL(void)                                  const { return this.GetProperty(POSITION_PROP_SL);                   }  // Уровень Stop Loss для открытой позиции
       double            TP(void)                                  const { return this.GetProperty(POSITION_PROP_TP);                   }  // Уровень Take Profit для открытой позиции
       double            PriceCurrent(void)                        const { return this.GetProperty(POSITION_PROP_PRICE_CURRENT);        }  // Текущая цена по символу
       double            Swap(void)                                const { return this.GetProperty(POSITION_PROP_SWAP);                 }  // Накопленный своп
       double            Profit(void)                              const { return this.GetProperty(POSITION_PROP_PROFIT);               }  // Текущая прибыль
       double            ContractSize(void)                        const { return this.GetProperty(POSITION_PROP_CONTRACT_SIZE);        }  // Размер торгового контракта символа
       double            PriceClose(void)                          const { return this.GetProperty(POSITION_PROP_PRICE_CLOSE);          }  // Цена закрытия
       double            Commissions(void)                         const { return this.GetProperty(POSITION_PROP_COMMISSIONS);          }  // Совокупная комиссия всех сделок
       double            Fee(void)                                 const { return this.GetProperty(POSITION_PROP_FEE);                  }  // Совокупная оплата за проведение сделок
       
    //--- Строковые свойства
       string            Symbol(void)                              const { return this.GetProperty(POSITION_PROP_SYMBOL);               }  // Символ, по которому открыта позиция
       string            Comment(void)                             const { return this.GetProperty(POSITION_PROP_COMMENT);              }  // Комментарий к позиции
       string            ExternalID(void)                          const { return this.GetProperty(POSITION_PROP_EXTERNAL_ID);          }  // Идентификатор позиции во внешней системе (на бирже)
       string            AccountServer(void)                       const { return this.GetProperty(POSITION_PROP_ACCOUNT_SERVER);       }  // Имя сервера
       string            AccountCurrency(void)                     const { return this.GetProperty(POSITION_PROP_ACCOUNT_CURRENCY);     }  // Валюта депозита аккаунта
       string            CurrencyProfit(void)                      const { return this.GetProperty(POSITION_PROP_CURRENCY_PROFIT);      }  // Валюта прибыли символа позиции
       
       
    //--- Дополнительные свойства
       ulong             DealIn(void)                              const;                                                                  // Тикет сделки открытия
       ulong             DealOut(void)                             const;                                                                  // Тикет сделки закрытия
       int               ProfitInPoints(void)                      const;                                                                  // Прибыль в пунктах
       int               SpreadIn(void)                            const;                                                                  // Спред при открытии
       int               SpreadOut(void)                           const;                                                                  // Спред при закрытии
       double            SpreadOutCost(void)                       const;                                                                  // Стоимость спреда при закрытии
       double            PriceOutAsk(void)                         const;                                                                  // Цена Ask при закрытии
       double            PriceOutBid(void)                         const;                                                                  // Цена Bid при закрытии
       
    //--- Добавляет сделку в список сделок, возвращает указатель
       CDeal            *DealAdd(const long ticket);
       
    //--- Возвращает описание типа позиции
       string            TypeDescription(void) const;
       
    //--- Возвращает описание времени и цены открытия позиции
       string            TimePriceCloseDescription(void);
    
    //--- Возвращает описание времени и цены закрытия позиции
       string            TimePriceOpenDescription(void);
       
    //--- Возвращает описание позиции
       string            Description(void);
    
    //--- Распечатывает в журнале свойства позиции и её сделок
       void              Print(void);
       
    //--- Сравнивает два объекта между собой по указанному в mode свойству
       virtual int       Compare(const CObject *node, const int mode=0) const;
       
    //--- Конструктор/деструктор
                         CPosition(const long position_id, const string symbol);
                         CPosition(void){}
                        ~CPosition();
      };
    

    Рассмотрим реализацию методов класса.

    В конструкторе класса устанавливаем идентификатор позиции и символ из параметров, переданных в метод, и записываем данные аккаунта и символа:

    //+------------------------------------------------------------------+
    //| Конструктор                                                      |
    //+------------------------------------------------------------------+
    CPosition::CPosition(const long position_id, const string symbol)
      {
       this.m_list_deals.Sort(DEAL_PROP_TIME_MSC);
       this.SetID(position_id);
       this.SetSymbol(symbol);
       this.SetAccountLogin(::AccountInfoInteger(ACCOUNT_LOGIN));
       this.SetAccountServer(::AccountInfoString(ACCOUNT_SERVER));
       this.SetAccountCurrency(::AccountInfoString(ACCOUNT_CURRENCY));
       this.SetCurrencyProfit(::SymbolInfoString(this.Symbol(),SYMBOL_CURRENCY_PROFIT));
       this.SetContractSize(::SymbolInfoDouble(this.Symbol(),SYMBOL_TRADE_CONTRACT_SIZE));
       this.m_digits     = (int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS);
       this.m_point      = ::SymbolInfoDouble(this.Symbol(),SYMBOL_POINT);
       this.m_tick_value = ::SymbolInfoDouble(this.Symbol(), SYMBOL_TRADE_TICK_VALUE);
      }
    


    В деструкторе класса очищаем список сделок позиции:

    //+------------------------------------------------------------------+
    //| Деструктор                                                       |
    //+------------------------------------------------------------------+
    CPosition::~CPosition()
      {
       this.m_list_deals.Clear();
      }
    


    Метод, сравнивающий два объекта между собой по указанному свойству:

    //+------------------------------------------------------------------+
    //| Сравнивает два объекта между собой по указанному свойству        |
    //+------------------------------------------------------------------+
    int CPosition::Compare(const CObject *node,const int mode=0) const
      {
       const CPosition *obj=node;
       switch(mode)
         {
          case POSITION_PROP_TICKET           :  return(this.Ticket() > obj.Ticket()                   ?  1  :  this.Ticket() < obj.Ticket()                    ? -1  :  0);
          case POSITION_PROP_TIME             :  return(this.Time() > obj.Time()                       ?  1  :  this.Time() < obj.Time()                        ? -1  :  0);
          case POSITION_PROP_TIME_MSC         :  return(this.TimeMsc() > obj.TimeMsc()                 ?  1  :  this.TimeMsc() < obj.TimeMsc()                  ? -1  :  0);
          case POSITION_PROP_TIME_UPDATE      :  return(this.TimeUpdate() > obj.TimeUpdate()           ?  1  :  this.TimeUpdate() < obj.TimeUpdate()            ? -1  :  0);
          case POSITION_PROP_TIME_UPDATE_MSC  :  return(this.TimeUpdateMsc() > obj.TimeUpdateMsc()     ?  1  :  this.TimeUpdateMsc() < obj.TimeUpdateMsc()      ? -1  :  0);
          case POSITION_PROP_TYPE             :  return(this.TypePosition() > obj.TypePosition()       ?  1  :  this.TypePosition() < obj.TypePosition()        ? -1  :  0);
          case POSITION_PROP_MAGIC            :  return(this.Magic() > obj.Magic()                     ?  1  :  this.Magic() < obj.Magic()                      ? -1  :  0);
          case POSITION_PROP_IDENTIFIER       :  return(this.ID() > obj.ID()                           ?  1  :  this.ID() < obj.ID()                            ? -1  :  0);
          case POSITION_PROP_REASON           :  return(this.Reason() > obj.Reason()                   ?  1  :  this.Reason() < obj.Reason()                    ? -1  :  0);
          case POSITION_PROP_ACCOUNT_LOGIN    :  return(this.AccountLogin() > obj.AccountLogin()       ?  1  :  this.AccountLogin() < obj.AccountLogin()        ? -1  :  0);
          case POSITION_PROP_TIME_CLOSE       :  return(this.TimeClose() > obj.TimeClose()             ?  1  :  this.TimeClose() < obj.TimeClose()              ? -1  :  0);
          case POSITION_PROP_TIME_CLOSE_MSC   :  return(this.TimeCloseMsc() > obj.TimeCloseMsc()       ?  1  :  this.TimeCloseMsc() < obj.TimeCloseMsc()        ? -1  :  0);
          case POSITION_PROP_VOLUME           :  return(this.Volume() > obj.Volume()                   ?  1  :  this.Volume() < obj.Volume()                    ? -1  :  0);
          case POSITION_PROP_PRICE_OPEN       :  return(this.PriceOpen() > obj.PriceOpen()             ?  1  :  this.PriceOpen() < obj.PriceOpen()              ? -1  :  0);
          case POSITION_PROP_SL               :  return(this.SL() > obj.SL()                           ?  1  :  this.SL() < obj.SL()                            ? -1  :  0);
          case POSITION_PROP_TP               :  return(this.TP() > obj.TP()                           ?  1  :  this.TP() < obj.TP()                            ? -1  :  0);
          case POSITION_PROP_PRICE_CURRENT    :  return(this.PriceCurrent() > obj.PriceCurrent()       ?  1  :  this.PriceCurrent() < obj.PriceCurrent()        ? -1  :  0);
          case POSITION_PROP_SWAP             :  return(this.Swap() > obj.Swap()                       ?  1  :  this.Swap() < obj.Swap()                        ? -1  :  0);
          case POSITION_PROP_PROFIT           :  return(this.Profit() > obj.Profit()                   ?  1  :  this.Profit() < obj.Profit()                    ? -1  :  0);
          case POSITION_PROP_CONTRACT_SIZE    :  return(this.ContractSize() > obj.ContractSize()       ?  1  :  this.ContractSize() < obj.ContractSize()        ? -1  :  0);
          case POSITION_PROP_PRICE_CLOSE      :  return(this.PriceClose() > obj.PriceClose()           ?  1  :  this.PriceClose() < obj.PriceClose()            ? -1  :  0);
          case POSITION_PROP_COMMISSIONS      :  return(this.Commissions() > obj.Commissions()         ?  1  :  this.Commissions() < obj.Commissions()          ? -1  :  0);
          case POSITION_PROP_FEE              :  return(this.Fee() > obj.Fee()                         ?  1  :  this.Fee() < obj.Fee()                          ? -1  :  0);
          case POSITION_PROP_SYMBOL           :  return(this.Symbol() > obj.Symbol()                   ?  1  :  this.Symbol() < obj.Symbol()                    ? -1  :  0);
          case POSITION_PROP_COMMENT          :  return(this.Comment() > obj.Comment()                 ?  1  :  this.Comment() < obj.Comment()                  ? -1  :  0);
          case POSITION_PROP_EXTERNAL_ID      :  return(this.ExternalID() > obj.ExternalID()           ?  1  :  this.ExternalID() < obj.ExternalID()            ? -1  :  0);
          case POSITION_PROP_CURRENCY_PROFIT  :  return(this.CurrencyProfit() > obj.CurrencyProfit()   ?  1  :  this.CurrencyProfit() < obj.CurrencyProfit()    ? -1  :  0);
          case POSITION_PROP_ACCOUNT_CURRENCY :  return(this.AccountCurrency() > obj.AccountCurrency() ?  1  :  this.AccountCurrency() < obj.AccountCurrency()  ? -1  :  0);
          case POSITION_PROP_ACCOUNT_SERVER   :  return(this.AccountServer() > obj.AccountServer()     ?  1  :  this.AccountServer() < obj.AccountServer()      ? -1  :  0);
          default                             :  return -1;
         }
      }
    

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


    Метод, возвращающий время с миллисекундами:

    //+------------------------------------------------------------------+
    //| Возвращает время с миллисекундами                                |
    //+------------------------------------------------------------------+
    string CPosition::TimeMscToString(const long time_msc, int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const
      {
       return(::TimeToString(time_msc/1000, flags) + "." + ::IntegerToString(time_msc %1000, 3, '0'));
      }
    


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

    //+------------------------------------------------------------------+
    //| Возвращает указатель на сделку открытия                          |
    //+------------------------------------------------------------------+
    CDeal *CPosition::GetDealIn(void) const
      {
       int total=this.m_list_deals.Total();
       for(int i=0; i<total; i++)
         {
          CDeal *deal=this.m_list_deals.At(i);
          if(deal==NULL)
             continue;
          if(deal.Entry()==DEAL_ENTRY_IN)
             return deal;
         }
       return NULL;
      }
    

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


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

    //+------------------------------------------------------------------+
    //| Возвращает указатель на сделку закрытия                          |
    //+------------------------------------------------------------------+
    CDeal *CPosition::GetDealOut(void) const
      {
       for(int i=this.m_list_deals.Total()-1; i>=0; i--)
         {
          CDeal *deal=this.m_list_deals.At(i);
          if(deal==NULL)
             continue;
          if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
             return deal;
         }
       return NULL;
      }
    

    В цикле по списку сделок позиции ищем сделку со способом изменения позиции DEAL_ENTRY_OUT (выход из рынка) или DEAL_ENTRY_OUT_BY (закрытие встречной позицией) и возвращаем указатель на найденную сделку


    Метод, возвращающий тикет сделки открытия:

    //+------------------------------------------------------------------+
    //| Возвращает тикет сделки открытия                                 |
    //+------------------------------------------------------------------+
    ulong CPosition::DealIn(void) const
      {
       CDeal *deal=this.GetDealIn();
       return(deal!=NULL ? deal.Ticket() : 0);
      }
    

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


    Метод, возвращающий тикет сделки закрытия:

    //+------------------------------------------------------------------+
    //| Возвращает тикет сделки закрытия                                 |
    //+------------------------------------------------------------------+
    ulong CPosition::DealOut(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Ticket() : 0);
      }
    

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


    Метод, возвращающий спред при открытии:

    //+------------------------------------------------------------------+
    //| Возвращает спред при открытии                                    |
    //+------------------------------------------------------------------+
    int CPosition::SpreadIn(void) const
      {
       CDeal *deal=this.GetDealIn();
       return(deal!=NULL ? deal.Spread() : 0);
      }
    

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


    Метод, возвращающий спред при закрытии:

    //+------------------------------------------------------------------+
    //| Возвращает спред при закрытии                                    |
    //+------------------------------------------------------------------+
    int CPosition::SpreadOut(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Spread() : 0);
      }
    

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


    Метод, возвращающий цену Ask при закрытии:

    //+------------------------------------------------------------------+
    //| Возвращает цену Ask при закрытии                                 |
    //+------------------------------------------------------------------+
    double CPosition::PriceOutAsk(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Ask() : 0);
      }
    

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


    Метод, возвращающий цену Bid при закрытии:

    //+------------------------------------------------------------------+
    //| Возвращает цену Bid при закрытии                                 |
    //+------------------------------------------------------------------+
    double CPosition::PriceOutBid(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Bid() : 0);
      }
    

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


    Метод, возвращающий прибыль в пунктах:

    //+------------------------------------------------------------------+
    //| Возвращает прибыль в пунктах                                     |
    //+------------------------------------------------------------------+
    int CPosition::ProfitInPoints(void) const
      {
    //--- Если Point символа не получен ранее, сообщаем об этом и возвращаем 0
       if(this.m_point==0)
         {
          ::Print("The Point() value could not be retrieved.");
          return 0;
         }
    //--- Получаем цены открытия и закрытия позиции
       double open =this.PriceOpen();
       double close=this.PriceClose();
       
    //--- Если цены получить не удалось - возвращаем 0
       if(open==0 || close==0)
          return 0;
       
    //--- В зависимости от типа позиции возвращаем рассчитанное значение прибыли позиции в пунктах
       return (int)::round(this.TypePosition()==POSITION_TYPE_BUY ? (close-open)/this.m_point : (open-close)/this.m_point);
      }
    


    Метод, возвращающий стоимость спреда при закрытии:

    //+------------------------------------------------------------------+
    //| Возвращает стоимость спреда при закрытии                         |
    //+------------------------------------------------------------------+
    double CPosition::SpreadOutCost(void) const
      {
    //--- Получаем сделку закрытия
       CDeal *deal=this.GetDealOut();
       if(deal==NULL)
          return 0;
    
    //--- Получаем профит позиции и профит позиции в пунктах
       double profit=this.Profit();
       int profit_pt=this.ProfitInPoints();
       
    //--- Если профит нулевой - возвращаем стоимость спреда по формуле TickValue * Spread * Lots
       if(profit==0)
          return(this.m_tick_value * deal.Spread() * deal.Volume());
    
    //--- Рассчитываем и возвращаем стоимость спреда (пропорция)
       return(profit_pt>0 ? deal.Spread() * ::fabs(profit / profit_pt) : 0);
      }
    

    В методе используется два способа расчёта стоимости спреда:

    1. если профит позиции не равен нулю, то стоимость спреда рассчитывается по пропорции: размер спреда в пунктах * профит позиции в деньгах / профит позиции в пунктах.
    2. если профит позиции равен нулю, то стоимость спреда рассчитывается по формуле: рассчитанная стоимость тика * размер спреда в пунктах * объём в сделке.


    Метод, устанавливающий совокупную комиссию всех сделок:

    //+------------------------------------------------------------------+
    //| Устанавливает совокупную комиссию всех сделок                    |
    //+------------------------------------------------------------------+
    void CPosition::SetCommissions(void)
      {
       double res=0;
       int total=this.m_list_deals.Total();
       for(int i=0; i<total; i++)
         {
          CDeal *deal=this.m_list_deals.At(i);
          res+=(deal!=NULL ? deal.Commission() : 0);
         }
       this.SetProperty(POSITION_PROP_COMMISSIONS, res);
      }
    

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


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

    //+------------------------------------------------------------------+
    //| Устанавливает совокупную оплату за проведение сделок             |
    //+------------------------------------------------------------------+
    void CPosition::SetFee(void)
      {
       double res=0;
       int total=this.m_list_deals.Total();
       for(int i=0; i<total; i++)
         {
          CDeal *deal=this.m_list_deals.At(i);
          res+=(deal!=NULL ? deal.Fee() : 0);
         }
       this.SetProperty(POSITION_PROP_FEE, res);
      }
    

    Здесь всё точно так же, как и в предыдущем методе — возвращаем общую сумму значений Fee каждой сделки позиции.

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


    Метод, добавляющий сделку в список сделок позиции:

    //+------------------------------------------------------------------+
    //| Добавляет сделку в список сделок                                 |
    //+------------------------------------------------------------------+
    CDeal *CPosition::DealAdd(const long ticket)
      {
    //--- Устанавливаем временному объекту тикет искомой сделки и устанавливаем флаг сортировки списка сделок по тикету
       this.m_temp_deal.SetTicket(ticket);
       this.m_list_deals.Sort(DEAL_PROP_TICKET);
       
    //--- Записываем результат проверки присутствия в списке сделки с таким тикетом
       bool exist=(this.m_list_deals.Search(&this.m_temp_deal)!=WRONG_VALUE);
       
    //--- Возвращаем для списка сортировку по времени в миллисекундах
       this.m_list_deals.Sort(DEAL_PROP_TIME_MSC);
    
    //--- Если сделка с таким тикетом уже есть в списке - возвращаем NULL
       if(exist)
          return NULL;
          
    //--- Создаём новый объект-сделку
       CDeal *deal=new CDeal(ticket);
       if(deal==NULL)
          return NULL;
       
    //--- Добавляем созданный объект в список в порядке сортировки по времени в миллисекундах
    //--- Если сделку добавить в список не удалось - удаляем объект сделки и возвращаем NULL
       if(!this.m_list_deals.InsertSort(deal))
         {
          delete deal;
          return NULL;
         }
       
    //--- Если это сделка закрытия позиции - записываем в значение прибыли позиции прибыль и своп из свойств сделки
       if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
         {
          this.SetProfit(deal.Profit());
          this.SetSwap(deal.Swap());
         }
         
    //--- Возвращаем указатель на созданный объект-сделку
       return deal;   
      }
    

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


    Методы, возвращающие описания некоторых свойств позиции:

    //+------------------------------------------------------------------+
    //| Возвращает описание типа позиции                                 |
    //+------------------------------------------------------------------+
    string CPosition::TypeDescription(void) const
      {
       return(this.TypePosition()==POSITION_TYPE_BUY ? "Buy" : this.TypePosition()==POSITION_TYPE_SELL ? "Sell" : "Unknown::"+(string)this.TypePosition());
      }
    //+------------------------------------------------------------------+
    //| Возвращает описание времени и цены открытия позиции              |
    //+------------------------------------------------------------------+
    string CPosition::TimePriceOpenDescription(void)
      {
       return(::StringFormat("Opened %s [%.*f]", this.TimeMscToString(this.TimeMsc()),this.m_digits, this.PriceOpen()));
      }
    //+------------------------------------------------------------------+
    //| Возвращает описание времени и цены закрытия позиции              |
    //+------------------------------------------------------------------+
    string CPosition::TimePriceCloseDescription(void)
      {
       if(this.TimeCloseMsc()==0)
          return "Not closed yet";
       return(::StringFormat("Closed %s [%.*f]", this.TimeMscToString(this.TimeCloseMsc()),this.m_digits, this.PriceClose()));
      }
    //+------------------------------------------------------------------+
    //| Возвращает краткое описание позиции                              |
    //+------------------------------------------------------------------+
    string CPosition::Description(void)
      {
       return(::StringFormat("%I64d (%s): %s %.2f %s #%I64d, Magic %I64d", this.AccountLogin(), this.AccountServer(),
                             this.Symbol(), this.Volume(), this.TypeDescription(), this.ID(), this.Magic()));
      }
    

    Данные методы используются для, например, вывода в журнал описания позиции.

    Распечатать описание позиции в журнале можно при помощи метода Print:

    //+------------------------------------------------------------------+
    //| Распечатывает в журнале свойства позиции и её сделок             |
    //+------------------------------------------------------------------+
    void CPosition::Print(void)
      {
       ::PrintFormat("%s\n-%s\n-%s", this.Description(), this.TimePriceOpenDescription(), this.TimePriceCloseDescription());
       for(int i=0; i<this.m_list_deals.Total(); i++)
         {
          CDeal *deal=this.m_list_deals.At(i);
          if(deal==NULL)
             continue;
          deal.Print();
         }
      }
    

    Сначала распечатывается заголовок с описанием позиции, а затем в цикле по всем сделкам позиции распечатывается описание каждой сделки при помощи её метода Print().

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


    Класс для поиска и фильтрации по свойствам сделок и позиций

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

    В папке \MQL5\Services\AccountReporter\ создадим новый файл Select.mqh класса CSelect:

    //+------------------------------------------------------------------+
    //|                                                       Select.mqh |
    //|                        Copyright 2024, MetaQuotes Software Corp. |
    //|                             https://mql5.com/ru/users/artmedia70 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Software Corp."
    #property link      "https://mql5.com/ru/users/artmedia70"
    #property version   "1.00"
    
    //+------------------------------------------------------------------+
    //| Класс для выборки объектов, удовлетворяющих критерию             |
    //+------------------------------------------------------------------+
    class CSelect
      {
      }


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

    //+------------------------------------------------------------------+
    //|                                                       Select.mqh |
    //|                        Copyright 2024, MetaQuotes Software Corp. |
    //|                             https://mql5.com/ru/users/artmedia70 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Software Corp."
    #property link      "https://mql5.com/ru/users/artmedia70"
    #property version   "1.00"
    
    enum ENUM_COMPARER_TYPE
      {
       EQUAL,                                                   // Равно
       MORE,                                                    // Больше
       LESS,                                                    // Меньше
       NO_EQUAL,                                                // Не равно
       EQUAL_OR_MORE,                                           // Больше или равно
       EQUAL_OR_LESS                                            // Меньше или равно
      };
    
    //+------------------------------------------------------------------+
    //| Включаемые файлы                                                 |
    //+------------------------------------------------------------------+
    #include "Deal.mqh"
    #include "Position.mqh"
    
    //+------------------------------------------------------------------+
    //| Список-хранилище                                                 |
    //+------------------------------------------------------------------+
    CArrayObj   ListStorage; // Объект-хранилище для хранения сортированных списков коллекций
    //+------------------------------------------------------------------+
    //| Класс для выборки объектов, удовлетворяющих критерию             |
    //+------------------------------------------------------------------+
    class CSelect
      {
      }
    


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

    //+------------------------------------------------------------------+
    //|                                                       Select.mqh |
    //|                        Copyright 2024, MetaQuotes Software Corp. |
    //|                             https://mql5.com/ru/users/artmedia70 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Software Corp."
    #property link      "https://mql5.com/ru/users/artmedia70"
    #property version   "1.00"
    
    enum ENUM_COMPARER_TYPE                                     // Режимы сравнения
      {
       EQUAL,                                                   // Равно
       MORE,                                                    // Больше
       LESS,                                                    // Меньше
       NO_EQUAL,                                                // Не равно
       EQUAL_OR_MORE,                                           // Больше или равно
       EQUAL_OR_LESS                                            // Меньше или равно
      };
    
    //+------------------------------------------------------------------+
    //| Включаемые файлы                                                 |
    //+------------------------------------------------------------------+
    #include "Deal.mqh"
    #include "Position.mqh"
    
    //+------------------------------------------------------------------+
    //| Список-хранилище                                                 |
    //+------------------------------------------------------------------+
    CArrayObj   ListStorage; // Объект-хранилище для хранения сортированных списков коллекций
    //+------------------------------------------------------------------+
    //| Класс для выборки объектов, удовлетворяющих критерию             |
    //+------------------------------------------------------------------+
    class CSelect
      {
    private:
       //--- Метод сравнения двух величин
       template<typename T>
       static bool       CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode);
    public:
    //+------------------------------------------------------------------+
    //| Методы работы со сделками                                        |
    //+------------------------------------------------------------------+
       //--- Возвращает список сделок, у которых одно из (1) целочисленных, (2) вещественных и (3) строковых свойств удовлетворяет заданному критерию
       static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property,long value,ENUM_COMPARER_TYPE mode);
       static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property,double value,ENUM_COMPARER_TYPE mode);
       static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property,string value,ENUM_COMPARER_TYPE mode);
       
       //--- Возвращает индекс сделки в списке с максимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства
       static int        FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property);
       static int        FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property);
       static int        FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property);
       
       //--- Возвращает индекс сделки в списке с минимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства
       static int        FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property);
       static int        FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property);
       static int        FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property);
       
    //+------------------------------------------------------------------+
    //| Методы работы с позициями                                        |
    //+------------------------------------------------------------------+
       //--- Возвращает список позиций, у которых одно из (1) целочисленных, (2) вещественных и (3) строковых свойств удовлетворяет заданному критерию
       static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property,long value,ENUM_COMPARER_TYPE mode);
       static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property,double value,ENUM_COMPARER_TYPE mode);
       static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property,string value,ENUM_COMPARER_TYPE mode);
       
       //--- Возвращает индекс позиции в списке с максимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства
       static int        FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property);
       static int        FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property);
       static int        FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property);
       
       //--- Возвращает индекс позиции в списке с минимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства
       static int        FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property);
       static int        FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property);
       static int        FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property);
      };
    //+------------------------------------------------------------------+
    //| Метод сравнения двух величин                                     |
    //+------------------------------------------------------------------+
    template<typename T>
    bool CSelect::CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode)
      {
       switch(mode)
         {
          case EQUAL           :  return(value1==value2   ?  true  :  false);
          case NO_EQUAL        :  return(value1!=value2   ?  true  :  false);
          case MORE            :  return(value1>value2    ?  true  :  false);
          case LESS            :  return(value1<value2    ?  true  :  false);
          case EQUAL_OR_MORE   :  return(value1>=value2   ?  true  :  false);
          case EQUAL_OR_LESS   :  return(value1<=value2   ?  true  :  false);
          default              :  return false;
         }
      }
    //+------------------------------------------------------------------+
    //| Методы работы со списками сделок                                 |
    //+------------------------------------------------------------------+
    //+------------------------------------------------------------------+
    //| Возвращает список сделок, у которых одно из целочисленных        |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT 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);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       int total=list_source.Total();
       for(int i=0; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          long obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop, value, mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Возвращает список сделок, у которых одно из вещественных         |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL 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);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       for(int i=0; i<list_source.Total(); i++)
         {
          CDeal *obj=list_source.At(i);
          double obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop,value,mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Возвращает список сделок, у которых одно из строковых            |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR 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);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       for(int i=0; i<list_source.Total(); i++)
         {
          CDeal *obj=list_source.At(i);
          string obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop,value,mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с максимальным значением целочисленного свойства                 |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CDeal *max_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          long obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          long obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с максимальным значением вещественного свойства                  |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CDeal *max_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          double obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          double obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с максимальным значением строкового свойства                     |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CDeal *max_obj=NULL;
       int total=list_source.Total();
       if(total==0) return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          string obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          string obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с минимальным значением целочисленного свойства                  |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_INT property)
      {
       int index=0;
       CDeal *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          long obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          long obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с минимальным значением вещественного свойства                   |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_DBL property)
      {
       int index=0;
       CDeal *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          double obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          double obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с минимальным значением строкового свойства                      |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_STR property)
      {
       int index=0;
       CDeal *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          string obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          string obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Методы работы со списками позиций                                |
    //+------------------------------------------------------------------+
    //+------------------------------------------------------------------+
    //| Возвращает список позиций, у которых одно из целочисленных       |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT 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);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       int total=list_source.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          long obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop, value, mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Возвращает список позиций, у которых одно из вещественных        |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL 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);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       for(int i=0; i<list_source.Total(); i++)
         {
          CPosition *obj=list_source.At(i);
          double obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop,value,mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Возвращает список позиций, у которых одно из строковых           |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR 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);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       for(int i=0; i<list_source.Total(); i++)
         {
          CPosition *obj=list_source.At(i);
          string obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop,value,mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с максимальным значением целочисленного свойства                 |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CPosition *max_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          long obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          long obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с максимальным значением вещественного свойства                  |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CPosition *max_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          double obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          double obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с максимальным значением строкового свойства                     |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CPosition *max_obj=NULL;
       int total=list_source.Total();
       if(total==0) return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          string obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          string obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с минимальным значением целочисленного свойства                  |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_INT property)
      {
       int index=0;
       CPosition *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          long obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          long obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с минимальным значением вещественного свойства                   |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_DBL property)
      {
       int index=0;
       CPosition *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          double obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          double obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с минимальным значением строкового свойства                      |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_STR property)
      {
       int index=0;
       CPosition *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          string obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          string obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    

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

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


    Класс коллекция исторических позиций

    В папке терминала \MQL5\Services\AccountReporter\ создадим новый файл PositionsControl.mqh класса CPositionsControl.
    Класс должен быть унаследован от базового объекта Стандартной Библиотеки CObject, а файлы класса исторических позиций и класса для поиска и фильтрации должны быть подключены к создаваемому файлу:

    //+------------------------------------------------------------------+
    //|                                             PositionsControl.mqh |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #include "Position.mqh"
    #include "Select.mqh"
    
    //+------------------------------------------------------------------+
    //| Класс-коллекция исторических позиций                             |
    //+------------------------------------------------------------------+
    class CPositionsControl : public CObject
      {
      }


    Объявим приватные, защищённые и публичные методы класса:

    //+------------------------------------------------------------------+
    //| Класс-коллекция исторических позиций                             |
    //+------------------------------------------------------------------+
    class CPositionsControl : public CObject
      {
    private:
    //--- Возвращает (1) тип позиции, (2) причину открытия по типу сделки
       ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal);
       ENUM_POSITION_REASON PositionReasonByDeal(const CDeal *deal);
    
    protected:
       CPosition         m_temp_pos;          // Временный объект позиции для поиска
       CArrayObj         m_list_pos;          // Список позиций
       
    //--- Возвращает объект-позицию из списка по идентификатору
       CPosition        *GetPositionObjByID(const long id);
    
    //--- Возвращает флаг того, что позиция рыночная
       bool              IsMarketPosition(const long id);
       
    public:
    //--- Создаёт и обновляет список позиций. Может быть переопределён в наследуемых классах
       virtual bool      Refresh(void);
       
    //--- Возвращает (1) список, (2) количество позиций в списке
       CArrayObj        *GetPositionsList(void)           { return &this.m_list_pos;          }
       int               PositionsTotal(void)       const { return this.m_list_pos.Total();   }
    
    //--- Распечатывает в журнале свойства всех позиций в списке и их сделок
       void              Print(void);
    
    //--- Конструктор/деструктор
                         CPositionsControl(void);
                        ~CPositionsControl();
      };
    


    Рассмотрим реализации объявленных методов.

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

    //+------------------------------------------------------------------+
    //| Конструктор                                                      |
    //+------------------------------------------------------------------+
    CPositionsControl::CPositionsControl(void)
      {
       this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC);
      }
    


    В деструкторе класса уничтожаем список исторических позиций:

    //+------------------------------------------------------------------+
    //| Деструктор                                                       |
    //+------------------------------------------------------------------+
    CPositionsControl::~CPositionsControl()
      {
       this.m_list_pos.Shutdown();
      }
    


    Метод, возвращающий указатель на объект позиции из списка по идентификатору:

    //+------------------------------------------------------------------+
    //| Возвращает объект-позицию из списка по идентификатору            |
    //+------------------------------------------------------------------+
    CPosition *CPositionsControl::GetPositionObjByID(const long id)
      {
    //--- Устанавливаем временному объекту идентификатор позиции, а списку - флаг сортировки по идентификатору позиции
       this.m_temp_pos.SetID(id);
       this.m_list_pos.Sort(POSITION_PROP_IDENTIFIER);
       
    //--- Получаем из списка индекс объекта-позиции с указанным идентификатором (либо -1 при его отсутствии)
    //--- По полученному индексу получаем указатель на объект позицию из списка (либо NULL при значении индекса -1)
       int index=this.m_list_pos.Search(&this.m_temp_pos);
       CPosition *pos=this.m_list_pos.At(index);
       
    //--- Возвращаем списку флаг сортировки по времени закрытия позиции в миллисекундах и
    //--- возвращаем указатель на объект-позицию (либо NULL при его отсутствии)
       this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC);
       return pos;
      }
    


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

    //+------------------------------------------------------------------+
    //| Возвращает флаг того, что позиция рыночная                       |
    //+------------------------------------------------------------------+
    bool CPositionsControl::IsMarketPosition(const long id)
      {
    //--- В цикле по списку действующих позиций в терминале
       for(int i=::PositionsTotal()-1; i>=0; i--)
         {
          //--- получаем тикет позиции по индексу цикла
          ulong ticket=::PositionGetTicket(i);
          //--- Если тикет получен и позицию можно выбрать и её идентификатор равен переданному в метод -
          //--- это искомая рыночная позиция, возвращаем true
          if(ticket!=0 && ::PositionSelectByTicket(ticket) && ::PositionGetInteger(POSITION_IDENTIFIER)==id)
             return true;
         }
    //--- Нет такой рыночной позиции - возвращаем false
       return false;
      }
    


    Метод, возвращающий тип позиции по типу сделки:

    //+------------------------------------------------------------------+
    //| Возвращает тип позиции по типу сделки                            |
    //+------------------------------------------------------------------+
    ENUM_POSITION_TYPE CPositionsControl::PositionTypeByDeal(const CDeal *deal)
      {
       if(deal==NULL)
          return WRONG_VALUE;
       switch(deal.TypeDeal())
         {
          case DEAL_TYPE_BUY   :  return POSITION_TYPE_BUY;
          case DEAL_TYPE_SELL  :  return POSITION_TYPE_SELL;
          default              :  return WRONG_VALUE;
         }
      }
    

    В зависимости от типа сделки, возвращаем соответствующий тип позиции.


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

    //+------------------------------------------------------------------+
    //| Возвращает причину открытия позиции по типу сделки               |
    //+------------------------------------------------------------------+
    ENUM_POSITION_REASON CPositionsControl::PositionReasonByDeal(const CDeal *deal)
      {
       if(deal==NULL)
          return WRONG_VALUE;
       switch(deal.Reason())
         {
          case DEAL_REASON_CLIENT :  return POSITION_REASON_CLIENT;
          case DEAL_REASON_MOBILE :  return POSITION_REASON_MOBILE;
          case DEAL_REASON_WEB    :  return POSITION_REASON_WEB;
          case DEAL_REASON_EXPERT :  return POSITION_REASON_EXPERT;
          default                 :  return WRONG_VALUE;
         }
      }
    

    В зависимости от причины совершения сделки возвращаем соответствующую причину открытия позиции.


    Метод, создающий или обновляющий список исторических позиций:

    //+------------------------------------------------------------------+
    //| Создаёт список исторических позиций                              |
    //+------------------------------------------------------------------+
    bool CPositionsControl::Refresh(void)
      {
    //--- Если запросить историю сделок и ордеров не удалось - возвращаем false
       if(!::HistorySelect(0,::TimeCurrent()))
          return false;
          
    //--- Ставим списку позиций флаг сортировки по времени в миллисекундах
       this.m_list_pos.Sort(POSITION_PROP_TIME_MSC);
       
    //--- Объявляем переменную результата и указатель на объект позиции
       bool res=true;
       CPosition *pos=NULL;
    
    //--- В цикле по количеству сделок истории
       int total=::HistoryDealsTotal();
       for(int i=total-1; i>=0; i--)
         {
          //--- получаем тикет очередной сделки в списке
          ulong ticket=::HistoryDealGetTicket(i);
          
          //--- Если тикет сделки не получен, или это не операция покупки/продажи - идём дальше
          ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE);
          if(ticket==0 || (deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL))
             continue;
          
          //--- Получаем из сделки значение идентификатора позиции
          long pos_id=::HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
          
          //--- Если это рыночная позиция - идём далее
          if(this.IsMarketPosition(pos_id))
             continue;
             
          //--- Получаем указатель на объект-позицию из списка
          pos=this.GetPositionObjByID(pos_id);
          
          //--- Если позиции с таким идентификатором в списке ещё нет
          if(pos==NULL)
            {
             //--- Создаём новый объект позиции и, если объект создать не удалось, добавляем к переменной res значение false и идём далее
             string pos_symbol=HistoryDealGetString(ticket, DEAL_SYMBOL);
             pos=new CPosition(pos_id, pos_symbol);
             if(pos==NULL)
               {
                res &=false;
                continue;
               }
             
             //--- Если объект позиции не удалось добавить в список - добавляем к переменной res значение false, удаляем объект позиции и идём далее
             if(!this.m_list_pos.InsertSort(pos))
               {
                res &=false;
                delete pos;
                continue;
               }
            }
          
          //--- Если объект сделки не удалось добавить в список сделок объекта позиции - добавляем к переменной res значение false и идём далее
          CDeal *deal=pos.DealAdd(ticket);
          if(deal==NULL)
            {
             res &=false;
             continue;
            }
          
          //--- Всё успешно.
          //--- В зависимости от типа сделки устанавливаем свойства позиции
          if(deal.Entry()==DEAL_ENTRY_IN)
            {
             pos.SetTicket(deal.Order());
             pos.SetMagic(deal.Magic());
             pos.SetTime(deal.Time());
             pos.SetTimeMsc(deal.TimeMsc());
             ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal);
             pos.SetTypePosition(type);
             ENUM_POSITION_REASON reason=this.PositionReasonByDeal(deal);
             pos.SetReason(reason);
             pos.SetPriceOpen(deal.Price());
             pos.SetVolume(deal.Volume());
            }
          if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
            {
             pos.SetPriceCurrent(deal.Price());
             pos.SetPriceClose(deal.Price());
             pos.SetTimeClose(deal.Time());
             pos.SetTimeCloseMsc(deal.TimeMsc());
            }
          if(deal.Entry()==DEAL_ENTRY_INOUT)
            {
             ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal);
             pos.SetTypePosition(type);
             pos.SetVolume(deal.Volume()-pos.Volume());
            }
         }
         
    //--- Все исторические позиции созданы, а соответствующие сделки добавлены в списки сделок объектов-позиций
    //--- Устанавливаем  списку позиций флаг сортировки по времени закрытия в миллисекундах
       this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC);
    
    //--- В цикле по созданному списку закрытых позиций устанавливаем каждой позиции значения Commissions и Fee
       for(int i=0; i<this.m_list_pos.Total(); i++)
         {
          CPosition *pos=this.m_list_pos.At(i);
          if(pos==NULL)
             continue;
          pos.SetCommissions();
          pos.SetFee();
         }
    
    //--- Возвращаем результат создания и добавления позиции в список
       return res;
      }
    

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


    Метод, распечатывающий в журнале свойства позиций и их сделок:

    //+------------------------------------------------------------------+
    //| Распечатывает в журнале свойства позиций и их сделок             |
    //+------------------------------------------------------------------+
    void CPositionsControl::Print(void)
      {
       int total=this.m_list_pos.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=this.m_list_pos.At(i);
          if(pos==NULL)
             continue;
          pos.Print();
         }
      }
    

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

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

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


    Класс аккаунта

    В папке терминала \MQL5\Services\AccountReporter\ создадим новый файл Account.mqh класса CAccount.
    Класс должен быть унаследован от класса базового объекта Стандартной Библиотеки CObject, а файл класса коллекции исторических позиций должен быть подключен к создаваемому файлу:

    //+------------------------------------------------------------------+
    //|                                                      Account.mqh |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #include "PositionsControl.mqh"
    
    //+------------------------------------------------------------------+
    //| Класс аккаунта                                                   |
    //+------------------------------------------------------------------+
    class CAccount : public CObject
      {
      }


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

    //+------------------------------------------------------------------+
    //| Класс аккаунта                                                   |
    //+------------------------------------------------------------------+
    class CAccount : public CObject
      {
    private:
       
    protected:
       CPositionsControl m_positions;                  // Объект контроля исторических позиций
    //--- целочисленные свойства аккаунта
       long              m_login;                    //   Номер счета
       ENUM_ACCOUNT_TRADE_MODE m_trade_mode;         //   Тип торгового счета
       long              m_leverage;                 //   Размер предоставленного плеча
       int               m_limit_orders;             //   Максимально допустимое количество действующих отложенных ордеров
       ENUM_ACCOUNT_STOPOUT_MODE m_margin_so_mode;   //   Режим задания минимально допустимого уровня залоговых средств
       bool              m_trade_allowed;            //   Разрешенность торговли для текущего счета
       bool              m_trade_expert;             //   Разрешенность торговли для эксперта
       ENUM_ACCOUNT_MARGIN_MODE m_margin_mode;       //   Режим расчета маржи
       int               m_currency_digits;          //   Количество знаков после запятой для валюты счета, необходимых для точного отображения торговых результатов
       bool              m_fifo_close;               //   Признак того, что позиции можно закрывать только по правилу FIFO
       bool              m_hedge_allowed;            //   Признак того, что разрешены встречные позиции по одному символу
        
    //--- вещественные свойства аккаунта
       double            m_balance;                  //   Баланс счета в валюте депозита
       double            m_credit;                   //   Размер предоставленного кредита в валюте депозита
       double            m_profit;                   //   Размер текущей прибыли на счете в валюте депозита
       double            m_equity;                   //   Значение собственных средств на счете в валюте депозита
       double            m_margin;                   //   Размер зарезервированных залоговых средств на счете  в валюте депозита
       double            m_margin_free;              //   Размер свободных средств на счете  в валюте депозита, доступных для открытия позиции
       double            m_margin_level;             //   Уровень залоговых средств на счете в процентах
       double            m_margin_so_call;           //   Уровень залоговых средств, при котором требуется пополнение счета (Margin Call)
       double            m_margin_so_so;             //   Уровень залоговых средств, при достижении которого происходит принудительное закрытие самой убыточной позиции (Stop Out)
       double            m_margin_initial;           //   Размер средств, зарезервированных на счёте, для обеспечения гарантийной суммы по всем отложенным ордерам 
       double            m_margin_maintenance;       //   Размер средств, зарезервированных на счёте, для обеспечения минимальной суммы по всем открытым позициям
       double            m_assets;                   //   Текущий размер активов на счёте
       double            m_liabilities;              //   Текущий размер обязательств на счёте
       double            m_commission_blocked;       //   Текущая сумма заблокированных комиссий по счёту
      
    //--- строковые свойства аккаунта
       string            m_name;                     //   Имя клиента
       string            m_server;                   //   Имя торгового сервера
       string            m_currency;                 //   Валюта депозита
       string            m_company;                  //   Имя компании, обслуживающей счет
       
    public:
    


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

    public:
    //--- Возвращает (1) объект контроля, (2) список исторических позиций, (3) количество позиций
       CPositionsControl*GetPositionsCtrlObj(void)        { return &this.m_positions;                  }
       CArrayObj        *GetPositionsList(void)           { return this.m_positions.GetPositionsList();}
       int               PositionsTotal(void)             { return this.m_positions.PositionsTotal();  }
    
    //--- Возвращает список позиций по фильтру (1) целочисленного, (2) вещественного, (3) строкового свойства
       CArrayObj        *GetPositionsList(ENUM_POSITION_PROPERTY_INT property, long   value, ENUM_COMPARER_TYPE mode)
                           { return CSelect::ByPositionProperty(this.GetPositionsList(), property, value, mode); }
       
       CArrayObj        *GetPositionsList(ENUM_POSITION_PROPERTY_DBL property, double value, ENUM_COMPARER_TYPE mode)
                           { return CSelect::ByPositionProperty(this.GetPositionsList(), property, value, mode); }
       
       CArrayObj        *GetPositionsList(ENUM_POSITION_PROPERTY_STR property, string value, ENUM_COMPARER_TYPE mode)
                           { return CSelect::ByPositionProperty(this.GetPositionsList(), property, value, mode); }
    
    //--- (1) Обновляет, (2) распечатывает в журнал список закрытых позиций
       bool              PositionsRefresh(void)           { return this.m_positions.Refresh();}
       void              PositionsPrint(void)             { this.m_positions.Print();         }
       
    //--- устанавливает (1) логин, (2) сервер
       void              SetLogin(const long login)       { this.m_login=login;               }
       void              SetServer(const string server)   { this.m_server=server;             }
       
    //--- возврат целочисленных свойств аккаунта
       long              Login(void)                const { return this.m_login;              }  //   Номер счета
       ENUM_ACCOUNT_TRADE_MODE TradeMode(void)      const { return this.m_trade_mode;         }  //   Тип торгового счета
       long              Leverage(void)             const { return this.m_leverage;           }  //   Размер предоставленного плеча
       int               LimitOrders(void)          const { return this.m_limit_orders;       }  //   Максимально допустимое количество действующих отложенных ордеров
       ENUM_ACCOUNT_STOPOUT_MODE MarginSoMode(void) const { return this.m_margin_so_mode;     }  //   Режим задания минимально допустимого уровня залоговых средств
       bool              TradeAllowed(void)         const { return this.m_trade_allowed;      }  //   Разрешенность торговли для текущего счета
       bool              TradeExpert(void)          const { return this.m_trade_expert;       }  //   Разрешенность торговли для эксперта
       ENUM_ACCOUNT_MARGIN_MODE MarginMode(void)    const { return this.m_margin_mode;        }  //   Режим расчета маржи
       int               CurrencyDigits(void)       const { return this.m_currency_digits;    }  //   Количество знаков после запятой для валюты счета, необходимых для точного отображения торговых результатов
       bool              FIFOClose(void)            const { return this.m_fifo_close;         }  //   Признак того, что позиции можно закрывать только по правилу FIFO
       bool              HedgeAllowed(void)         const { return this.m_hedge_allowed;      }  //   Признак того, что разрешены встречные позиции по одному символу
        
    //--- возврат вещественных свойств аккаунта
       double            Balance(void)              const { return this.m_balance;            }  //   Баланс счета в валюте депозита
       double            Credit(void)               const { return this.m_credit;             }  //   Размер предоставленного кредита в валюте депозита
       double            Profit(void)               const { return this.m_profit;             }  //   Размер текущей прибыли на счете в валюте депозита
       double            Equity(void)               const { return this.m_equity;             }  //   Значение собственных средств на счете в валюте депозита
       double            Margin(void)               const { return this.m_margin;             }  //   Размер зарезервированных залоговых средств на счете  в валюте депозита
       double            MarginFree(void)           const { return this.m_margin_free;        }  //   Размер свободных средств на счете  в валюте депозита, доступных для открытия позиции
       double            MarginLevel(void)          const { return this.m_margin_level;       }  //   Уровень залоговых средств на счете в процентах
       double            MarginSoCall(void)         const { return this.m_margin_so_call;     }  //   Уровень залоговых средств, при котором требуется пополнение счета (Margin Call)
       double            MarginSoSo(void)           const { return this.m_margin_so_so;       }  //   Уровень залоговых средств, при достижении которого происходит принудительное закрытие самой убыточной позиции (Stop Out)
       double            MarginInitial(void)        const { return this.m_margin_initial;     }  //   Размер средств, зарезервированных на счёте, для обеспечения гарантийной суммы по всем отложенным ордерам 
       double            MarginMaintenance(void)    const { return this.m_margin_maintenance; }  //   Размер средств, зарезервированных на счёте, для обеспечения минимальной суммы по всем открытым позициям
       double            Assets(void)               const { return this.m_assets;             }  //   Текущий размер активов на счёте
       double            Liabilities(void)          const { return this.m_liabilities;        }  //   Текущий размер обязательств на счёте
       double            CommissionBlocked(void)    const { return this.m_commission_blocked; }  //   Текущая сумма заблокированных комиссий по счёту
      
    //--- возврат строковых свойств аккаунта
       string            Name(void)                 const { return this.m_name;               }  //   Имя клиента
       string            Server(void)               const { return this.m_server;             }  //   Имя торгового сервера
       string            Currency(void)             const { return this.m_currency;           }  //   Валюта депозита
       string            Company(void)              const { return this.m_company;            }  //   Имя компании, обслуживающей счет
       
    //--- возвращает описание (1) аккаунта, (2) типа торгового счёта, (3) режима расчёта маржи
       string            Description(void)          const;
       string            TradeModeDescription(void) const;
       string            MarginModeDescription(void)const;
       
    //--- виртуальный метод сравнения двух объектов
       virtual int       Compare(const CObject *node,const int mode=0) const;
    
    //--- Выводит в журнал описание аккаунта
       void              Print(void)                      { ::Print(this.Description());      }
    
    //--- конструкторы/деструктор
                         CAccount(void){}
                         CAccount(const long login, const string server_name);
                        ~CAccount() {}
      };
    


    Рассмотрим реализацию объявленных методов.

    В конструкторе класса установим объекту все свойства текущего аккаунта:

    //+------------------------------------------------------------------+
    //| Конструктор                                                      |
    //+------------------------------------------------------------------+
    CAccount::CAccount(const long login, const string server_name)
      {
       this.m_login=login;
       this.m_server=server_name;
       
    //--- устанавливаем целочисленные свойства аккаунта
       this.m_trade_mode          = (ENUM_ACCOUNT_TRADE_MODE)::AccountInfoInteger(ACCOUNT_TRADE_MODE);    //   Тип торгового счета
       this.m_leverage            = ::AccountInfoInteger(ACCOUNT_LEVERAGE);                               //   Размер предоставленного плеча
       this.m_limit_orders        = (int)::AccountInfoInteger(ACCOUNT_LIMIT_ORDERS);                      //   Максимально допустимое количество действующих отложенных ордеров
       this.m_margin_so_mode      = (ENUM_ACCOUNT_STOPOUT_MODE)AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE);//   Режим задания минимально допустимого уровня залоговых средств
       this.m_trade_allowed       = ::AccountInfoInteger(ACCOUNT_TRADE_ALLOWED);                          //   Разрешенность торговли для текущего счета
       this.m_trade_expert        = ::AccountInfoInteger(ACCOUNT_TRADE_EXPERT);                           //   Разрешенность торговли для эксперта
       this.m_margin_mode         = (ENUM_ACCOUNT_MARGIN_MODE)::AccountInfoInteger(ACCOUNT_MARGIN_MODE);  //   Режим расчета маржи
       this.m_currency_digits     = (int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS);                   //   Количество знаков после запятой для валюты счета, необходимых для точного отображения торговых результатов
       this.m_fifo_close          = ::AccountInfoInteger(ACCOUNT_FIFO_CLOSE);                             //   Признак того, что позиции можно закрывать только по правилу FIFO
       this.m_hedge_allowed       = ::AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED);                          //   Признак того, что разрешены встречные позиции по одному символу
        
    //--- устанавливаем вещественные свойства аккаунта
       this.m_balance             = ::AccountInfoDouble(ACCOUNT_BALANCE);                                 //   Баланс счета в валюте депозита
       this.m_credit              = ::AccountInfoDouble(ACCOUNT_CREDIT);                                  //   Размер предоставленного кредита в валюте депозита
       this.m_profit              = ::AccountInfoDouble(ACCOUNT_PROFIT);                                  //   Размер текущей прибыли на счете в валюте депозита
       this.m_equity              = ::AccountInfoDouble(ACCOUNT_EQUITY);                                  //   Значение собственных средств на счете в валюте депозита
       this.m_margin              = ::AccountInfoDouble(ACCOUNT_MARGIN);                                  //   Размер зарезервированных залоговых средств на счете  в валюте депозита
       this.m_margin_free         = ::AccountInfoDouble(ACCOUNT_MARGIN_FREE);                             //   Размер свободных средств на счете  в валюте депозита, доступных для открытия позиции
       this.m_margin_level        = ::AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);                            //   Уровень залоговых средств на счете в процентах
       this.m_margin_so_call      = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL);                          //   Уровень залоговых средств, при котором требуется пополнение счета (Margin Call)
       this.m_margin_so_so        = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_SO);                            //   Уровень залоговых средств, при достижении которого происходит принудительное закрытие самой убыточной позиции (Stop Out)
       this.m_margin_initial      = ::AccountInfoDouble(ACCOUNT_MARGIN_INITIAL);                          //   Размер средств, зарезервированных на счёте, для обеспечения гарантийной суммы по всем отложенным ордерам 
       this.m_margin_maintenance  = ::AccountInfoDouble(ACCOUNT_MARGIN_MAINTENANCE);                      //   Размер средств, зарезервированных на счёте, для обеспечения минимальной суммы по всем открытым позициям
       this.m_assets              = ::AccountInfoDouble(ACCOUNT_ASSETS);                                  //   Текущий размер активов на счёте
       this.m_liabilities         = ::AccountInfoDouble(ACCOUNT_LIABILITIES);                             //   Текущий размер обязательств на счёте
       this.m_commission_blocked  = ::AccountInfoDouble(ACCOUNT_COMMISSION_BLOCKED);                      //   Текущая сумма заблокированных комиссий по счёту
      
    //--- устанавливаем строковые свойства аккаунта
       this.m_name                = ::AccountInfoString(ACCOUNT_NAME);                                    //   Имя клиента
       this.m_currency            = ::AccountInfoString(ACCOUNT_CURRENCY);                                //   Валюта депозита
       this.m_company             = ::AccountInfoString(ACCOUNT_COMPANY);                                 //   Имя компании, обслуживающей счет
      }
    


    Метод сравнения двух объектов:

    //+------------------------------------------------------------------+
    //| Метод сравнения двух объектов                                    |
    //+------------------------------------------------------------------+
    int CAccount::Compare(const CObject *node,const int mode=0) const
      {
       const CAccount *obj=node;
       return(this.Login()>obj.Login()   ? 1 : this.Login()<obj.Login()   ? -1 :
              this.Server()>obj.Server() ? 1 : this.Server()<obj.Server() ? -1 : 0);
      }
    

    Метод сравнивает два объекта аккаунта только по двум свойствам — по логину и по имени сервера. Если логины двух сравниваемых объектов равны, то проверяется равенство имени сервера. Если и серверы одинаковы — значит два объекта равны. Иначе возвращается либо 1, либо -1 в зависимости от того, больше, либо меньше значение сравниваемого свойства двух объектов.


    Методы, возвращающие описания некоторых свойств объекта аккаунта:

    //+------------------------------------------------------------------+
    //| Возвращает описание типа торгового счёта                         |
    //+------------------------------------------------------------------+
    string CAccount::TradeModeDescription(void) const
      {
       string mode=::StringSubstr(::EnumToString(this.TradeMode()), 19);
       if(mode.Lower())
          mode.SetChar(0, ushort(mode.GetChar(0)-32));
       return mode;
      }
    //+------------------------------------------------------------------+
    //| Возвращает описание режима расчёта маржи                         |
    //+------------------------------------------------------------------+
    string CAccount::MarginModeDescription(void) const
      {
       string mode=::StringSubstr(::EnumToString(this.MarginMode()), 20);
       ::StringReplace(mode, "RETAIL_", "");
       if(mode.Lower())
          mode.SetChar(0, ushort(mode.GetChar(0)-32));
       return mode;
      }
    

    Данные методы используются для составления описания аккаунта в методе Description:

    //+------------------------------------------------------------------+
    //| Возвращает описание аккаунта                                     |
    //+------------------------------------------------------------------+
    string CAccount::Description(void) const
      {
       return(::StringFormat("%I64d: %s (%s, %s, %.2f %s, %s)",
                             this.Login(), this.Name(), this.Company(), this.TradeModeDescription(),
                             this.Balance(), this.Currency(), this.MarginModeDescription()));
      }
    

    Метод возвращает строку в виде

    68008618: Artem (MetaQuotes Ltd., Demo, 10779.50 USD, Hedging)
    

    Эту строку можно вывести в журнал методом Print() данного класса.

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


    Класс коллекция аккаунтов

    В папке терминала \MT5\MQL5\Services\AccountReporter\ создадим новый файл Accounts.mqh класса CAccounts.
    Класс должен быть унаследован от класса базового объекта Стандартной Библиотеки CObject, а файл класса аккаунта должен быть подключен к создаваемому файлу:

    //+------------------------------------------------------------------+
    //|                                                     Accounts.mqh |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #include "Account.mqh"
    
    //+------------------------------------------------------------------+
    //| Класс-коллекция аккаунтов                                        |
    //+------------------------------------------------------------------+
    class CAccounts : public CObject
      {
      }


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

    //+------------------------------------------------------------------+
    //| Класс-коллекция аккаунтов                                        |
    //+------------------------------------------------------------------+
    class CAccounts : public CObject
      {
    private:
       CArrayObj         m_list;           // Список объектов-аккаунтов
       CAccount          m_tmp;            // Временный объект-аккаунт для поиска
       
    protected:
    //--- Создаёт новый объект-аккаунт и добавляет его в список
       CAccount         *Add(const long login, const string server);
       
    public:
    //--- Создаёт новый объект-аккаунт
       bool              Create(const long login, const string server);
       
    //--- Возвращает указатель на указанный объект-аккаунт по (1) логину и серверу, (2) индексу в списке
       CAccount         *Get(const long login, const string server);
       CAccount         *Get(const int index)                         const { return this.m_list.At(index);  }
    
    //--- Объединяет списки позиций аккаунтов и возвращает общий
       CArrayObj        *GetCommonPositionsList(void);
    
    //--- Возвращает список позиций указанного аккаунта
       CArrayObj        *GetAccountPositionsList(const long login, const string server);
    
    //--- Возвращает количество хранимых аккаунтов
       int               Total(void)                                  const { return this.m_list.Total();    }
       
    //--- Обновляет списки позиций указанного аккаунта
       bool              PositionsRefresh(const long login, const string server);
    
    //--- Конструктор/деструктор
                         CAccounts();
                        ~CAccounts();
      };
    


    Рассмотрим реализацию объявленных методов.

    В конструкторе класса списку аккаунтов устанавливаем флаг сортированного списка:

    //+------------------------------------------------------------------+
    //| Конструктор                                                      |
    //+------------------------------------------------------------------+
    CAccounts::CAccounts()
      {
       this.m_list.Sort();
      }
    


    В деструкторе класса очищаем список аккаунтов:

    //+------------------------------------------------------------------+
    //| Деструктор                                                       |
    //+------------------------------------------------------------------+
    CAccounts::~CAccounts()
      {
       this.m_list.Clear();
      }
    


    Защищённый метод, создающий новый объект аккаунта и добавляющий его в список:

    //+------------------------------------------------------------------+
    //| Создаёт новый объект-аккаунт и добавляет его в список            |
    //+------------------------------------------------------------------+
    CAccount *CAccounts::Add(const long login,const string server)
      {
    //--- Создаём новый объект-аккаунт
       CAccount *account=new CAccount(login, server);
       if(account==NULL)
          return NULL;
    //--- Если созданный объект не добавлен в список - удаляем его и возвращаем NULL
       if(!this.m_list.Add(account))
         {
          delete account;
          return NULL;
         }
    //--- Возвращаем указатель на созданный объект
       return account;
      }
    

    Это защищённый метод, и работает он в составе публичного метода, создающего новый объект аккаунт:

    //+------------------------------------------------------------------+
    //| Создаёт новый объект-аккаунт                                     |
    //+------------------------------------------------------------------+
    bool CAccounts::Create(const long login,const string server)
      {
    //--- Во временный объект-аккаунт устанавливаем логин и сервер
       this.m_tmp.SetLogin(login);
       this.m_tmp.SetServer(server);
       
    //--- Списку объектов-аккаунтов устанавливаем флаг сортированного списка и
    //--- получаем индекс объекта в списке, имеющего те же логин и сервер, что и у временного объекта
       this.m_list.Sort();
       int index=this.m_list.Search(&this.m_tmp);
       
    //--- Возвращаем флаг успешного добавления объекта в список (результат работы метода Add), либо false, если объект в списке уже есть
       return(index==WRONG_VALUE ? this.Add(login, server)!=NULL : false);
      }
    


    Метод, возвращающий указатель на указанный объект аккаунт:

    //+------------------------------------------------------------------+
    //| Возвращает указатель на указанный объект-аккаунт                 |
    //+------------------------------------------------------------------+
    CAccount *CAccounts::Get(const long login,const string server)
      {
    //--- Во временный объект-аккаунт устанавливаем логин и сервер
       this.m_tmp.SetLogin(login);
       this.m_tmp.SetServer(server);
       
    //--- Списку объектов-аккаунтов устанавливаем флаг сортированного списка и
    //--- получаем индекс объекта в списке, имеющего те же логин и сервер, что и у временного объекта
       this.m_list.Sort();
       int index=this.m_list.Search(&this.m_tmp);
    
    //--- Возвращаем указатель на объект в списке по индексу, либо NULL, если индекс равен -1
       return this.m_list.At(index);
      }
    


    Метод, обновляющий списки позиций указанного аккаунта:

    //+------------------------------------------------------------------+
    //| Обновляет списки позиций указанного аккаунта                     |
    //+------------------------------------------------------------------+
    bool CAccounts::PositionsRefresh(const long login, const string server)
      {
    //--- Получаем указатель на объект-аккаунт с указанными логином и сервером
       CAccount *account=this.Get(login, server);
       if(account==NULL)
          return false;
    
    //--- Если полученный объект - не текущий аккаунт,
       if(account.Login()!=::AccountInfoInteger(ACCOUNT_LOGIN) || account.Server()!=::AccountInfoString(ACCOUNT_SERVER))
         {
          //--- сообщаем, что обновление данных не текущего аккаунта приведёт к некорректным данным и возвращаем false
          ::Print("Error. Updating the list of positions for a non-current account will result in incorrect data.");
          return false;
         }
    
    //--- Возвращаем результат обновления данных текущего аккаунта
       return account.PositionsRefresh();
      }
    


    Метод, объединяющий списки позиций аккаунтов и возвращающий общий список:

    //+------------------------------------------------------------------+
    //| Объединяет списки позиций аккаунтов и возвращает общий           |
    //+------------------------------------------------------------------+
    CArrayObj *CAccounts::GetCommonPositionsList(void)
      {
    //--- Создаём новый список и сбрасываем для него флаг управления памятью
       CArrayObj *list=new CArrayObj();
       if(list==NULL)
          return NULL;
       list.FreeMode(false);
    
    //--- В цикле по списку аккаунтов
       int total=this.m_list.Total();
       for(int i=0; i<total; i++)
         {
          //--- получаем очередной объект-аккаунт
          CAccount *account=this.m_list.At(i);
          if(account==NULL)
             continue;
          
          //--- Получаем список закрытых позиций аккаунта
          CArrayObj *src=account.GetPositionsList();
          if(src==NULL)
             continue;
    
          //--- Если это первый аккаунт в списке
          if(i==0)
            {
             //--- копируем в новый список элементы из списка позиций аккаунта
             if(!list.AssignArray(src))
               {
                delete list;
                return NULL;
               }
            }
          //--- Если это не первый аккаунт в списке
          else
            {
             //--- добавляем в конец нового списка элементы из списка позиций аккаунта
             if(!list.AddArray(src))
                continue;
            }
         }
       
    //--- Отправляем новый список в хранилище
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
         
    //--- Возвращаем указатель на созданный и заполненный список
       return list;
      }
    


    Метод, возвращающий список позиций указанного аккаунта:

    //+------------------------------------------------------------------+
    //| Возвращает список позиций указанного аккаунта                    |
    //+------------------------------------------------------------------+
    CArrayObj *CAccounts::GetAccountPositionsList(const long login,const string server)
      {
       CAccount *account=this.Get(login, server);
       return(account!=NULL ? account.GetPositionsList() : NULL);
      }
    

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

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

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


    Программа сервис для создания торговых отчётов и отправки уведомлений

    Определимся как должна работать программа.

    При запуске сервиса будет проверяться наличие в клиентском терминале MetaQuotes ID и разрешение на отправку Push-уведомлений на смартфон.

    Настройки эти можно найти в меню "Tools -- Options" во вкладке "Notifications":


    Если нет значения в поле MetaQuotes ID, или не установлена галочка Enable Push notifications, то сервис выдаст окно с запросом на установку этих параметров. Если отказаться от установки этих параметров, то должно быть выдано предупреждение, что нет MQID или не разрешена отправка уведомлений на смартфон, и что все сообщения будут только в журнале. Если установить все параметры, то отчёты будут отправляться как на смартфон, так и в журнал терминала "Эксперты". В основном цикле программа будет постоянно проверять состояние настроек отправки уведомлений в терминале. Поэтому, если при запуске сервиса не было установлено разрешение на отправку уведомлений, то всегда можно включить его уже после запуска программы сервиса — она увидит изменения и установит у себя соответствующий флаг.

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

    • Общие параметры отчётов
      • какие аккаунты использовать для отчётов: (все или текущий),
      • создавать ли отчёты в разрезе символов: (да/нет) — сначала создаётся отчёт, а затем из него создаются отдельные отчёты по каждому из символов, участвовавших в торговле,
      • создавать ли отчёты в разрезе магиков: (да/нет) — сначала создаётся отчёт, а затем из него создаются отдельные отчёты по каждому из магиков, участвовавших в торговле,
      • включать ли в отчёты комиссии: (да/нет) — если включено, то кроме общей суммы всех издержек будут отображены по отдельности затраты на комиссии, свопы и оплаты за проведение сделок,
      • включать ли в отчёты возможные потери на спредах при закрытии позиций: (да/нет) — если включено, то отдельно будет отображена сумма стоимости всех возможных затрат на спреде при закрытии;
    • Настройки ежедневных отчётов
      • отправлять ли отчёты за последние сутки; относится и к отчётам за указанные периоды времени: (да/нет) — если включено, то ежедневно в указанное время будут отправляться отчёты за последние сутки и за настраиваемые промежутки торгового времени (за количество дней, месяцев и лет),
      • час отправки отчёта: (по умолчанию 8),
      • минуты отправки отчёта: (по умолчанию 0);
    • Настройки ежедневных отчётов за настраиваемые периоды времени
      • отправлять ли отчёты за указанное количество дней: (да/нет) — если включено, то ежедневно в указанное выше время будут создаваться отчёты за заданное количество дней; количество дней отчёта считается вычитанием указанного количества дней от текущей даты,
      • количество дней для отчётов за указанное количество дней: (по умолчанию 7),
      • отправлять ли отчёты за указанное количество месяцев: (да/нет) — если включено, то ежедневно в указанное выше время будут создаваться отчёты за заданное количество месяцев; количество месяцев отчёта считается вычитанием указанного количества месяцев от текущей даты,
      • количество месяцев для отчётов за указанное количество месяцев: (по умолчанию 3),
      • отправлять ли отчёты за указанное количество лет: (да/нет) — если включено, то ежедневно в указанное выше время будут создаваться отчёты за заданное количество лет; количество лет отчёта считается вычитанием указанного количества лет от текущей даты,
      • количество лет для отчётов за указанное количество лет: (по умолчанию 2);
    • Настройки еженедельных отчётов для всех остальных периодов
      • день недели для отправки еженедельных отчётов: (по умолчанию Суббота) — при наступлении указанного дня будут созданы и отправлены те отчёты, которые указаны в настройках ниже,
      • час отправки отчётов: (по умолчанию 8),
      • минуты отправки отчётов: (по умолчанию 0),
      • отправлять ли отчёты за период с начала текущей недели: (да/нет) — если включено, то еженедельно в указанный день создаётся отчёт за период с начала текущей недели,
      • отправлять ли отчёты за период с начала текущего месяца: (да/нет) — если включено, то еженедельно в указанный день создаётся отчёт за период с начала текущего месяца,
      • отправлять ли отчёты за период с начала текущего года: (да/нет) — если включено, то еженедельно в указанный день создаётся отчёт за период с начала текущего года,
      • отправлять ли отчёты за весь торговый период: (да/нет) — если включено, то еженедельно в указанный день создаётся отчёт за весь торговый период.


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

    В папке терминала \MQL5\Services\AccountReporter\ создадим новый файл программы сервиса Reporter.mq5:



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

    //+------------------------------------------------------------------+
    //|                                                     Reporter.mq5 |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property service
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #define   COUNTER_DELAY       1000                                   // Задержка счётчика в миллисекундах в рабочем цикле
    #define   REFRESH_ATTEMPTS    5                                      // Количество попыток получения корректных данных аккаунта 
    #define   REFRESH_DELAY       500                                    // Задержка в миллисекундах перед очередной попыткой получения данных
    #define   TABLE_COLUMN_W      10                                     // Ширина колонки таблицы статистики для вывода в журнал
    
    #include <Arrays\ArrayString.mqh>                                    // Динамический массив переменных типа string для объекта списка символов
    #include <Arrays\ArrayLong.mqh>                                      // Динамический массив переменных типа long для объекта списка магиков
    #include <Tools\DateTime.mqh>                                        // Расширение структуры MqlDateTime
    #include "Accounts.mqh"                                              // Класс-коллекция объектов-аккаунтов
    
    //+------------------------------------------------------------------+
    //| Перечисления                                                     |
    //+------------------------------------------------------------------+
    enum ENUM_USED_ACCOUNTS                                              // Перечисление используемых аккаунтов в статистике
      {
       USED_ACCOUNT_CURRENT,                                             // Current Account only
       USED_ACCOUNTS_ALL,                                                // All used accounts
      };
    
    enum ENUM_REPORT_RANGE                                               // Перечисление диапазонов статистики
      {
       REPORT_RANGE_DAILY,                                               // Сутки
       REPORT_RANGE_WEEK_BEGIN,                                          // С начала недели
       REPORT_RANGE_MONTH_BEGIN,                                         // С начала месяца
       REPORT_RANGE_YEAR_BEGIN,                                          // С начала года
       REPORT_RANGE_NUM_DAYS,                                            // Количество дней
       REPORT_RANGE_NUM_MONTHS,                                          // Количество месяцев
       REPORT_RANGE_NUM_YEARS,                                           // Количество лет
       REPORT_RANGE_ALL,                                                 // Весь период
      };
      
    enum ENUM_REPORT_BY                                                  // Перечисление фильтров статистики
      {
       REPORT_BY_RANGE,                                                  // Диапазон дат
       REPORT_BY_SYMBOLS,                                                // По символам
       REPORT_BY_MAGICS,                                                 // По магикам
      };
      
    //+------------------------------------------------------------------+
    //| Входные параметры                                                |
    //+------------------------------------------------------------------+
    input group                "============== Report options =============="
    input ENUM_USED_ACCOUNTS   InpUsedAccounts      =  USED_ACCOUNT_CURRENT;// Accounts included in statistics
    input bool                 InpReportBySymbols   =  true;             // Reports by Symbol
    input bool                 InpReportByMagics    =  true;             // Reports by Magics
    input bool                 InpCommissionsInclude=  true;             // Including Comissions
    input bool                 InpSpreadInclude     =  true;             // Including Spread
    
    input group                "========== Daily reports for daily periods =========="
    input bool                 InpSendDReport       =  true;             // Send daily report (per day and specified periods)
    input uint                 InpSendDReportHour   =  8;                // Hour of sending the report (Local time)
    input uint                 InpSendDReportMin    =  0;                // Minutes of sending the report (Local time)
    
    input group                "========= Daily reports for specified periods ========="
    input bool                 InpSendSReportDays   =  true;             // Send a report for the specified num days
    input uint                 InpSendSReportDaysN  =  7;                // Number of days to report for the specified number of days
    input bool                 InpSendSReportMonths =  true;             // Send a report for the specified num months
    input uint                 InpSendSReportMonthsN=  3;                // Number of months to report for the specified number of months
    input bool                 InpSendSReportYears  =  true;             // Send a report for the specified num years
    input uint                 InpSendSReportYearN  =  2;                // Number of years to report for the specified number of years
    
    input group                "======== Weekly reports for all other periods ========"
    input ENUM_DAY_OF_WEEK     InpSendWReportDayWeek=  SATURDAY;         // Day of sending the reports (Local time)
    input uint                 InpSendWReportHour   =  8;                // Hour of sending the reports (Local time)
    input uint                 InpSendWReportMin    =  0;                // Minutes of sending the reports (Local time)
    input bool                 InpSendWReport       =  true;             // Send a report for the current week
    input bool                 InpSendMReport       =  false;            // Send a report for the current month
    input bool                 InpSendYReport       =  false;            // Send a report for the current year
    input bool                 InpSendAReport       =  false;            // Send a report for the entire trading period
    
    //+------------------------------------------------------------------+
    //| Глобальные переменные                                            |
    //+------------------------------------------------------------------+
    CAccounts      ExtAccounts;                                          // Объект управления аккаунтами
    long           ExtLogin;                                             // Логин текущего аккаунта
    string         ExtServer;                                            // Сервер текущего аккаунта
    bool           ExtNotify;                                            // Флаг разрешения Push-уведомлений
    
    //+------------------------------------------------------------------+
    //| Service program start function                                   |
    //+------------------------------------------------------------------+
    void OnStart()
      {
      }
    


    Видим, что у нас подключен файл \MQL5\Include\Tools\DateTime.mqh. Это структура, унаследованная от стандартной MqlDateTime:

    //+------------------------------------------------------------------+
    //|                                                     DateTime.mqh |
    //|                             Copyright 2000-2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    //+------------------------------------------------------------------+
    //| Structure CDateTime.                                             |
    //| Purpose: Working with dates and time.                            |
    //|         Extends the MqlDateTime structure.                       |
    //+------------------------------------------------------------------+
    struct CDateTime : public MqlDateTime
      {
       //--- additional information
       string            MonthName(const int num) const;
       string            ShortMonthName(const int num) const;
       string            DayName(const int num) const;
       string            ShortDayName(const int num) const;
       string            MonthName(void)               const { return(MonthName(mon));            }
       string            ShortMonthName(void)          const { return(ShortMonthName(mon));       }
       string            DayName(void)                 const { return(DayName(day_of_week));      }
       string            ShortDayName(void)            const { return(ShortDayName(day_of_week)); }
       int               DaysInMonth(void) const;
       //--- data access
       datetime          DateTime(void)                      { return(StructToTime(this));        }
       void              DateTime(const datetime value)      { TimeToStruct(value,this);          }
       void              DateTime(const MqlDateTime& value)  { this=value;                        }
       void              Date(const datetime value);
       void              Date(const MqlDateTime &value);
       void              Time(const datetime value);
       void              Time(const MqlDateTime &value);
       //--- settings
       void              Sec(const int value);
       void              Min(const int value);
       void              Hour(const int value);
       void              Day(const int value);
       void              Mon(const int value);
       void              Year(const int value);
       //--- increments
       void              SecDec(int delta=1);
       void              SecInc(int delta=1);
       void              MinDec(int delta=1);
       void              MinInc(int delta=1);
       void              HourDec(int delta=1);
       void              HourInc(int delta=1);
       void              DayDec(int delta=1);
       void              DayInc(int delta=1);
       void              MonDec(int delta=1);
       void              MonInc(int delta=1);
       void              YearDec(int delta=1);
       void              YearInc(int delta=1);
       //--- check
       void              DayCheck(void);
      };
    

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

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

    //+------------------------------------------------------------------+
    //| Service program start function                                   |
    //+------------------------------------------------------------------+
    void OnStart()
      {
    //---
       CArrayObj  *PositionsList  =  NULL;          // Список закрытых позиций аккаунтов
       long        account_prev   =  0;             // Прошлый логин
       double      balance_prev   =  EMPTY_VALUE;   // Прошлый баланс
       bool        Sent           =  false;         // Флаг отправленного отчёта за не дневные периоды
       int         day_of_year_prev= WRONG_VALUE;   // Прошлый номер дня в году
       
    //--- Создаём списки торгуемых в истории символов и магиков и список сообщений для Push-уведомлений
       CArrayString  *SymbolsList =  new CArrayString();
       CArrayLong    *MagicsList  =  new CArrayLong();
       CArrayString  *MessageList =  new CArrayString();
       if(SymbolsList==NULL || MagicsList==NULL || MessageList==NULL)
         {
          Print("Failed to create list CArrayObj");
          return;
         }
         
    //--- Проверяем наличие MetaQuotes ID и разрешение отправки на него уведомлений
       ExtNotify=CheckMQID();
       if(ExtNotify)
          Print(MQLInfoString(MQL_PROGRAM_NAME)+"-Service notifications OK");
    
    //--- Основной цикл
       int count=0;
       while(!IsStopped())
         {
          //+------------------------------------------------------------------+
          //| Задержка в цикле                                                 |
          //+------------------------------------------------------------------+
          //--- Увеличиваем счётчик цикла. Если счётчик не превысил заданного значения - повторяем
          Sleep(16);
          count+=10;
          if(count<COUNTER_DELAY)
             continue;
          //--- Ожидание завершено. Сбрасываем счётчик цикла
          count=0;
          
          //+------------------------------------------------------------------+
          //| Проверка настроек уведомлений                                    |
          //+------------------------------------------------------------------+
          //--- Если флаг уведомлений не установлен - проверяем настройки уведомлений в терминале и, если активированы - сообщаем об этом
          if(!ExtNotify && TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
            {
             Print("Now MetaQuotes ID is specified and sending notifications is allowed");
             SendNotification("Now MetaQuotes ID is specified and sending notifications is allowed");
             ExtNotify=true;
            }
          //--- Если флаг уведомлений установлен, но в терминале нет на них разрешения - сообщаем об этом
          if(ExtNotify && (!TerminalInfoInteger(TERMINAL_MQID) || !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)))
            {
             string caption=MQLInfoString(MQL_PROGRAM_NAME);
             string message="The terminal has a limitation on sending notifications. Please check your notification settings";
             MessageBox(message, caption, MB_OK|MB_ICONWARNING);
             ExtNotify=false;
            }
          
          //+------------------------------------------------------------------+
          //| Смена аккаунта                                                   |
          //+------------------------------------------------------------------+
          //--- Если текущий логин не равен предыдущему
          if(AccountInfoInteger(ACCOUNT_LOGIN)!=account_prev)
            {
             //--- если не дождались обновления данных аккаунта - повторим на следующей итерации цикла
             if(!DataUpdateWait(balance_prev))
                continue;
             
             //--- Получены данные нового аккаунта
             //--- Сохраним текущие логин и баланс как предыдущие для следующей проверки
             account_prev=AccountInfoInteger(ACCOUNT_LOGIN);
             balance_prev=AccountInfoDouble(ACCOUNT_BALANCE);
             
             //--- Сбросим флаг отправленного сообщения и вызовем обработчик смены аккаунта
             Sent=false;
             AccountChangeHandler();
            }
          
          //+------------------------------------------------------------------+
          //| Ежедневные отчёты                                                |
          //+------------------------------------------------------------------+
          //--- Заполним структуру данными о локальном времени и дате
          MqlDateTime tm={};
          TimeLocal(tm);
          
          //--- Очистим список сообщений, отправляемых на MQID
          MessageList.Clear();
          
          //--- Если текущий номер дня в году не равен прошлому - это начало нового дня
          if(tm.day_of_year!=day_of_year_prev)
            {
             //--- Если часы/минуты достигли заданных значений для отправки статистики
             if(tm.hour>=(int)InpSendDReportHour && tm.min>=(int)InpSendDReportMin)
               {
                //--- Если разрешена отправка ежедневной статистики
                if(InpSendDReport)
                  {
                   //--- обновляем списки закрытых позиций за сутки на текущем аккаунте
                   ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                   //--- если в настройках задано получение статистики со всех аккаунтов -
                   //--- получаем список закрытых позиций всех аккаунтов, бывших активными при работе сервиса
                   if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                      PositionsList=ExtAccounts.GetCommonPositionsList();
                   //--- иначе - получаем список закрытых позиций только текущего на данный момент аккаунта
                   else
                      PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                   
                   //--- Создаём сообщения о торговой статистике за дневной диапазон времени,
                   //--- распечатываем созданные сообщения в журнал и отправляем их на MQID
                   SendReport(REPORT_RANGE_DAILY, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество дней,
                   //--- Создаём сообщения о торговой статистике за количество дней в InpSendSReportDaysN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportDays)
                      SendReport(REPORT_RANGE_NUM_DAYS, InpSendSReportDaysN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество месяцев,
                   //--- Создаём сообщения о торговой статистике за количество месяцев в InpSendSReportMonthsN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportMonths)
                      SendReport(REPORT_RANGE_NUM_MONTHS, InpSendSReportMonthsN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество лет,
                   //--- Создаём сообщения о торговой статистике за количество лет в InpSendSReportYearN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportYears)
                      SendReport(REPORT_RANGE_NUM_YEARS, InpSendSReportYearN, PositionsList, SymbolsList, MagicsList, MessageList);
                  }
    
                //--- Записываем текущий день как прошлый для последующей проверки
                day_of_year_prev=tm.day_of_year;
               }
            }
            
          //+------------------------------------------------------------------+
          //| Еженедельные отчёты                                              |
          //+------------------------------------------------------------------+
          //--- Если день недели равен устанорвленному в настройках,
          if(tm.day_of_week==InpSendWReportDayWeek)
            {
             //--- если сообщение ещё не отправлено и наступило время отправки сообщений
             if(!Sent && tm.hour>=(int)InpSendWReportHour && tm.min>=(int)InpSendWReportMin)
               {
                //--- обновляем списки закрытых позиций на текущем аккаунте
                ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                
                //--- если в настройках задано получение статистики со всех аккаунтов -
                //--- получаем список закрытых позиций всех аккаунтов, бывших активными при работе сервиса
                if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                   PositionsList=ExtAccounts.GetCommonPositionsList();
                //--- иначе  -получаем список закрытых позиций только текущего на данный момент аккаунта
                else
                   PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                
                //--- Если в настройках разрешена отправка торговой статистики за неделю,
                //--- Создаём сообщения о торговой статистике с начала текущей недели,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendWReport)
                   SendReport(REPORT_RANGE_WEEK_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за месяц,
                //--- Создаём сообщения о торговой статистике с начала текущего месяца,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendMReport)
                   SendReport(REPORT_RANGE_MONTH_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за год,
                //--- Создаём сообщения о торговой статистике с начала текущго года,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendYReport)
                   SendReport(REPORT_RANGE_YEAR_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за весь период,
                //--- Создаём сообщения о торговой статистике с начала эпохи (01.01.1970 00:00),
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendAReport)
                   SendReport(REPORT_RANGE_ALL, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Устанавливаем флаг, что все сообщения со статистикой в журнал распечатаны
                Sent=true;
               }
            }
          //--- Если ещё не наступил указанный в настройках день недели для отправки статистики - сбрасываем флаг отправленных сообщений
          else
             Sent=false;
    
          //--- Если список сообщений для отправки на MQID не пустой - вызываем функцию отправки уведомлений на смартфон
          if(MessageList.Total()>0)
             SendMessage(MessageList);
         }
         
       //+------------------------------------------------------------------+
       //| Завершение работы сервиса                                        |
       //+------------------------------------------------------------------+
       //--- Очищаем и удаляем списки сообщений, символов и магиков
       if(MessageList!=NULL)
         {
          MessageList.Clear();
          delete MessageList;
         }
       if(SymbolsList!=NULL)
         {
          SymbolsList.Clear();
          delete SymbolsList;
         }
       if(MagicsList!=NULL)
         {
          MagicsList.Clear();
          delete MagicsList;
         }
      }
    

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

    //+------------------------------------------------------------------+
    //| Проверяет наличие в терминале MetaQuotes ID                      |
    //| и разрешение отправки уведомлений на мобильный терминал          |
    //+------------------------------------------------------------------+
    bool CheckMQID(void)
      {
       string caption=MQLInfoString(MQL_PROGRAM_NAME); // Заголовок окна сообщений
       string message=caption+"-Service OK";           // Текст окна сообщений
       int    mb_id=IDOK;                              // Код возврата MessageBox()
       
    //--- Если в настройках терминала не установлен MQID - сделаем запрос на его установку с пояснениями о порядке действий
       if(!TerminalInfoInteger(TERMINAL_MQID))
         {
          message="The client terminal does not have a MetaQuotes ID for sending Push notifications.\n"+
                  "1. Install the mobile version of the MetaTrader 5 terminal from the App Store or Google Play.\n"+
                  "2. Go to the \"Messages\" section of your mobile terminal.\n"+
                  "3. Click \"MQID\".\n"+
                  "4. In the client terminal, in the \"Tools - Settings\" menu, in the \"Notifications\" tab, in the MetaQuotes ID field, enter the received code.";
          mb_id=MessageBox(message, caption, MB_RETRYCANCEL|MB_ICONWARNING);
         }
         
    //--- Если нажата кнопка "Cancel" - сообщим об отказе от использования Push-уведомлений
       if(mb_id==IDCANCEL)
         {
          message="You refused to enter your MetaQuotes ID. The service will send notifications to the “Experts” tab of the terminal";
          MessageBox(message, caption, MB_OK|MB_ICONINFORMATION);
         }
         
    //--- Если нажата кнопка "Retry" - 
       else
         {
          //--- Если в терминале установлен MetaQuotes ID для отправки Push-уведомлений
          if(TerminalInfoInteger(TERMINAL_MQID))
            {
             //--- если в терминале отсутствует разрешение на отправку уведомлений на смартфон
             if(!TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
               {
                //--- показываем сообщение с просьбой дать разрешение на отправку уведомлений в настройках
                message="Please enable sending Push notifications in the terminal settings in the \"Notifications\" tab in the \"Tools - Settings\" menu.";
                mb_id=MessageBox(message, caption, MB_RETRYCANCEL|MB_ICONEXCLAMATION);
                
                //--- Если в ответ на сообщение нажата кнопка Cancel
                if(mb_id==IDCANCEL)
                  {
                   //--- сообщаем об отказе от отправки уведомлений на смартфон
                   string message="You have opted out of sending Push notifications. The service will send notifications to the “Experts” tab of the terminal.";
                   MessageBox(message, caption, MB_OK|MB_ICONINFORMATION);
                  }
                //--- Если в ответ на сообщение нажата кнопка Retry (ожидается, что сделано это будет после включения разрешения в настройках),
                //--- но разрешения на отправку уведомлений в терминале так и нет,
                if(mb_id==IDRETRY && !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
                  {
                   //--- сообщаем, что пользователь отказался от отправки уведомлений на смартфон, и сообщения будут только в журнале
                   string message="You have not allowed push notifications. The service will send notifications to the “Experts” tab of the terminal.";
                   MessageBox(message, caption, MB_OK|MB_ICONINFORMATION);
                  }
               }
            }
          //--- Если в терминале не установлен MetaQuotes ID для отправки Push-уведомлений
          else
            {
             //--- сообщаем, что в терминале не установлен MetaQuotes ID для отправки уведомлений на смартфон, и сообщения будут только в журнале
             string message="You have not set your MetaQuotes ID. The service will send notifications to the “Experts” tab of the terminal";
             MessageBox(message, caption, MB_OK|MB_ICONINFORMATION);
            }
         }
    //--- Возвращаем флаг, что MetaQuotes ID в терминале установлен и отправка уведомлений разрешена
       return(TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED));
      }
    


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

          //+------------------------------------------------------------------+
          //| Проверка настроек уведомлений                                    |
          //+------------------------------------------------------------------+
          //--- Если флаг уведомлений не установлен - проверяем настройки уведомлений в терминале и, если активированы - сообщаем об этом
          if(!ExtNotify && TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
            {
             Print("Now MetaQuotes ID is specified and sending notifications is allowed");
             SendNotification("Now MetaQuotes ID is specified and sending notifications is allowed");
             ExtNotify=true;
            }
          //--- Если флаг уведомлений установлен, но в терминале нет на них разрешения - сообщаем об этом
          if(ExtNotify && (!TerminalInfoInteger(TERMINAL_MQID) || !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)))
            {
             string caption=MQLInfoString(MQL_PROGRAM_NAME);
             string message="The terminal has a limitation on sending notifications. Please check your notification settings";
             MessageBox(message, caption, MB_OK|MB_ICONWARNING);
             ExtNotify=false;
            }
    

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

    Далее в цикле идёт проверка на смену аккаунта:

          //+------------------------------------------------------------------+
          //| Смена аккаунта                                                   |
          //+------------------------------------------------------------------+
          //--- Если текущий логин не равен предыдущему
          if(AccountInfoInteger(ACCOUNT_LOGIN)!=account_prev)
            {
             //--- если не дождались обновления данных аккаунта - повторим на следующей итерации цикла
             if(!DataUpdateWait(balance_prev))
                continue;
             
             //--- Получены данные нового аккаунта
             //--- Сохраним текущие логин и баланс как предыдущие для следующей проверки
             account_prev=AccountInfoInteger(ACCOUNT_LOGIN);
             balance_prev=AccountInfoDouble(ACCOUNT_BALANCE);
             
             //--- Сбросим флаг отправленного сообщения и вызовем обработчик смены аккаунта
             Sent=false;
             AccountChangeHandler();
            }
    

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

    //+------------------------------------------------------------------+
    //| Ожидает обновления данных аккаунта                               |
    //+------------------------------------------------------------------+
    bool DataUpdateWait(double &balance_prev)
      {
       int attempts=0;   // Количество попыток
     
    //--- До тех пор пока снят флаг остановки программы и пока количество попыток меньше установленного в REFRESH_ATTEMPTS
       while(!IsStopped() && attempts<REFRESH_ATTEMPTS)
         {
          //--- Если баланс текущего аккаунта отличается от баланса ранее сохранённого значения баланса,
          //--- считаем, что данные аккаунта получить удалось - возвращаем true
          if(NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE)-balance_prev, 8)!=0)
             return true;
          
          //--- Ожидаем полсекунды для следующей попытки, увеличиваем количество попыток и
          //--- выводим в журнал сообщение об ожидании получения данных и количестве попыток
          Sleep(500);
          attempts++;
          PrintFormat("%s::%s: Waiting for account information to update. Attempt %d", MQLInfoString(MQL_PROGRAM_NAME),__FUNCTION__, attempts);
         }
         
    //--- Если по истечении всех попыток получить данные нового аккаунта не удалось,
    //--- сообщаем об этом в журнал, записываем в "прошлый баланс" пустое значение и возвращаем false
       PrintFormat("%s::%s: Could not wait for updated account data... Try again", MQLInfoString(MQL_PROGRAM_NAME),__FUNCTION__);
       balance_prev=EMPTY_VALUE;
       return false;
      }
    

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

    Далее в цикле организованы проверки даты и времени для создания ежедневных и еженедельных отчётов:

          //+------------------------------------------------------------------+
          //| Ежедневные отчёты                                                |
          //+------------------------------------------------------------------+
          //--- Заполним структуру данными о локальном времени и дате
          MqlDateTime tm={};
          TimeLocal(tm);
          
          //--- Очистим список сообщений, отправляемых на MQID
          MessageList.Clear();
          
          //--- Если текущий номер дня в году не равен прошлому - это начало нового дня
          if(tm.day_of_year!=day_of_year_prev)
            {
             //--- Если часы/минуты достигли заданных значений для отправки статистики
             if(tm.hour>=(int)InpSendDReportHour && tm.min>=(int)InpSendDReportMin)
               {
                //--- Если разрешена отправка ежедневной статистики
                if(InpSendDReport)
                  {
                   //--- обновляем списки закрытых позиций за сутки на текущем аккаунте
                   ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                   //--- если в настройках задано получение статистики со всех аккаунтов -
                   //--- получаем список закрытых позиций всех аккаунтов, бывших активными при работе сервиса
                   if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                      PositionsList=ExtAccounts.GetCommonPositionsList();
                   //--- иначе - получаем список закрытых позиций только текущего на данный момент аккаунта
                   else
                      PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                   
                   //--- Создаём сообщения о торговой статистике за дневной диапазон времени,
                   //--- распечатываем созданные сообщения в журнал и отправляем их на MQID
                   SendReport(REPORT_RANGE_DAILY, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество дней,
                   //--- Создаём сообщения о торговой статистике за количество дней в InpSendSReportDaysN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportDays)
                      SendReport(REPORT_RANGE_NUM_DAYS, InpSendSReportDaysN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество месяцев,
                   //--- Создаём сообщения о торговой статистике за количество месяцев в InpSendSReportMonthsN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportMonths)
                      SendReport(REPORT_RANGE_NUM_MONTHS, InpSendSReportMonthsN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество лет,
                   //--- Создаём сообщения о торговой статистике за количество лет в InpSendSReportYearN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportYears)
                      SendReport(REPORT_RANGE_NUM_YEARS, InpSendSReportYearN, PositionsList, SymbolsList, MagicsList, MessageList);
                  }
    
                //--- Записываем текущий день как прошлый для последующей проверки
                day_of_year_prev=tm.day_of_year;
               }
            }
            
          //+------------------------------------------------------------------+
          //| Еженедельные отчёты                                              |
          //+------------------------------------------------------------------+
          //--- Если день недели равен устанорвленному в настройках,
          if(tm.day_of_week==InpSendWReportDayWeek)
            {
             //--- если сообщение ещё не отправлено и наступило время отправки сообщений
             if(!Sent && tm.hour>=(int)InpSendWReportHour && tm.min>=(int)InpSendWReportMin)
               {
                //--- обновляем списки закрытых позиций на текущем аккаунте
                ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                
                //--- если в настройках задано получение статистики со всех аккаунтов -
                //--- получаем список закрытых позиций всех аккаунтов, бывших активными при работе сервиса
                if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                   PositionsList=ExtAccounts.GetCommonPositionsList();
                //--- иначе  -получаем список закрытых позиций только текущего на данный момент аккаунта
                else
                   PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                
                //--- Если в настройках разрешена отправка торговой статистики за неделю,
                //--- Создаём сообщения о торговой статистике с начала текущей недели,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendWReport)
                   SendReport(REPORT_RANGE_WEEK_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за месяц,
                //--- Создаём сообщения о торговой статистике с начала текущего месяца,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendMReport)
                   SendReport(REPORT_RANGE_MONTH_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за год,
                //--- Создаём сообщения о торговой статистике с начала текущго года,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendYReport)
                   SendReport(REPORT_RANGE_YEAR_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за весь период,
                //--- Создаём сообщения о торговой статистике с начала эпохи (01.01.1970 00:00),
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendAReport)
                   SendReport(REPORT_RANGE_ALL, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Устанавливаем флаг, что все сообщения со статистикой в журнал распечатаны
                Sent=true;
               }
            }
          //--- Если ещё не наступил указанный в настройках день недели для отправки статистики - сбрасываем флаг отправленных сообщений
          else
             Sent=false;
    
          //--- Если список сообщений для отправки на MQID не пустой - вызываем функцию отправки уведомлений на смартфон
          if(MessageList.Total()>0)
             SendMessage(MessageList);
    

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

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

    Функция, возвращающая список с указанным диапазоном статистики:

    //+------------------------------------------------------------------+
    //| Возвращает список с указанным диапазоном статистики              |
    //+------------------------------------------------------------------+
    CArrayObj *GetListDataRange(ENUM_REPORT_RANGE range, CArrayObj *list, datetime &time_start, const int num_periods)
      {
    //--- Текущая дата
       CDateTime current={};
       current.Date(TimeLocal());
       
    //--- Дата начала периода
       CDateTime begin_range=current;
       
    //--- Устанавливаем время начала периода в значение 00:00:00
       begin_range.Hour(0);
       begin_range.Min(0);
       begin_range.Sec(0);
       
    //--- В зависимости от указанного периода требуемой статистики, корректируем дату начала периода
       switch(range)
         {
          //--- Сутки
          case REPORT_RANGE_DAILY       :  // уменьшаем значение День на 1
            begin_range.DayDec(1);       
            break;
            
          //--- С начала недели
          case REPORT_RANGE_WEEK_BEGIN  :  // уменьшаем значение День на (количество прошедших дней в неделе)-1
            begin_range.DayDec(begin_range.day_of_week==SUNDAY ? 6 : begin_range.day_of_week-1);
            break;
            
          //--- С начала месяца
          case REPORT_RANGE_MONTH_BEGIN :  // устанавливаем в значение День первое число месяца
            begin_range.Day(1);
            break;
            
          //--- С начала года
          case REPORT_RANGE_YEAR_BEGIN  :  // устанавливаем в значение Месяц первый месяц в году, а в значение День первое число месяца
            begin_range.Mon(1);
            begin_range.Day(1);
            break;
            
          //--- Количество дней
          case REPORT_RANGE_NUM_DAYS    :  // Уменьшаем значение День на указанное количество дней
            begin_range.DayDec(fabs(num_periods));
            break;
            
          //--- Количество месяцев
          case REPORT_RANGE_NUM_MONTHS  :  // Уменьшаем значение Месяц на указанное количество месяцев
            begin_range.MonDec(fabs(num_periods));
            break;
            
          //--- Количество лет
          case REPORT_RANGE_NUM_YEARS   :  // Уменьшаем значение Год на указанное количество лет
            begin_range.YearDec(fabs(num_periods));
            break;
            
          //---REPORT_RANGE_ALL Весь период
          default                       :  // Устанавливаем дату 1970.01.01
            begin_range.Year(1970);
            begin_range.Mon(1);
            begin_range.Day(1);
            break;
         }
         
    //--- Записываем дату начала периода и возвращаем указатель на список позиций,
    //--- время открытия которых больше, либо равно времени начала запрошенного периода
       time_start=begin_range.DateTime();
       return CSelect::ByPositionProperty(list,POSITION_PROP_TIME,time_start,EQUAL_OR_MORE);
      }
    

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


    Функция-обработчик смены аккаунта:

    //+------------------------------------------------------------------+
    //| Обработчик смены аккаунта                                        |
    //+------------------------------------------------------------------+
    void AccountChangeHandler(void)
      {
    //--- Записываем логин и сервер текущего аккаунта
       long   login  = AccountInfoInteger(ACCOUNT_LOGIN);
       string server = AccountInfoString(ACCOUNT_SERVER);
       
    //--- Получаем указатель на объект-аккаунт по данным текущего аккаунта
       CAccount *account = ExtAccounts.Get(login, server);
       
    //--- Если объект пустой - создаём новый объект-аккаунт и получаем указатель на него
       if(account==NULL && ExtAccounts.Create(login, server))
          account=ExtAccounts.Get(login, server);
       
    //--- Если в итоге объект-аккаунт не получен - сообщаем об этом и уходим
       if(account==NULL)
         {
          PrintFormat("Error getting access to account object: %I64d (%s)", login, server);
          return;
         }
       
    //--- Записываем текущие значения логина и сервера из данных объекта-аккаунта
       ExtLogin =account.Login();
       ExtServer=account.Server();
       
    //--- Распечатываем данные аккаунта в журнал и выводим сообщение о начале создания списка закрытых позиций
       account.Print();
       Print("Beginning to create a list of closed positions...");
    
    //--- Создаём список закрытых позиций и по завершении процесса сообщаем в журнал количество созданных позиций и затраченное время
       ulong start=GetTickCount();
       ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
       PrintFormat("A list of %d positions was created in %I64u ms", account.PositionsTotal(), GetTickCount()-start);
      }
    

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


    Функция, создающая статистику за указанный диапазон времени:

    //+------------------------------------------------------------------+
    //| Создаёт статистику за указанный диапазон времени                 |
    //+------------------------------------------------------------------+
    void SendReport(ENUM_REPORT_RANGE range, int num_periods, CArrayObj *list_common, CArrayString *list_symbols, CArrayLong *list_magics, CArrayString *list_msg)
      {
       string array_msg[2] = {NULL, NULL};    // Массив сообщений (0) для выводла в журнал, (1) для отправки на смартфон
       datetime time_start = 0;               // Здесь будем хранить время начала периода статистики
       CArrayObj *list_tmp = NULL;            // Временный список для фильтрации по символам и магикам
       
    //--- Получаем список позиций за период range
       CArrayObj *list_range=GetListDataRange(range, list_common, time_start, num_periods);
       if(list_range==NULL)
          return;
          
    //--- Если список позиций пуст - сообщаем в журнал, что за данный период времени не было торговых транзакций
       if(list_range.Total()==0)
         {
          PrintFormat("\"%s\" no trades",ReportRangeDescription(range, num_periods));
          return;
         }
    
    //--- Предварительно обнулив, создаём списки символов и магиков позиций в полученном списке закрытых позиций за период времени
       list_symbols.Clear();
       list_magics.Clear();
       CreateSymbolMagicLists(list_range, list_symbols, list_magics);
       
    //--- Создаём статистику о закрытых позициях за указанный период,
    //--- распечатываем в журнале созданную статистику из array_msg[0] и
    //--- записываем в список сообщений для Push-уведомлений строку из array_msg[1]
       if(CreateStatisticsMessage(range, num_periods, REPORT_BY_RANGE, MQLInfoString(MQL_PROGRAM_NAME),time_start, list_range, list_symbols, list_magics, 0, array_msg))
         {
          Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_RANGE, time_start));       // Заголовок статистики
          Print(StatisticsTableHeader("Symbols ", InpCommissionsInclude, InpSpreadInclude));  // "Шапка" таблицы
          Print(array_msg[0]);                                                                // Статистика за период времени
          Print("");                                                                          // Отступ строки
          list_msg.Add(array_msg[1]);                                                         // Сохраняем сообщение для Push-уведомлений в список для последующей отправки
         }
       
    //--- Если разрешена статистика раздельно по символам
       if(InpReportBySymbols)
         {
          //--- Выводим в журнал заголовок статистики и "шапку" таблицы
          Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_SYMBOLS, time_start));
          Print(StatisticsTableHeader("Symbol ", InpCommissionsInclude, InpSpreadInclude));
          
          //--- В цикле по списку символов
          for(int i=0; i<list_symbols.Total(); i++)
            {
             //--- получаем наименование очередного символа
             string symbol=list_symbols.At(i);
             if(symbol=="")
                continue;
             //--- фильтруем список позиций, оставляя в нём только позиции с полученным символом
             list_tmp=CSelect::ByPositionProperty(list_range, POSITION_PROP_SYMBOL, symbol, EQUAL);
             
             //--- Создаём статистику о закрытых позициях за указанный период по текущему символу списка,
             //--- распечатываем в журнале созданную статистику из array_msg[0] и
             //--- записываем в список сообщений для Push-уведомлений строку из array_msg[1]
             if(CreateStatisticsMessage(range, num_periods, REPORT_BY_SYMBOLS, MQLInfoString(MQL_PROGRAM_NAME), time_start, list_tmp, list_symbols, list_magics, i, array_msg))
               {
                Print(array_msg[0]);
                list_msg.Add(array_msg[1]);
               }
            }
          //--- По окончании цикла по всем символам выводим в журнал разделительную строку
          Print("");
         }
       
    //--- Если разрешена статистика раздельно по магикам
       if(InpReportByMagics)
         {
          //--- Выводим в журнал заголовок статистики и "шапку" таблицы
          Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_MAGICS, time_start));
          Print(StatisticsTableHeader("Magic ", InpCommissionsInclude, InpSpreadInclude));
          
          //--- В цикле по списку магиков
          for(int i=0; i<list_magics.Total(); i++)
            {
             //--- получаем номер очередного магика
             long magic=list_magics.At(i);
             if(magic==LONG_MAX)
                continue;
             //--- фильтруем список позиций, оставляя в нём только позиции с полученным магиком
             list_tmp=CSelect::ByPositionProperty(list_range, POSITION_PROP_MAGIC, magic, EQUAL);
             
             //--- Создаём статистику о закрытых позициях за указанный период по текущему магику списка,
             //--- распечатываем в журнале созданную статистику из array_msg[0] и
             //--- записываем в список сообщений для Push-уведомлений строку из array_msg[1]
             if(CreateStatisticsMessage(range, num_periods, REPORT_BY_MAGICS, MQLInfoString(MQL_PROGRAM_NAME), time_start, list_tmp, list_symbols, list_magics, i, array_msg))
               {
                Print(array_msg[0]);
                list_msg.Add(array_msg[1]);
               }
            }
          //--- По окончании цикла по всем магикам выводим в журнал разделительную строку
          Print("");
         }
      }
    

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


    Функция, создающая и возвращающая строку "шапки" таблицы:

    //+------------------------------------------------------------------+
    //| Создаёт и возвращает строку "шапки" таблицы                      |
    //+------------------------------------------------------------------+
    string StatisticsTableHeader(const string first, const bool commissions, const bool spreads)
      {
    //--- Объявим и инициализируем заголовки столбцов таблицы
       string h_trades="Trades ";
       string h_long="Long ";
       string h_short="Short ";
       string h_profit="Profit ";
       string h_max="Max ";
       string h_min="Min ";
       string h_avg="Avg ";
       string h_costs="Costs ";
    //--- столбцы таблицы, отключаемые в настройках
       string h_commiss=(commissions ? "Commiss " : "");
       string h_swap=(commissions    ? "Swap "    : "");
       string h_fee=(commissions     ? "Fee "     : "");
       string h_spread=(spreads      ? "Spread "  : "");
    //--- ширина столбцов таблицы
       int w=TABLE_COLUMN_W;
       int c=(commissions ? TABLE_COLUMN_W : 0);
       
    //--- Разделители столбцов таблицы, отключаемых в настройках
       string sep1=(commissions ? "|" : "");
       string sep2=(spreads ? "|" : "");
       
    //--- Создаём строку "шапки" таблицы
       return StringFormat("|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s%s%*s%s%*s%s%*s%s",
                           w,first,
                           w,h_trades,
                           w,h_long,
                           w,h_short,
                           w,h_profit,
                           w,h_max,
                           w,h_min,
                           w,h_avg,
                           w,h_costs,
                           c,h_commiss,sep1,
                           c,h_swap,sep1,
                           c,h_fee,sep1,
                           w,h_spread,sep2);
                           
      }
    

    Функция создаёт строку в виде

    |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    

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

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

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


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

    //+------------------------------------------------------------------+
    //| Возвращает заголовок описания запрашиваемого периода статистики  |
    //+------------------------------------------------------------------+
    string StatisticsRangeTitle(const ENUM_REPORT_RANGE range, const int num_periods, const ENUM_REPORT_BY report_by, const datetime time_start, const string symbol=NULL, const long magic=LONG_MAX)
      {
       string report_by_str=
         (
          report_by==REPORT_BY_SYMBOLS  ?  (symbol==NULL     ?  "by symbols "  :  "by "+symbol+" ") :
          report_by==REPORT_BY_MAGICS   ?  (magic==LONG_MAX  ?  "by magics "   :  "by magic #"+(string)magic+" ") : ""
         );
       return StringFormat("Report %sfor the period \"%s\" from %s", report_by_str,ReportRangeDescription(range, num_periods), TimeToString(time_start, TIME_DATE));
      }
    

    В зависимости от диапазона статистики и от фильтров статистики (по символу, по магику или по дате) создаётся и возвращается строка вида

    Report for the period "3 months" from 2024.04.23 00:00
    

    либо

    Report by symbols for the period "3 months" from 2024.04.23 00:00
    

    либо

    Report by magics for the period "3 months" from 2024.04.23 00:00
    

    и т.д.

    Функция, возвращающая текст сообщения со статистикой:

    //+------------------------------------------------------------------+
    //| Возвращает текст сообщения со статистикой                        |
    //+------------------------------------------------------------------+
    bool CreateStatisticsMessage(const ENUM_REPORT_RANGE range, const int num_periods, const ENUM_REPORT_BY report_by, const string header, const datetime time_start,
                                 CArrayObj *list, CArrayString *list_symbols, CArrayLong *list_magics, const int index, string &array_msg[])
      {
    //--- Получаем из переданных списков по индексу символ и магик
       string   symbol = list_symbols.At(index);
       long     magic  = list_magics.At(index);
    //--- Если переданные списки пусты, или не получены данные из них - возвращаем false
       if(list==NULL || list.Total()==0 || (report_by==REPORT_BY_SYMBOLS && symbol=="") || (report_by==REPORT_BY_MAGICS && magic==LONG_MAX))
          return false;
       
       CPosition  *pos_min  =  NULL;          // Указатель на позицию с минимальным значением свойства
       CPosition  *pos_max  =  NULL;          // Указатель на позицию с максимальным значением свойства
       CArrayObj  *list_tmp =  NULL;          // Указатель на временный список для фильтрации по свойствам
       int         index_min=  WRONG_VALUE;   // Индекс позиции в списке с минимальным значением свойства
       int         index_max=  WRONG_VALUE;   // Индекс позиции в списке с максимальным значением свойства
       
    //--- Получаем из списка позиций суммы свойств позиций
       double profit=PropertyValuesSum(list, POSITION_PROP_PROFIT);            // Общий профит позиций в списке
       double commissions=PropertyValuesSum(list,POSITION_PROP_COMMISSIONS);   // Общая комиссия позиций в списке
       double swap=PropertyValuesSum(list, POSITION_PROP_SWAP);                // Общий своп позиций в списке
       double fee=PropertyValuesSum(list, POSITION_PROP_FEE);                  // Общая оплата за проведение сделок позиций в списке
       double costs=commissions+swap+fee;                                      // Издержки: общая сумма значений всех комиссий
       double spreads=PositionsCloseSpreadCostSum(list);                       // Общие затраты на спред всех позиций в списке
       
    //--- Определяем текстовые описания всех полученных значений
       string s_0=(report_by==REPORT_BY_SYMBOLS ? symbol : report_by==REPORT_BY_MAGICS ? (string)magic : (string)list_symbols.Total())+" ";
       string s_trades=StringFormat("%d ", list.Total());
       string s_profit=StringFormat("%+.2f ", profit);
       string s_costs=StringFormat("%.2f ",costs);
       string s_commiss=(InpCommissionsInclude ? StringFormat("%.2f ",commissions) : "");
       string s_swap=(InpCommissionsInclude ? StringFormat("%.2f ",swap) : "");
       string s_fee=(InpCommissionsInclude ? StringFormat("%.2f ",fee) : "");
       string s_spread=(InpSpreadInclude ? StringFormat("%.2f ",spreads) : "");
       
    //--- Получаем список только длинных позиций и создаём описание их количества
       list_tmp=CSelect::ByPositionProperty(list, POSITION_PROP_TYPE, POSITION_TYPE_BUY, EQUAL);
       string s_long=(list_tmp!=NULL ? (string)list_tmp.Total() : "0")+" ";
       
    //--- Получаем список только коротких позиций и создаём описание их количества
       list_tmp=CSelect::ByPositionProperty(list, POSITION_PROP_TYPE, POSITION_TYPE_SELL, EQUAL);
       string s_short=(list_tmp!=NULL ? (string)list_tmp.Total() : "0")+" ";
       
    //--- Получаем индекс позиции в списке с максимальным профитом и создаём описание полученного значения
       index_max=CSelect::FindPositionMax(list, POSITION_PROP_PROFIT);
       pos_max=list.At(index_max);
       double profit_max=(pos_max!=NULL ? pos_max.Profit() : EMPTY_VALUE);
       string s_max=(profit_max!=EMPTY_VALUE ? StringFormat("%+.2f ",profit_max) : "No trades ");
       
    //--- Получаем индекс позиции в списке с минимальным профитом и создаём описание полученного значения
       index_min=CSelect::FindPositionMin(list, POSITION_PROP_PROFIT);
       pos_min=list.At(index_min);
       double profit_min=(pos_min!=NULL ? pos_min.Profit() : EMPTY_VALUE);
       string s_min=(profit_min!=EMPTY_VALUE ? StringFormat("%+.2f ",profit_min) : "No trades ");
       
    //--- Создаём описание среднего значения профита всех позиций в списке
       string s_avg=StringFormat("%.2f ", PropertyAverageValue(list, POSITION_PROP_PROFIT));
       
    //--- Ширина столбцов таблицы
       int w=TABLE_COLUMN_W;
       int c=(InpCommissionsInclude ? TABLE_COLUMN_W : 0);
    
    //--- Разделители отключаемых в настройках столбцов таблицы
       string sep1=(InpCommissionsInclude ? "|" : "");
       string sep2=(InpSpreadInclude ? "|" : "");
       
    //--- Для вывода в журнал создаём строку со столбцами таблицы, внутри которых расположены полученных выше значения
       array_msg[0]=StringFormat("|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s%s%*s%s%*s%s%*s%s",
                                 w,s_0,
                                 w,s_trades,
                                 w,s_long,
                                 w,s_short,
                                 w,s_profit,
                                 w,s_max,
                                 w,s_min,
                                 w,s_avg,
                                 w,s_costs,
                                 c,s_commiss,sep1,
                                 c,s_swap,sep1,
                                 c,s_fee,sep1,
                                 w,s_spread,sep2);
       
    //--- Для отправки уведомления на MQID создаём строку со столбцами таблицы, внутри которых расположены полученных выше значения
       array_msg[1]=StringFormat("%s:\nTrades: %s Long: %s Short: %s\nProfit: %s Max: %s Min: %s Avg: %s\n%s%s%s%s%s",
                                 StatisticsRangeTitle(range, num_periods, report_by, time_start, (report_by==REPORT_BY_SYMBOLS ? symbol : NULL), (report_by==REPORT_BY_MAGICS ? magic : LONG_MAX)),
                                 s_trades,
                                 s_long,
                                 s_short,
                                 s_profit,
                                 s_max,
                                 s_min,
                                 s_avg,
                                 (costs!=0 ? "Costs: "+s_costs : ""),
                                 (InpCommissionsInclude && commissions!=0 ? " Commiss: "+s_commiss : ""),
                                 (InpCommissionsInclude && swap!=0        ? " Swap: "+s_swap       : ""),
                                 (InpCommissionsInclude && fee!=0         ? " Fee: "+s_fee         : ""),
                                 (InpSpreadInclude      && spreads!=0     ? " Spreads: "+s_spread  : ""));
    //--- Всё успешно
       return true;
      }
    

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


    Функция, заполняющая списки магиков и символов позиций из переданного списка:

    //+------------------------------------------------------------------+
    //| Заполняет списки магиков и символов позиций из переданного списка|
    //+------------------------------------------------------------------+
    void CreateSymbolMagicLists(CArrayObj *list, CArrayString *list_symbols, CArrayLong *list_magics)
      {
    //--- Если передан невалидный указатель на список позиций, либо список пустой - уходим
       if(list==NULL || list.Total()==0)
          return;
       
       int index=WRONG_VALUE;  // Индекс искомого символа или магика в списке
    
    //--- В цикле по списку позиций
       for(int i=0; i<list.Total(); i++)
         {
          //--- получаем указатель на очередную позицию
          CPosition *pos=list.At(i);
          if(pos==NULL)
             continue;
       
          //--- Получаем символ позиции
          string symbol=pos.Symbol();
       
          //--- Списку символов устанавливаем флаг сортированн6ого списка и получаем индекс символа в списке символов
          list_symbols.Sort();
          index=list_symbols.Search(symbol);
          
          //--- Если такого символа в списке нет - добавляем его в список
          if(index==WRONG_VALUE)
             list_symbols.Add(symbol);
       
          //--- Получаем магик позиции
          long magic=pos.Magic();
          
          //--- Списку магиков устанавливаем флаг сортированного списка и получаем индекс магика в списке магиков
          list_magics.Sort();
          index=list_magics.Search(magic);
          
          //--- Если такого магика в списке нет - добавляем его в список
          if(index==WRONG_VALUE)
             list_magics.Add(magic);
         }
      }
    

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

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

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

    //+------------------------------------------------------------------+
    //| Возвращает сумму величин указанного                              |
    //| целочисленного свойства всех позиций в списке                    |
    //+------------------------------------------------------------------+
    long PropertyValuesSum(CArrayObj *list, const ENUM_POSITION_PROPERTY_INT property)
      {
       long res=0;
       int total=list.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=list.At(i);
          res+=(pos!=NULL ? pos.GetProperty(property) : 0);
         }
       return res;
      }
    

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

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

    //+------------------------------------------------------------------+
    //| Возвращает сумму величин указанного                              |
    //| вещественного свойства всех позиций в списке                     |
    //+------------------------------------------------------------------+
    double PropertyValuesSum(CArrayObj *list, const ENUM_POSITION_PROPERTY_DBL property)
      {
       double res=0;
       int total=list.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=list.At(i);
          res+=(pos!=NULL ? pos.GetProperty(property) : 0);
         }
       return res;
      }
    


    На том же принципе создадим функции, возвращающие среднее значение указанного свойства.

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

    //+------------------------------------------------------------------+
    //| Возвращает среднюю величину указанного                           |
    //| целочисленного свойства всех позиций в списке                    |
    //+------------------------------------------------------------------+
    double PropertyAverageValue(CArrayObj *list, const ENUM_POSITION_PROPERTY_INT property)
      {
       long res=0;
       int total=list.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=list.At(i);
          res+=(pos!=NULL ? pos.GetProperty(property) : 0);
         }
       return(total>0 ? (double)res/(double)total : 0);
      }
    


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

    //+------------------------------------------------------------------+
    //| Возвращает среднюю величину указанного                           |
    //| вещественного свойства всех позиций в списке                     |
    //+------------------------------------------------------------------+
    double PropertyAverageValue(CArrayObj *list, const ENUM_POSITION_PROPERTY_DBL property)
      {
       double res=0;
       int total=list.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=list.At(i);
          res+=(pos!=NULL ? pos.GetProperty(property) : 0);
         }
       return(total>0 ? res/(double)total : 0);
      }
    


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

    //+------------------------------------------------------------------+
    //| Возвращает сумму стоимости спредов                               |
    //| сделок закрытия всех позиций в списке                            |
    //+------------------------------------------------------------------+
    double PositionsCloseSpreadCostSum(CArrayObj *list)
      {
       double res=0;
       if(list==NULL)
          return 0;
       int total=list.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=list.At(i);
          res+=(pos!=NULL ? pos.SpreadOutCost() : 0);
         }
       return res;
      }
    

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


    Функция, возвращающая описание периода отчёта:

    //+------------------------------------------------------------------+
    //| Возвращает описание периода отчёта                               |
    //+------------------------------------------------------------------+
    string ReportRangeDescription(ENUM_REPORT_RANGE range, const int num_period)
      {
       switch(range)
         {
          //--- Сутки
          case REPORT_RANGE_DAILY       : return("Daily");
          //---С начала недели
          case REPORT_RANGE_WEEK_BEGIN  : return("Weekly");
          //--- С начала месяца
          case REPORT_RANGE_MONTH_BEGIN : return("Month-to-date");
          //--- С начала года
          case REPORT_RANGE_YEAR_BEGIN  : return("Year-to-date");
          //--- Количество дней
          case REPORT_RANGE_NUM_DAYS    : return StringFormat("%d days", num_period);
          //--- Количество месяцев
          case REPORT_RANGE_NUM_MONTHS  : return StringFormat("%d months", num_period);
          //--- Количество лет
          case REPORT_RANGE_NUM_YEARS   : return StringFormat("%d years", num_period);
          //--- Весь период
          case REPORT_RANGE_ALL         : return("Entire period");
          //--- any other
          default                       : return("Unknown period: "+(string)range);
         }
      }
    

    В зависимости от переданного значения периода отчёта и количества дней/месяцев/лет, создаётся и возвращается строка описания.


    Мы рассмотрели все функции программы сервиса, и саму программу — её главный цикл. Давайте её скомпилируем и запустим сервис. После компиляции, программа будет находиться в окне терминала "Навигатор" в разделе "Сервисы".

    Находим наш сервис, и в меню ПКМ выбираем "Добавить сервис":


    После чего откроется окно настроек программы:



    После запуска сервиса будет создан ежедневный отчёт, включающий

    • общий отчёт за три месяца и отчёт за три месяца в разрезе символов и магиков,
    • общий отчёт за два года и отчёт за два года в разрезе символов и магиков:
    Reporter        -Service notifications OK
    Reporter        68008618: Artem (MetaQuotes Ltd., Demo, 10779.50 USD, Hedging)
    Reporter        Beginning to create a list of closed positions...
    Reporter        A list of 155 positions was created in 8828 ms
    Reporter        "Daily" no trades
    Reporter        "7 days" no trades
    Reporter        Report for the period "3 months" from 2024.04.23 00:00
    Reporter        |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |        2 |       77 |       17 |       60 |  +247.00 |   +36.70 |    -0.40 |     3.20 |     0.00 |     0.00 |     0.00 |     0.00 |     5.10 |
    Reporter        
    Reporter        Report by symbols for the period "3 months" from 2024.04.23 00:00
    Reporter        |   Symbol |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |   EURUSD |       73 |       17 |       56 |  +241.40 |   +36.70 |    -0.40 |     3.30 |     0.00 |     0.00 |     0.00 |     0.00 |     4.30 |
    Reporter        |   GBPUSD |        4 |        0 |        4 |    +5.60 |    +2.20 |    +0.10 |     1.40 |     0.00 |     0.00 |     0.00 |     0.00 |     0.80 |
    Reporter        
    Reporter        Report by magics for the period "3 months" from 2024.04.23 00:00
    Reporter        |    Magic |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |        0 |       75 |       15 |       60 |  +246.60 |   +36.70 |    -0.40 |     3.28 |     0.00 |     0.00 |     0.00 |     0.00 |     4.90 |
    Reporter        | 10879099 |        1 |        1 |        0 |    +0.40 |    +0.40 |    +0.40 |     0.40 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 27394171 |        1 |        1 |        0 |    +0.00 |    +0.00 |    +0.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        
    Reporter        Report for the period "2 years" from 2022.07.23 00:00
    Reporter        |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |        2 |      155 |       35 |      120 |  +779.50 |  +145.00 |   -22.80 |     5.03 |     0.00 |     0.00 |     0.00 |     0.00 |    15.38 |
    Reporter        
    Reporter        Report by symbols for the period "2 years" from 2022.07.23 00:00
    Reporter        |   Symbol |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |   EURUSD |      138 |       30 |      108 |  +612.40 |   +36.70 |   -22.80 |     4.43 |     0.00 |     0.00 |     0.00 |     0.00 |     6.90 |
    Reporter        |   GBPUSD |       17 |        5 |       12 |  +167.10 |  +145.00 |    -7.20 |     9.83 |     0.00 |     0.00 |     0.00 |     0.00 |     8.48 |
    Reporter        
    Reporter        Report by magics for the period "2 years" from 2022.07.23 00:00
    Reporter        |    Magic |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |        0 |      131 |       31 |      100 |  +569.10 |   +36.70 |    -8.50 |     4.34 |     0.00 |     0.00 |     0.00 |     0.00 |     8.18 |
    Reporter        |        1 |        2 |        0 |        2 |    +2.80 |    +1.80 |    +1.00 |     1.40 |     0.00 |     0.00 |     0.00 |     0.00 |     1.80 |
    Reporter        |      123 |        2 |        0 |        2 |    +0.80 |    +0.40 |    +0.40 |     0.40 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |     1024 |        2 |        1 |        1 |    +0.10 |    +0.10 |    +0.00 |     0.05 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |
    Reporter        |   140578 |        1 |        0 |        1 |  +145.00 |  +145.00 |  +145.00 |   145.00 |     0.00 |     0.00 |     0.00 |     0.00 |     4.00 |
    Reporter        |  1114235 |        1 |        0 |        1 |    +2.30 |    +2.30 |    +2.30 |     2.30 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |  1769595 |        1 |        0 |        1 |   +15.00 |   +15.00 |   +15.00 |    15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |  1835131 |        1 |        0 |        1 |    +3.60 |    +3.60 |    +3.60 |     3.60 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |  2031739 |        1 |        0 |        1 |   +15.00 |   +15.00 |   +15.00 |    15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |  2293883 |        1 |        0 |        1 |    +1.40 |    +1.40 |    +1.40 |     1.40 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |  2949243 |        1 |        0 |        1 |   -15.00 |   -15.00 |   -15.00 |   -15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |
    Reporter        | 10879099 |        1 |        1 |        0 |    +0.40 |    +0.40 |    +0.40 |     0.40 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 12517499 |        1 |        1 |        0 |   +15.00 |   +15.00 |   +15.00 |    15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |
    Reporter        | 12976251 |        1 |        0 |        1 |    +2.90 |    +2.90 |    +2.90 |     2.90 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 13566075 |        1 |        0 |        1 |   +15.00 |   +15.00 |   +15.00 |    15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 13959291 |        1 |        0 |        1 |   +15.10 |   +15.10 |   +15.10 |    15.10 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 15728763 |        1 |        0 |        1 |   +11.70 |   +11.70 |   +11.70 |    11.70 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 16121979 |        1 |        0 |        1 |   +15.00 |   +15.00 |   +15.00 |    15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 16318587 |        1 |        0 |        1 |   -15.00 |   -15.00 |   -15.00 |   -15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |
    Reporter        | 16580731 |        1 |        0 |        1 |    +2.10 |    +2.10 |    +2.10 |     2.10 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 21299323 |        1 |        0 |        1 |   -22.80 |   -22.80 |   -22.80 |   -22.80 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |
    Reporter        | 27394171 |        1 |        1 |        0 |    +0.00 |    +0.00 |    +0.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        
    Reporter        Beginning of sending 31 notifications to MQID
    Reporter        10 out of 31 messages sent.
    Reporter        No more than 10 messages per minute! Message limit has been reached. Wait 55 seconds until a minute is up.
    Reporter        20 out of 31 messages sent.
    Reporter        No more than 10 messages per minute! Message limit has been reached. Wait 55 seconds until a minute is up.
    Reporter        30 out of 31 messages sent.
    Reporter        No more than 10 messages per minute! Message limit has been reached. Wait 55 seconds until a minute is up.
    Reporter        Sending 31 notifications completed
    

    По окончании вывода отчётов в журнал, сервис запустил отправку отчётов на смартфон. 31 сообщение было отправлено за 4 раза — по 10 сообщений в минуту.

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

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


    то статистика будет уже в ином виде:

    Reporter        -Service notifications OK
    Reporter        68008618: Artem (MetaQuotes Ltd., Demo, 10779.50 USD, Hedging)
    Reporter        Beginning to create a list of closed positions...
    Reporter        A list of 155 positions was created in 8515 ms
    Reporter        "Daily" no trades
    Reporter        "Weekly" no trades
    Reporter        Report for the period "Month-to-date" from 2024.07.01 00:00
    Reporter        |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |          
    Reporter        |        2 |       22 |        3 |       19 |   +46.00 |    +5.80 |    -0.30 |     2.09 |     0.00 |          
    Reporter        
    Reporter        Report for the period "Year-to-date" from 2024.01.01 00:00
    Reporter        |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |          
    Reporter        |        2 |      107 |       31 |       76 |  +264.00 |   +36.70 |    -7.20 |     2.47 |     0.00 |          
    Reporter        
    Reporter        Report for the period "Entire period" from 1970.01.01 00:00
    Reporter        |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |          
    Reporter        |        2 |      155 |       35 |      120 |  +779.50 |  +145.00 |   -22.80 |     5.03 |     0.00 |          
    Reporter        
    Reporter        Beginning of sending 3 notifications to MQID
    Reporter        Sending 3 notifications completed
    

    Все таблички с отчётами выше печатаются в журнале "Эксперты" терминала.

    На смартфон же приходят отчёты в немного ином виде:


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


    Заключение

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

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

    К статье прикреплены все файлы данного проекта и архив, который можно распаковать в папку терминала MQL5 и сразу же пользоваться программой, предварительно скомпилировав файл Reporter.mq5.


    Прикрепленные файлы |
    Deal.mqh (50.63 KB)
    Position.mqh (68.16 KB)
    Select.mqh (43.49 KB)
    Account.mqh (35.2 KB)
    Accounts.mqh (15.84 KB)
    Reporter.mq5 (99.94 KB)
    MQL5.zip (38.79 KB)
    Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
    theonementor
    theonementor | 31 июл. 2024 в 14:29
    correct me if I'm wrong, why are you re-doing yourself? Account, Position and Select classes, looks awfully similar to what you already have in do easy library! why re-implement what you already have ready? If you needed additional functionality, wouldn't it be better to add the functionality to already existing library and use it?
    Artyom Trishkin
    Artyom Trishkin | 31 июл. 2024 в 14:50
    theonementor # :
    поправьте меня, если я не прав, зачем вы себя переделываете? Классы Account, Position и Select очень похожи на то, что у вас уже есть в библиотеке do easy! зачем заново реализовывать то, что уже готово? Если вам нужен дополнительный функционал, не лучше ли добавить его в уже существующую библиотеку и использовать ее?

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

    theonementor
    theonementor | 1 авг. 2024 в 12:38
    Artyom Trishkin # :

    This article is not related to the library articles. But the concept of constructing objects is taken from the library. Please reread the article to understand.

    will you add this functionality to library later?
    Artyom Trishkin
    Artyom Trishkin | 1 авг. 2024 в 12:45
    theonementor #:
    will you add this functionality to library later?

    Позже, да.

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

    Введение в MQL5 (Часть 5): Функции для работы с массивами для начинающих Введение в MQL5 (Часть 5): Функции для работы с массивами для начинающих
    В пятой статье из нашей серии мы познакомимся с миром массивов в MQL5. Статья предназначена для начинающих. В статье попытаемся упрощенно рассмотреть сложные концепции программирования, чтобы материал был понятен всем. Давайте вместе будем изучать основные концепции, обсуждать вопросы и делиться знаниями!
    Расширенные переменные и типы данных в MQL5 Расширенные переменные и типы данных в MQL5
    Переменные и типы данных — очень важные темы не только в программировании на MQL5, но и в любом языке программирования. Переменные и типы данных MQL5 можно разделить на простые и расширенные. Здесь мы рассмотрим расширенные переменные и типы данных. Простые мы изучали в предыдущей статье.
    Создаем простой мультивалютный советник с использованием MQL5 (Часть 7): Сигналы индикаторов ZigZag и Awesome Oscillator Создаем простой мультивалютный советник с использованием MQL5 (Часть 7): Сигналы индикаторов ZigZag и Awesome Oscillator
    Под мультивалютным советником в этой статье понимается советник, или торговый робот, который использует индикаторы ZigZag и Awesome Oscillator, фильтрующие сигналы друг друга.
    Алгоритм адаптивного социального поведения — Adaptive Social Behavior Optimization (ASBO): Двухфазная эволюция Алгоритм адаптивного социального поведения — Adaptive Social Behavior Optimization (ASBO): Двухфазная эволюция
    Эта статья является продолжением темы социального поведения живых организмов и его воздействия на разработку новой математической модели - ASBO (Adaptive Social Behavior Optimization). Мы погрузимся в двухфазную эволюцию, проведем тестирование алгоритма и сделаем выводы. Подобно тому, как в природе группа живых организмов объединяет свои усилия для выживания, ASBO использует принципы коллективного поведения для решения сложных задач оптимизации.