Мониторинг торговли с помощью Push-уведомлений — пример сервиса в MetaTrader 5
Содержание
- Введение
- Структура проекта
- Класс сделки
- Класс исторической позиции
- Класс для поиска и фильтрации по свойствам сделок и позиций
- Класс коллекция исторических позиций
- Класс аккаунта
- Класс коллекция аккаунтов
- Программа сервис для создания торговых отчётов и отправки уведомлений
- Заключение
Введение
При торговле на финансовых рынках важной составляющей является наличие информации о результатах торгов, проведённых за определённый прошедший период времени.
Наверное, каждый трейдер хотя бы раз сталкивался с необходимостью проконтролировать результаты торговли за прошедший день, неделю, месяц, и т.п., чтобы откорректировать свою стратегию по результатам торговли. Клиентский терминал MetaTrader 5 представляет хорошую статистику в виде отчётов, позволяющих оценить результаты своей торговли в удобной визуальной форме. Отчёт может помочь оптимизировать портфель, понять, как снизить риски и повысить стабильность торговли.
Чтобы проанализировать свою стратегию, нужно нажать "Отчет \ Обзор" в контекстном меню раздела торговой истории или "Отчеты" в меню "Вид" ( либо просто по комбинации клавиш Alt+E):
| |
Подробнее об отчётах в терминале MetaTrader 5 можно почитать в статье "Новый отчет в MetaTrader: 5 самых важных показателей торговли".
Если же по какой-либо причине стандартных отчётов, предоставляемых клиентским терминалом, не достаточно, язык MQL5 предоставляет широкие возможности для создания своих собственных программ, в том числе и для построения отчётов и отправки их на смартфон трейдера. Вот такую возможность мы сегодня и будем обсуждать.
Наша программа должна запускаться в момент запуска терминала, отслеживать смену аккаунта или торгового счёта, наступление дня и времени для создания и отправки отчётов. Для таких целей нам подойдёт тип программы "Сервис".
Как следует из справки, Сервис — программа, которая в отличие от индикаторов, советников и скриптов для своей работы не требует привязки к графику. Как и скрипты, сервисы не обрабатывают никаких событий, кроме события запуска. Для запуска сервиса в его коде обязательно должна быть функция-обработчик OnStart. Сервисы не принимают никаких других событий кроме Start, но могут сами отправлять графикам пользовательские события с помощью EventChartCustom. Сервисы хранятся в директории <каталог_терминала>\MQL5\Services.
Каждый сервис, запущенный в терминале, работает в собственном потоке. А это значит, что зацикленный сервис не может повлиять на работу других программ. Наш сервис должен работать в бесконечном цикле, проверять наступление заданного времени, считывать всю торговую историю, создавать списки закрытых позиций, фильтровать эти списки по разным критериям и выводить по ним отчёты в журнал и в Push уведомлениях на смартфон пользователя. Кроме того, при первом запуске сервиса, или изменении его настроек, сервис должен проверять возможность отправки из терминала Push уведомлений. А для этого должно быть организовано интерактивное взаимодействие с пользователем при помощи окон сообщений с ожиданием ответа и реакции пользователя. Ко всему прочему, при отправке Push уведомлений есть ограничения по частоте уведомлений в единицу времени. Поэтому необходимо организовывать задержки при отправке уведомлений. И всё это никак не должно влиять на работу других приложений, запущенных в клиентском терминале. Исходя из всего перечисленного, Сервисы — самое удобное средство для создания такого проекта.
С типом программы определились. Теперь необходимо составить представление о составляющих, необходимых для сборки всего задуманного воедино.
Структура проекта
Рассмотрим программу, её составляющие, "от конца к началу":
- Программа сервис. Имеет доступ к данным всех аккаунтов, которые были активными за всё время беспрерывной работы сервиса. Из данных всех аккаунтов программа получает списки закрытых позиций и объединяет их в один общий список. В зависимости от настроек, сервис может использовать данные о закрытых позициях только текущего активного аккаунта, либо с текущего и каждого из ранее использованных в торговле в клиентском терминале аккаунтов.
Из данных о закрытых позициях, полученных из списка аккаунтов, создаётся статистика торговли за требуемые торговые периоды и отправляется в Push уведомлениях на смартфон пользователя. Дополнительно торговая статистика в табличном виде выводится в журнал терминала "Эксперты". - Коллекция аккаунтов. Включает в себя список аккаунтов, к которым был подключен терминал за время беспрерывной работы сервиса. Коллекция аккаунтов даёт доступ к любому аккаунту в списке и ко всем закрытым позициям всех аккаунтов. Списки доступны в программе сервисе, и на их основании сервис делает выборки и создаёт статистику.
- Класс объекта аккаунта. Хранит в себе данные одного аккаунта со списком (коллекцией) всех закрытых позиций, транзакции которых были проведены на этом аккаунте за время беспрерывной работы сервиса. Даёт доступ к свойствам аккаунта, к созданию и обновлению списка закрытых позиций этого аккаунта и возвращает списки закрытых позиций по различным критериям выборки.
- Класс коллекция исторических позиций. Содержит список объектов позиций, даёт доступ к свойствам закрытых позиций, к созданию и обновлению списка позиций. Возвращает список закрытых позиций.
- Класс объекта позиции. Хранит свойства закрытой позиции и предоставляет к ним доступ. В объекте организован функционал для сравнения двух объектов по различным свойствам, что даёт возможность для создания списков позиций по различным критериям выборки. Содержит список сделок этой позиции и предоставляет к ним доступ.
- Класс объекта сделки. Хранит свойства одной сделки и предоставляет к ним доступ. В объекте организован функционал для сравнения двух объектов по различным свойствам, что даёт возможность для создания списков сделок по различным критериям выборки.
Концепцию воссоздания закрытой позиции из списка исторических сделок мы рассматривали в статье "Как просматривать сделки прямо на графике и не утонуть в торговой истории". По списку сделок определяется принадлежность каждой сделки к какой-либо позиции по идентификатору позиции (PositionID), записанному в свойствах сделки. Создаётся объект позиции, в который найденные сделки помещаются в список сделок. Здесь будем делать таким же образом. Но для организации построения объектов сделок и позиций будем использовать совсем иную, давно проверенную концепцию, где у каждого объекта есть одинаковые по реализации методы доступа к свойствам для их установки и получения. Такая концепция позволяет создавать объекты в едином общем ключе, хранить их в списках, фильтровать и сортировать по любому из свойств объектов и получать новые списки в разрезе указанного свойства.
Чтобы правильно понимать концепцию построения классов данного проекта, настоятельно рекомендуются к прочтению три статьи, в которых очень подробно описывается:
- структура свойств объектов "(Часть I): Концепция, организация данных, первые результаты",
- структура списков объектов "(Часть II): Коллекция исторических ордеров и сделок" и
- методы фильтрации объектов в списках по свойствам "(Часть 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); }
В методе используется два способа расчёта стоимости спреда:
- если профит позиции не равен нулю, то стоимость спреда рассчитывается по пропорции: размер спреда в пунктах * профит позиции в деньгах / профит позиции в пунктах.
- если профит позиции равен нулю, то стоимость спреда рассчитывается по формуле: рассчитанная стоимость тика * размер спреда в пунктах * объём в сделке.
Метод, устанавливающий совокупную комиссию всех сделок:
//+------------------------------------------------------------------+ //| Устанавливает совокупную комиссию всех сделок | //+------------------------------------------------------------------+ 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.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
поправьте меня, если я не прав, зачем вы себя переделываете? Классы Account, Position и Select очень похожи на то, что у вас уже есть в библиотеке do easy! зачем заново реализовывать то, что уже готово? Если вам нужен дополнительный функционал, не лучше ли добавить его в уже существующую библиотеку и использовать ее?
Эта статья не относится к статьям по библиотеке. А вот концепция построения объектов взята из библиотеки. Пожалуйста, перечитайте статью для понимания.
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?
Позже, да.
Но это не будет повторением того, что написано здесь. Будет возможность конструирования для себя чего-либо из набора методов.