2026-03-23 13:23:15 +07:00
//+------------------------------------------------------------------+
2026-03-23 13:10:01 +07:00
//| Article-15346-MQL5-Trade-Monitor-Push-Service.mq5 |
//| Copyright 2026, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
# property service
2026-03-23 13:23:15 +07: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-уведомлений
2026-03-23 13:10:01 +07:00
//+------------------------------------------------------------------+
//| Service program start function |
//+------------------------------------------------------------------+
void OnStart ( )
{
//---
2026-03-23 13:23:15 +07:00
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 ;
}
}
//+------------------------------------------------------------------+
//| Проверяет наличие в терминале 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 ( ) & & 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 ;
}
//+------------------------------------------------------------------+
//| Возвращает список с указанным диапазоном статистики |
//+------------------------------------------------------------------+
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 ( " " ) ;
}
}
//+------------------------------------------------------------------+
//| Создаёт и возвращает строку "шапки" таблицы |
//+------------------------------------------------------------------+
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 ) ;
}
//+------------------------------------------------------------------+
//| Возвращает заголовок описания запрашиваемого периода статистики |
//+------------------------------------------------------------------+
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 ) ) ;
}
//+------------------------------------------------------------------+
//| Возвращает текст сообщения со статистикой |
//+------------------------------------------------------------------+
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: \n Trades: %s Long: %s Short: %s \n Profit: %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 ;
}
//+------------------------------------------------------------------+
//| Отправляет сообщения из списка на MQID |
//+------------------------------------------------------------------+
void SendMessage ( CArrayString * list_msg )
{
//--- Если не разрешена отправка Push-уведомлений или переданный список пуст - уходим
if ( ! ExtNotify | | list_msg = = NULL | | list_msg . Total ( ) = = 0 )
return ;
int total = list_msg . Total ( ) ; // Количество сообщений в списке
int left = 60 ; // Счётчик количества секунд, остающееся до завершения минуты с начала отправки уведомлений
int count = 0 ; // Счётчик количества отправленных уведомлений с начала минуты
//--- Выведем в журнал сообщение о начале отправки уведомлений
PrintFormat ( " Beginning of sending %d notifications to MQID " , list_msg . Total ( ) ) ;
//--- В цикле по количеству сообщений в списке
for ( int i = 0 ; i < total ; i + + )
{
//--- отправляем на смартфон очередное уведомление
SendNotification ( list_msg . At ( i ) ) ;
//--- Увеличиваем счётчик отправленных уведомлений и
//--- ждём полсекунды перед отправкой следующего (≈2 уведомления в секунду)
count + + ;
Sleep ( 500 ) ;
//--- если это не самое первое уведомление, и оно не чётное,
//--- уменьшаем оставшееся время до окончания минуты (в минуту не более 10 уведомлений)
if ( i > 0 & & i % 2 ! = 0 )
left - = 1 ;
//--- Если отправлено 10 уведомлений с момента начала минуты
if ( count = = 10 )
{
//--- распечатаем в журнале предупреждающее сообщение о достижении лимита сообщений в минуту
PrintFormat ( " %d out of %d messages sent. \n No 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 ;
2026-03-23 13:10:01 +07:00
2026-03-23 13:23:15 +07:00
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 ) ;
2026-03-23 13:10:01 +07:00
}
//+------------------------------------------------------------------+
2026-03-23 13:23:15 +07:00
//| Возвращает среднюю величину указанного |
//| вещественного свойства всех позиций в списке |
//+------------------------------------------------------------------+
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 ) ;
}
}
//+------------------------------------------------------------------+