//+------------------------------------------------------------------+ //| Article-15346-MQL5-Trade-Monitor-Push-Service.mq5 | //| Copyright 2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property service #define COUNTER_DELAY 1000 // Задержка счётчика в миллисекундах в рабочем цикле #define REFRESH_ATTEMPTS 5 // Количество попыток получения корректных данных аккаунта #define REFRESH_DELAY 500 // Задержка в миллисекундах перед очередной попыткой получения данных #define TABLE_COLUMN_W 10 // Ширина колонки таблицы статистики для вывода в журнал #include // Динамический массив переменных типа string для объекта списка символов #include // Динамический массив переменных типа long для объекта списка магиков #include // Расширение структуры 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() { //--- 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=(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; } } //+------------------------------------------------------------------+ //| Проверяет наличие в терминале 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)); } //+------------------------------------------------------------------+ //| Ожидает обновления данных аккаунта | //+------------------------------------------------------------------+ bool DataUpdateWait(double &balance_prev) { int attempts=0; // Количество попыток //--- До тех пор пока снят флаг остановки программы и пока количество попыток меньше установленного в REFRESH_ATTEMPTS while(!IsStopped() && attempts0 && i %2!=0) left-=1; //--- Если отправлено 10 уведомлений с момента начала минуты if(count==10) { //--- распечатаем в журнале предупреждающее сообщение о достижении лимита сообщений в минуту PrintFormat("%d out of %d messages sent.\nNo more than 10 messages per minute! "+ "Message limit has been reached. Wait %d seconds until a minute is up.", i+1, list_msg.Total(), left); //--- подождём оставшееся время до истечения текущей минуты и переинициализируем счётчики Sleep(left*1000); count=0; left=60; } } //--- По завершении отправки уведомлений, сообщим об этом в журнале PrintFormat("Sending %d notifications completed", list_msg.Total()); } //+------------------------------------------------------------------+ //| Заполняет списки магиков и символов позиций из переданного списка| //+------------------------------------------------------------------+ 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; i0 ? (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; i0 ? 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