Article-15346-MQL5-Trade-Mo.../Article-15346-MQL5-Trade-Monitor-Push-Service.mq5

952 lines
61 KiB
MQL5
Raw Permalink Normal View History

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:\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;
}
//+------------------------------------------------------------------+
//| Отправляет сообщения из списка на 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.\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;
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);
}
}
//+------------------------------------------------------------------+