English 中文 Deutsch 日本語
preview
Искусство ведения логов (Часть 6): Сохранение логов в базу данных

Искусство ведения логов (Часть 6): Сохранение логов в базу данных

MetaTrader 5Примеры |
227 0
joaopedrodev
joaopedrodev

Введение

Представьте себе оживленный рынок цифровых сделок и финансового волшебства, где каждое действие отслеживается, записывается и тщательно анализируется для достижения успеха. Что если бы вы могли не только получить доступ к хронике каждого решения и ошибки, допущенной вашими советниками, но и использовать мощный инструмент для оптимизации и совершенствования торговых роботов в реальном времени? В первой части серии - "Искусство ведения логов (Часть 1): Основные понятия и первые шаги в MQL5", мы приступили к созданию пользовательской библиотеки журналов для разработки советников.

Мы преодолели ограничения стандартного интерфейса логирования MetaTrader 5, чтобы создать надежное, настраиваемое и динамичное решение для логирования, которое расширяет возможности MQL5. Наш путь начался с внедрения важнейших требований: надежной структуры Singleton для обеспечения согласованности кода, расширенных журналов базы данных для всестороннего аудита, универсальной гибкости вывода, классификации уровней логирования и настраиваемых форматов для удовлетворения разнообразных потребностей проекта.

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

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


Что такое базы данных?

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

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


Структура базы данных

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

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

  • Таблицы - является основой базы данных. Работает как электронная таблица, где мы группируем связанные данные. В случае с логами мы могли бы создать таблицу под названием "logs", предназначенную исключительно для хранения этих записей.

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

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

  • id → уникальный идентификатор журнала
  • timestamp → дата и время записи
  • level → уровень логирования (DEBUG, INFO, ERROR...)
  • message → сообщение в логе
  • source → источник лога (какая система или модуль сгенерировали запись)

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

  • Строки - если столбцы определяют, какая информация будет храниться, то строки представляют собой отдельные записи в таблице. Каждая строка содержит полный набор значений, заполняющих соответствующие столбцы. Рассмотрим типовую таблицу логов:

    ID Timestamp Level Message Origin
    1
     2025-02-12 10:15 DEBUG Расчетное значение индикатора RSI: 72.56
    Индикаторы
    2  2025-02-12 10:16 INFO Ордер на покупку успешно отправлен
    Управление ордерами
    3  2025-02-12 10:17 ALERT Стоп-лосс скорректирован до уровня безубыточности
    Управление рисками
    4  2025-02-12 10:18 ERROR Не удалось отправить ордер на продажу
    Управление ордерами
    5  2025-02-12 10:19 FATAL Не удалось инициализировать советника: Неверные настройки
    Инициализация

    Каждая строка представляет собой отдельную запись, описывающую конкретное событие.

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


Базы данных в MQL5

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

В отличие от языков, предназначенных для веб- или корпоративных приложений, MQL5 не имеет встроенной поддержки надежных реляционных баз данных, таких как MySQL или PostgreSQL. Но это не значит, что мы обязаны довольствоваться текстовыми файлами! Мы можем обойти это ограничение двумя способами:

Используя SQLite, легковесную файловую базу данных, которая изначально поддерживается в MQL5 (см. функции базы данных в MQL5), или устанавливая внешние соединения через API, что позволяет интегрироваться с более мощными базами данных. Для наших целей эффективного хранения и запроса логов SQLite является идеальным выбором. Это простое, быстрое решение, не требующее выделенного сервера, что делает его идеальным для наших нужд. Прежде чем перейти к реализации, давайте лучше разберемся в характеристиках базы данных, основанной на файле .sqlite.

  • Преимущества
    • Не требуется сервер: SQLite - это «встроенная» база данных, то есть она не требует установки или настройки сервера.
    • Готова к использованию: просто создайте файл .sqlite и начните сохранять данные.
    • Быстрое чтение: поскольку данные хранятся в одном файле, SQLite может очень быстро считывать небольшие и средние объемы данных.
    • Низкая задержка: Для простых запросов база данных может быть быстрее, чем традиционные реляционные базы данных.
    • Высокая совместимость: Совместима с несколькими языками программирования
  • Недостатки
    • Риск повреждения файла: Если файл поврежден, восстановление данных может быть сложным.
    • Резервное копирование вручную: Резервное копирование необходимо выполнить путем копирования файла .sqlite, поскольку SQLite не имеет встроенной поддержки автоматической репликации.
    • Плохо масштабируется: Для больших объемов данных и одновременного доступа SQLite — не лучший вариант. Но поскольку наша цель — хранить журналы локально, это не будет проблемой.

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


Основы работы с базами данных, которые нам необходимы

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

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

Для начала создадим простой тестовый советник DatabaseTest.mq5 внутри папки Experts/Logify. После создания файла у нас получится что-то подобное:

//+------------------------------------------------------------------+
//|                                                 DatabaseTest.mq5 |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/en/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/en/users/joaopedrodev"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+


Создание базы данных и подключение к ней

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

  • filename - имя файла базы данных относительно папки MQL5\\\\Files.
  • flags - комбинация флагов из перечисления ENUM_DATABASE_OPEN_FLAGS. Эти флаги определяют способ доступа к базе данных. Доступные флаги:
    • DATABASE_OPEN_READONLY - доступ только для чтения.
    • DATABASE_OPEN_READWRITE - разрешает чтение и запись.
    • DATABASE_OPEN_CREATE - создает базу данных на диске, если она не существует.
    • DATABASE_OPEN_MEMORY - создает временную базу данных в памяти.
    • DATABASE_OPEN_COMMON - файл будет сохранен в папке, общей для всех терминалов.

В нашем примере мы будем использовать DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE. Таким образом, мы гарантируем автоматическое создание базы данных, если она еще не существует, избегая ручных проверок.

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

Теперь наш код выглядит так:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Opening a database connection
   int dbHandle = DatabaseOpen(path,DATABASE_OPEN_READWRITE|DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Database error (Code: "+IntegerToString(GetLastError())+")");
      return(INIT_FAILED);
     }
   Print("Open database file");
   
   //--- Closing database after use
   DatabaseClose(handle_db);
   Print("Closed database file");
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

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


Создание таблицы

Но прежде чем создавать таблицы без каких-либо критериев, нам нужно проверить, что их еще не существует. Для этого мы используем функцию DatabaseTableExists(). Если таблицы еще не существует в базе данных, создаем ее с помощью простой SQL-команды. SQL (Structured Query Language) - это язык, используемый для взаимодействия с базами данных, позволяющий вставлять, запрашивать, изменять или удалять данные. SQL можно сравнить с "ресторанным меню" для баз данных: вы делаете заказ (SQL-запрос) и получаете именно то, что заказывали — при условии, конечно, что заказ был правильно сформулирован!

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

Для наших целей нам достаточно знать всего несколько команд SQL, первая из которых используется для создания таблицы (create table):

CREATE TABLE {table_name} ({column_name} {type_data}, …);
  • {table_name} - название создаваемой таблицы.
  • {column_name} {type_data} - определение столбцов, где {type_data} указывает тип данных (текст, число, дата и т. д.).

Теперь мы воспользуемся функцией DatabaseExecute() для выполнения команды создания таблицы. Структура таблицы будет основана на структуре MqlLogifyModel и будет содержать следующие поля:

  • id - уникальный идентификатор строки.
  • formated - отформатированное сообщение.
  • levelname - название уровня логирования.
  • msg - оригинальное сообщение.
  • args - аргументы сообщения.
  • timestamp - дата и время в числовом формате.
  • date_time - отформатированные дата и время.
  • level - уровень значимости записи в логах.
  • origin - источник лога.
  • filename - имя исходного файла.
  • function - функция, в которой был сгенерирован лог.
  • line - строка кода, в которой был сгенерирован лог.

Теперь наш код выглядит так:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Open the database connection
   int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")");
      return(INIT_FAILED);
     }
   Print("[INFO] Database connection opened successfully");
   
   //--- Create the 'logs' table if it does not exist
   if(!DatabaseTableExists(dbHandle, "logs"))
     {
      DatabaseExecute(dbHandle,
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT," // Auto-incrementing unique ID
         "formated TEXT,"     // Formatted log message
         "levelname TEXT,"    // Log level (INFO, ERROR, etc.)
         "msg TEXT,"          // Main log message
         "args TEXT,"         // Additional details
         "timestamp BIGINT,"  // Log event timestamp (Unix time)
         "date_time DATETIME,"// Human-readable date and time
         "level BIGINT,"      // Log level as an integer
         "origin TEXT,"       // Module or component name
         "filename TEXT,"     // Source file name
         "function TEXT,"     // Function where the log was recorded
         "line BIGINT);");    // Source code line number
      Print("[INFO] 'logs' table created successfully");
     }
   
   //--- Close the database connection
   DatabaseClose(dbHandle);
   Print("[INFO] Database connection closed successfully");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

На этом завершается этап создания базы данных и таблицы logs. После создания таблицы файл базы данных должен появиться в папке Files в проводнике:

При нажатии на файл, поддерживаемый MetaEditor, он должен открыться в окне, похожем на следующее:

Здесь представлен интерфейс, позволяющий просматривать базу данных и выполнять различные SQL-команды (показано красным). Мы будем часто использовать эту функцию для просмотра данных в редакторе.


Как добавить данные в базу

В SQL для вставки данных в таблицу используется следующая команда:

INSERT INTO {table_name} ({column}, ...) VALUES ({value}, ...)

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

  • DatabasePrepare() - создать ID для SQL-запроса, чтобы подготовить его к последующему выполнению. Это служит первым шагом для интерпретации запроса базой данных.
  • DatabaseBind() - используя эту функцию, вы связываете реальные значения с параметрами запроса. В команде SQL значения представляются заполнителями (например, ?1 , ?2 , etc.), которые будут заменены данными, предоставленными во время выполнения.
  • DatabaseRead() - выполнить подготовленный запрос. В случае команд, которые не возвращают записи (например, INSERT), функция обеспечивает выполнение инструкции и переход к следующей записи, если это необходимо.
  • DatabaseFinalize() - после использования важно освободить ресурсы, связанные с запросом. Эта функция завершает обработку ранее подготовленного запроса, предотвращая утечки памяти.

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

INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);

Обратите внимание, что в таблице перечислены все поля, за исключением поля id, которое автоматически генерируется базой данных. Кроме того, значения, которые необходимо вставить, обозначаются символами ?1 , ?2 и т.д., каждый заполнитель соответствует индексу, который впоследствии будет использоваться для сопоставления реального значения с помощью функции DatabaseBind().

//--- Prepare SQL statement for inserting a log entry
string sql = "INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) "
             "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);";
int sqlRequest = DatabasePrepare(dbHandle, sql);
if(sqlRequest == INVALID_HANDLE)
  {
   Print("[ERROR] Failed to prepare SQL statement for log insertion");
  }

//--- Bind values to the SQL statement
DatabaseBind(sqlRequest, 0, "06:24:00 [INFO] Buy order sent successfully"); // Formatted log message
DatabaseBind(sqlRequest, 1, "INFO");                                        // Log level name
DatabaseBind(sqlRequest, 2, "Buy order sent successfully");                 // Main log message
DatabaseBind(sqlRequest, 3, "Symbol: EURUSD, Volume: 0.1");                  // Additional details
DatabaseBind(sqlRequest, 4, 1739471040);                                     // Unix timestamp
DatabaseBind(sqlRequest, 5, "2025.02.13 18:24:00");                          // Readable date and time
DatabaseBind(sqlRequest, 6, 1);                                              // Log level as integer
DatabaseBind(sqlRequest, 7, "Order Management");                             // Module or component name
DatabaseBind(sqlRequest, 8, "File.mq5");                                     // Source file name
DatabaseBind(sqlRequest, 9, "OnInit");                                       // Function name
DatabaseBind(sqlRequest, 10, 100);                                           // Line number
После привязки всех значений, функция DatabaseRead() используется для выполнения подготовленного запроса. В случае успешного выполнения выводится сообщение с подтверждением, в противном случае сообщается об ошибке.
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Open the database connection
   int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")");
      return(INIT_FAILED);
     }
   Print("[INFO] Database connection opened successfully");
   
   //--- Create the 'logs' table if it does not exist
   if(!DatabaseTableExists(dbHandle, "logs"))
     {
      DatabaseExecute(dbHandle,
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT," // Auto-incrementing unique ID
         "formated TEXT,"     // Formatted log message
         "levelname TEXT,"    // Log level (INFO, ERROR, etc.)
         "msg TEXT,"          // Main log message
         "args TEXT,"         // Additional details
         "timestamp BIGINT,"  // Log event timestamp (Unix time)
         "date_time DATETIME,"// Human-readable date and time
         "level BIGINT,"      // Log level as an integer
         "origin TEXT,"       // Module or component name
         "filename TEXT,"     // Source file name
         "function TEXT,"     // Function where the log was recorded
         "line BIGINT);");    // Source code line number
      Print("[INFO] 'logs' table created successfully");
     }
   
   //--- Prepare SQL statement for inserting a log entry
   string sql = "INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) "
                "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);";
   int sqlRequest = DatabasePrepare(dbHandle, sql);
   if(sqlRequest == INVALID_HANDLE)
     {
      Print("[ERROR] Failed to prepare SQL statement for log insertion");
     }
   
   //--- Bind values to the SQL statement
   DatabaseBind(sqlRequest, 0, "06:24:00 [INFO] Buy order sent successfully"); // Formatted log message
   DatabaseBind(sqlRequest, 1, "INFO");                                        // Log level name
   DatabaseBind(sqlRequest, 2, "Buy order sent successfully");                 // Main log message
   DatabaseBind(sqlRequest, 3, "Symbol: EURUSD, Volume: 0.1");                  // Additional details
   DatabaseBind(sqlRequest, 4, 1739471040);                                     // Unix timestamp
   DatabaseBind(sqlRequest, 5, "2025.02.13 18:24:00");                          // Readable date and time
   DatabaseBind(sqlRequest, 6, 1);                                              // Log level as integer
   DatabaseBind(sqlRequest, 7, "Order Management");                             // Module or component name
   DatabaseBind(sqlRequest, 8, "File.mq5");                                     // Source file name
   DatabaseBind(sqlRequest, 9, "OnInit");                                       // Function name
   DatabaseBind(sqlRequest, 10, 100);                                           // Line number
   
   //--- Execute the SQL statement
   if(!DatabaseRead(sqlRequest))
     {
      Print("[ERROR] SQL insertion request failed");
     }
   else
     {
      Print("[INFO] Log entry inserted successfully");
     }
   
   //--- Close the database connection
   DatabaseClose(dbHandle);
   Print("[INFO] Database connection closed successfully");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
При запуске этого советника в консоли будут отображаться следующие сообщения:
[INFO] Database file opened successfully
[INFO] Table 'logs' created successfully
[INFO] Log entry inserted successfully
[INFO] Database file closed successfully

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



Как считывать данные из базы

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

  1. Подготовки SQL-запроса: используя функцию DatabasePrepare(), мы создаем идентификатор запроса, который должен быть выполнен.
  2. Выполнения запроса: С помощью подготовленного идентификатора функция DatabaseRead() выполняет запрос и устанавливает курсор на первую запись результата.
  3. Извлечения данных: Используя текущую запись, вы получаете значения каждого столбца из определенных функций в соответствии с ожидаемым типом данных. К этим функциям относятся:
    • DatabaseColumnText() - получает значение поля текущей записи в виде строки
    • DatabaseColumnInteger() - получает целочисленное значение из текущей записи
    • DatabaseColumnLong() - получает значение long из текущей записи
    • DatabaseColumnDouble() - получает значение double из текущей записи
    • DatabaseColumnBlob() - получает значение поля текущей записи в виде массива

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

Например, предположим, вы хотите получить все записи из таблицы логов. SQL-запрос для этой операции довольно прост:

SELECT * FROM logs

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

В итоге код выглядит так:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Open the database connection
   int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")");
      return INIT_FAILED;
     }
   Print("[INFO] Database connection opened successfully.");

   //--- Create the 'logs' table if it doesn't exist
   if(!DatabaseTableExists(dbHandle, "logs"))
     {
      string createTableSQL =
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT,"    // Auto-incrementing unique ID
         "formated TEXT,"                           // Formatted log message
         "levelname TEXT,"                          // Log level name (INFO, ERROR, etc.)
         "msg TEXT,"                                // Main log message
         "args TEXT,"                               // Additional arguments/details
         "timestamp BIGINT,"                        // Timestamp of the log event
         "date_time DATETIME,"                      // Human-readable date and time
         "level BIGINT,"                            // Log level as an integer
         "origin TEXT,"                             // Module or component name
         "filename TEXT,"                           // Source file name
         "function TEXT,"                           // Function where the log was recorded
         "line BIGINT);";                           // Line number in the source code

      DatabaseExecute(dbHandle, createTableSQL);
      Print("[INFO] 'logs' table created successfully.");
     }

   //--- Prepare SQL statement to retrieve log entries
   string sqlQuery = "SELECT * FROM logs";
   int sqlRequest = DatabasePrepare(dbHandle, sqlQuery);
   if(sqlRequest == INVALID_HANDLE)
     {
      Print("[ERROR] Failed to prepare SQL statement.");
     }

   //--- Execute the SQL statement
   if(!DatabaseRead(sqlRequest))
     {
      Print("[ERROR] SQL query execution failed.");
     }
   else
     {
      Print("[INFO] SQL query executed successfully.");

      //--- Bind SQL query results to the log data model
      MqlLogifyModel logData;
      DatabaseColumnText(sqlRequest, 1, logData.formated);
      DatabaseColumnText(sqlRequest, 2, logData.levelname);
      DatabaseColumnText(sqlRequest, 3, logData.msg);
      DatabaseColumnText(sqlRequest, 4, logData.args);
      DatabaseColumnLong(sqlRequest, 5, logData.timestamp);

      string dateTimeStr;
      DatabaseColumnText(sqlRequest, 6, dateTimeStr);
      logData.date_time = StringToTime(dateTimeStr);

      DatabaseColumnInteger(sqlRequest, 7, logData.level);
      DatabaseColumnText(sqlRequest, 8, logData.origin);
      DatabaseColumnText(sqlRequest, 9, logData.filename);
      DatabaseColumnText(sqlRequest, 10, logData.function);
      DatabaseColumnLong(sqlRequest, 11, logData.line);

      Print("[INFO] Data retrieved: Formatted = ", logData.formated, " | Level = ", logData.level, " | Origin = ", logData.origin);
     }

   //--- Close the database connection
   DatabaseClose(dbHandle);
   Print("[INFO] Database connection closed successfully.");

   return INIT_SUCCEEDED;
  }
//+------------------------------------------------------------------+

После выполнения кода мы получаем следующий результат:

[INFO] Database file opened successfully
[INFO] SQL request successfully
[INFO] Data read! | Formated: 06:24:00 [INFO] Buy order sent successfully | Level: 1 | Origin: Order Management
[INFO] Database file closed successfully

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


Настройка обработчика базы данных

Для использования базы данных для хранения логов нам необходимо правильно настроить обработчик. Настройка включает в себя определение атрибутов структуры конфигурации, аналогично тому, что мы сделали с обработчиком файлов. Создадим структуру конфигурации MqlLogifyHandleDatabaseConfig, скопируем ее и внесем несколько изменений:

struct MqlLogifyHandleDatabaseConfig
  {
   string directory;                         // Directory for log files
   string base_filename;                     // Base file name
   ENUM_LOG_FILE_EXTENSION file_extension;   // File extension type
   ENUM_LOG_ROTATION_MODE rotation_mode;     // Rotation mode
   int messages_per_flush;                   // Messages before flushing
   uint codepage;                            // Encoding (e.g., UTF-8, ANSI)
   ulong max_file_size_mb;                   // Max file size in MB for rotation
   int max_file_count;                       // Max number of files before deletion
   
   //--- Default constructor
   MqlLogifyHandleDatabaseConfig(void)
     {
      directory = "logs";                    // Default directory
      base_filename = "expert";              // Default base name
      file_extension = LOG_FILE_EXTENSION_LOG;// Default to .log extension
      rotation_mode = LOG_ROTATION_MODE_SIZE;// Default size-based rotation
      messages_per_flush = 100;              // Default flush threshold
      codepage = CP_UTF8;                    // Default UTF-8 encoding
      max_file_size_mb = 5;                  // Default max file size in MB
      max_file_count = 10;                   // Default max file count
     }
  };

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

//+------------------------------------------------------------------+
//| Struct: MqlLogifyHandleDatabaseConfig                            |
//+------------------------------------------------------------------+
struct MqlLogifyHandleDatabaseConfig
  {
   string directory;                         // Directory for log files
   string base_filename;                     // Base file name
   int messages_per_flush;                   // Messages before flushing
   
   //--- Default constructor
   MqlLogifyHandleDatabaseConfig(void)
     {
      directory = "logs";                    // Default directory
      base_filename = "expert";              // Default base name
      messages_per_flush = 100;              // Default flush threshold
     }
   
   //--- Destructor
   ~MqlLogifyHandleDatabaseConfig(void)
     {
     }

   //--- Validate configuration
   bool ValidateConfig(string &error_message)
     {
      //--- Saves the return value
      bool is_valid = true;
      
      //--- Check if the directory is not empty
      if(directory == "")
        {
         directory = "logs";
         error_message = "The directory cannot be empty.";
         is_valid = false;
        }
      
      //--- Check if the base filename is not empty
      if(base_filename == "")
        {
         base_filename = "expert";
         error_message = "The base filename cannot be empty.";
         is_valid = false;
        }
      
      //--- Check if the number of messages per flush is positive
      if(messages_per_flush <= 0)
        {
         messages_per_flush = 100;
         error_message = "The number of messages per flush must be greater than zero.";
         is_valid = false;
        }
   
      //--- No errors found
      return(is_valid);
     }
  };

Теперь мы наконец можем приступить к реализации обработчика.


Реализация обработчика базы данных

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

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

class CLogifyHandlerDatabase : public CLogifyHandler
  {
private:
   //--- Config
   MqlLogifyHandleDatabaseConfig m_config;
   
   //--- Update utilities
   CIntervalWatcher  m_interval_watcher;
   
   //--- Cache data
   MqlLogifyModel    m_cache[];
   int               m_index_cache;
   
public:
                     CLogifyHandlerDatabase(void);
                    ~CLogifyHandlerDatabase(void);
   
   //--- Configuration management
   void              SetConfig(MqlLogifyHandleDatabaseConfig &config);
   MqlLogifyHandleDatabaseConfig GetConfig(void);
   
   virtual void      Emit(MqlLogifyModel &data);         // Processes a log message and sends it to the specified destination
   virtual void      Flush(void);                        // Clears or completes any pending operations
   virtual void      Close(void);                        // Closes the handler and releases any resources
  };

Конструктор инициализирует атрибуты, гарантируя, что имя обработчика равно database, устанавливает интервал для m_interval_watcher и очищает кэш. В деструкторе мы вызываем метод Close(), гарантируя, что все ожидающие записи в лог будут сделаны до завершения создания объекта.

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

CLogifyHandlerDatabase::CLogifyHandlerDatabase(void)
  {
   m_name = "database";
   m_interval_watcher.SetInterval(PERIOD_D1);
   ArrayFree(m_cache);
   m_index_cache = 0;
  }
CLogifyHandlerDatabase::~CLogifyHandlerDatabase(void)
  {
   this.Close();
  }
void CLogifyHandlerDatabase::SetConfig(MqlLogifyHandleDatabaseConfig &config)
  {
   m_config = config;
   
   string err_msg = "";
   if(!m_config.ValidateConfig(err_msg))
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg);
     }
  }
MqlLogifyHandleDatabaseConfig CLogifyHandlerDatabase::GetConfig(void)
  {
   return(m_config);
  }

Теперь перейдем к сути обработчика базы данных — прямому сохранению записей журнала. Для этого мы реализуем три основных метода каждого обработчика:

  • Emit(MqlLogifyModel &data) обрабатывает сообщение журнала и отправляет его в кэш.
  • Flush() завершает или очищает любые операции, добавляя информацию в нужное место назначения (файл, консоль, база данных и т. д.).
  • Close() закрывает обработчик и освобождает все связанные с ним ресурсы.

Начиная с метода Emit(), который отвечает за добавление данных в кэш, если достигнут заданный лимит, вызывается метод Flush().

//+------------------------------------------------------------------+
//| Processes a log message and sends it to the specified destination|
//+------------------------------------------------------------------+
void CLogifyHandlerDatabase::Emit(MqlLogifyModel &data)
  {
   //--- Checks if the configured level allows
   if(data.level >= this.GetLevel())
     {
      //--- Resize cache if necessary
      int size = ArraySize(m_cache);
      if(size != m_config.messages_per_flush)
        {
         ArrayResize(m_cache, m_config.messages_per_flush);
         size = m_config.messages_per_flush;
        }
      
      //--- Add log to cache
      m_cache[m_index_cache++] = data;
      
      //--- Flush if cache limit is reached or update condition is met
      if(m_index_cache >= m_config.messages_per_flush || m_interval_watcher.Inspect())
        {
         //--- Save cache
         Flush();
         
         //--- Reset cache
         m_index_cache = 0;
         for(int i=0;i<size;i++)
           {
            m_cache[i].Reset();
           }
        }
     }
  }
//+------------------------------------------------------------------+

Продолжая использовать метод Flush(), считываем данные из кэша и добавляем их в базу данных, следуя той же структуре, которую я описывал в начале статьи, в разделе "Как добавлять данные в базу", используя функцию DatabasePrepare().

//+------------------------------------------------------------------+
//| Clears or completes any pending operations                       |
//+------------------------------------------------------------------+
void CLogifyHandlerDatabase::Flush(void)
  {
   //--- Get the full path of the file
   string path = m_config.directory+"\\"+m_config.base_filename+".sqlite";
   
   //--- Open database
   ResetLastError();
   int handle_db = DatabaseOpen(path,DATABASE_OPEN_CREATE|DATABASE_OPEN_READWRITE);
   if(handle_db == INVALID_HANDLE)
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+path+"'. Убедимся, что каталог существует и доступен для записи. (Code: "+IntegerToString(GetLastError())+")");
      return;
     }
   
   if(!DatabaseTableExists(handle_db,"logs"))
     {
      DatabaseExecute(handle_db,
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT,"
         "formated TEXT,"
         "levelname TEXT,"
         "msg TEXT,"
         "args TEXT,"
         "timestamp BIGINT,"
         "date_time DATETIME,"
         "level BIGINT,"
         "origin TEXT,"
         "filename TEXT,"
         "function TEXT,"
         "line BIGINT);");
     }
   
   //--- 
   string sql="INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);"; // parâmetro de consulta
   int request = DatabasePrepare(handle_db,sql);
   if(request == INVALID_HANDLE)
     {
      Print("Erro");
     }
   
   //--- Loop through all cached messages
   int size = ArraySize(m_cache);
   for(int i=0;i<size;i++)
     {
      if(m_cache[i].timestamp > 0)
        {
         DatabaseBind(request,0,m_cache[i].formated);
         DatabaseBind(request,1,m_cache[i].levelname);
         DatabaseBind(request,2,m_cache[i].msg);
         DatabaseBind(request,3,m_cache[i].args);
         DatabaseBind(request,4,m_cache[i].timestamp);
         DatabaseBind(request,5,TimeToString(m_cache[i].date_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS));
         DatabaseBind(request,6,(int)m_cache[i].level);
         DatabaseBind(request,7,m_cache[i].origin);
         DatabaseBind(request,8,m_cache[i].filename);
         DatabaseBind(request,9,m_cache[i].function);
         DatabaseBind(request,10,m_cache[i].line);
         DatabaseRead(request);
         DatabaseReset(request);
        }
     }
   
   //--- 
   DatabaseFinalize(request);
   
   //--- Close database
   DatabaseClose(handle_db);
  }
//+------------------------------------------------------------------+

Наконец, функция Close() гарантирует запись всех отложенных логов в журнал перед выходом.

void CLogifyHandlerDatabase::Close(void)
  {
   Flush();
  }

Мы внедрили надежный обработчик, обеспечивающий эффективное хранение логов без потери данных. Теперь, когда у нас готов обработчик базы данных для записи логов, нам необходимо создать эффективные методы для запроса этих записей. Идея состоит в том, чтобы иметь универсальный базовый метод, называемый Query(), который принимает SQL-команду в строковом формате и возвращает данные в виде массива типа MqlLogifyModel. На основе этого мы можем разработать специальные методы для обработки повторяющихся запросов. Наш метод Query() будет отвечать за открытие базы данных, выполнение запроса и сохранение результатов в структуре логов. Реализация:

class CLogifyHandlerDatabase : public CLogifyHandler
  {
public:
   //--- Query methods
   bool              Query(string query, MqlLogifyModel &data[]);
  };
//+------------------------------------------------------------------+
//| Get data by sql command                                          |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::Query(string query, MqlLogifyModel &data[])
  {
   //--- Get the full path of the file
   string path = m_config.directory+"\\"+m_config.base_filename+".sqlite";
   
   //--- Open database
   ResetLastError();
   int handle_db = DatabaseOpen(path,DATABASE_OPEN_READWRITE);
   if(handle_db == INVALID_HANDLE)
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+path+"'. Убедимся, что каталог существует и доступен для записи. (Code: "+IntegerToString(GetLastError())+")");
      return(false);
     }
   
   //--- Prepare the SQL query
   int request = DatabasePrepare(handle_db,query);
   if(request == INVALID_HANDLE)
     {
      Print("Erro query");
      return(false);
     }
   
   //--- Clears array before inserting new data
   ArrayFree(data);
   
   //--- Reads query results line by line
   for(int i=0;DatabaseRead(request);i++)
     {
      int size = ArraySize(data);
      ArrayResize(data,size+1,size);
      
      //--- Maps database data to the MqlLogifyModel model
      DatabaseColumnText(request,1,data[size].formated);
      DatabaseColumnText(request,2,data[size].levelname);
      DatabaseColumnText(request,3,data[size].msg);
      DatabaseColumnText(request,4,data[size].args);
      DatabaseColumnLong(request,5,data[size].timestamp);
      string value;
      DatabaseColumnText(request,6,value);
      data[size].date_time = StringToTime(value);
      DatabaseColumnInteger(request,7,data[size].level);
      DatabaseColumnText(request,8,data[size].origin);
      DatabaseColumnText(request,9,data[size].filename);
      DatabaseColumnText(request,10,data[size].function);
      DatabaseColumnLong(request,11,data[size].line);
     }
   
   //--- Ends the query and closes the database
   DatabaseFinalize(handle_db);
   DatabaseClose(handle_db);
   return(true);
  }
//+------------------------------------------------------------------+

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

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

SELECT * FROM 'logs' WHERE level=1;
SELECT * FROM 'logs' WHERE timestamp BETWEEN '{start_time}' AND '{stop_time}';
SELECT * FROM 'logs' WHERE origin LIKE '%{origin}%';
SELECT * FROM 'logs' WHERE msg LIKE '%{msg}%';
SELECT * FROM 'logs' WHERE args LIKE '%{args}%';
SELECT * FROM 'logs' WHERE filename LIKE '%{filename}%';
SELECT * FROM 'logs' WHERE function LIKE '%{function}%';

Теперь мы реализуем конкретные методы, использующие эти команды:

class CLogifyHandlerDatabase : public CLogifyHandler
  {
public:
   //--- Query methods
   bool              Query(string query, MqlLogifyModel &data[]);
   bool              QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[]);
   bool              QueryByDate(datetime start_time, datetime stop_time, MqlLogifyModel &data[]);
   bool              QueryByOrigin(string origin, MqlLogifyModel &data[]);
   bool              QueryByMsg(string msg, MqlLogifyModel &data[]);
   bool              QueryByArgs(string args, MqlLogifyModel &data[]);
   bool              QueryByFile(string file, MqlLogifyModel &data[]);
   bool              QueryByFunction(string function, MqlLogifyModel &data[]);
  };
//+------------------------------------------------------------------+
//| Get logs filtering by level                                      |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE level="+IntegerToString(level)+";",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by start end stop time                        |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByDate(datetime start_time, datetime stop_time, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE timestamp BETWEEN '"+IntegerToString((ulong)start_time)+"' AND '"+IntegerToString((ulong)stop_time)+"';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by origin                                     |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByOrigin(string origin, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE origin LIKE '%"+origin+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by message                                    |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByMsg(string msg, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE msg LIKE '%"+msg+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by args                                       |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByArgs(string args, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE args LIKE '%"+args+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by file name                                  |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByFile(string file, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE filename LIKE '%"+file+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by function name                              |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByFunction(string function, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE function LIKE '%"+function+"%';",data));
  }
//+------------------------------------------------------------------+

Теперь у нас есть набор эффективных и гибких методов для прямого доступа к логам из базы данных. Метод Query() позволяет выполнять любые команды SQL, даже более сложные с дополнительными фильтрами в зависимости от конкретных потребностей, а вспомогательные методы инкапсулируют распространенные запросы, делая их более интуитивно понятными в использовании и уменьшая количество ошибок.

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


Визуализация результата

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

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

//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
#include <Trade/Trade.mqh>
CLogify logify;
CTrade trade;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleDatabaseConfig m_config;
   m_config.directory = "db";
   m_config.base_filename = "logs";
   m_config.messages_per_flush = 5;
   
   //--- Handler Database
   CLogifyHandlerDatabase *handler_database = new CLogifyHandlerDatabase();
   handler_database.SetConfig(m_config);
   handler_database.SetLevel(LOG_LEVEL_DEBUG);
   handler_database.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));
   
   //--- Add handler in base class
   logify.AddHandler(handler_database);
   
   //--- Using logs
   logify.Info("Expert starting successfully", "Boot", "",__FILE__,__FUNCTION__,__LINE__);
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- No positions
   if(PositionsTotal() == 0)
     {
      double price_entry = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double volume = 1;
      if(trade.Buy(volume,_Symbol,price_entry,price_entry - 100 * _Point, price_entry + 100 * _Point,"Buy at market"))
        {
         logify.Debug("Transaction data | Price: "+DoubleToString(price_entry,_Digits)+" | Symbol: "+_Symbol+" | Volume: "+DoubleToString(volume,2), "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
         logify.Info("Purchase order sent successfully", "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
        }
      else
        {
         logify.Debug("Error code: "+IntegerToString(trade.ResultRetcode(),_Digits)+" | Description: "+trade.ResultRetcodeDescription(), "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
         logify.Error("Failed to send purchase order", "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
        }
     }
  }
//+------------------------------------------------------------------+

При тестировании стратегии в течение одного дня на EURUSD было сгенерировано 909 записей. В соответствии с нашими настройками, они были сохранены в файле .sqlite. Для доступа к ним просто перейдите в папку терминала или нажмите Ctrl/Cmd + Shift + D, и откроется файловый менеджер. Перейдите по пути MQL5/Files/db/logs.sqlite. Имея файл под рукой, мы можем открыть его непосредственно в MetaEditor, как и раньше:


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


Заключение

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

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

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

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17709

Прикрепленные файлы |
LogifyePart6p.zip (22.67 KB)
Разработка инструментария для анализа движения цен (Часть 19): ZigZag Analyzer Разработка инструментария для анализа движения цен (Часть 19): ZigZag Analyzer
Для анализа движения цены вручную трейдры используют линии тренда для подтверждения направления и определения потенциальных уровней разворота или продолжения тренда. В этой серии, где мы разрабатываем инструментарий для анализа движения цен, мы представляем инструмент который строит наклонные трендовые линий для удобного анализа рынка. Он четко обозначает ключевые тренды и уровни, необходимые для эффективной оценки ценового движения.
Нейросети в трейдинге: Потоковые модели с остаточной высокочастотной адаптацией (ResFlow) Нейросети в трейдинге: Потоковые модели с остаточной высокочастотной адаптацией (ResFlow)
Статья знакомит с фреймворком ResFlow, созданным для анализа временной динамики событийных потоков. Фреймворк сочетает низкочастотное моделирование трендов с высокочастотной корректировкой локальных колебаний. Ключевые достоинства — модульность, гибкость интеграции с разными алгоритмами и эффективное повышение временного разрешения без лишней нагрузки на модель.
Инженерия признаков с Python и MQL5 (Часть IV): Распознавание свечных паттернов с помощью UMAP-регрессии Инженерия признаков с Python и MQL5 (Часть IV): Распознавание свечных паттернов с помощью UMAP-регрессии
Методы уменьшения размерности широко используются для повышения производительности моделей машинного обучения. Мы рассмотрим относительно новый метод UMAP (Uniform Manifold Approximation and Projection) — приближение и проекция на равномерном многообразии. Эта новая методика разработана специально для решения проблемы артефактов и искажений в данных, которые присущи традиционным методам. UMAP — это эффективный метод уменьшения размерности, который позволяет группировать похожие свечные графики новым способом, снижая вероятность ошибок на данных, не входящих в выборку, и улучшая результаты торговли.
Создание торговой панели администратора на MQL5 (Часть IX): Организация кода (IV). Класс для панели управления торговлей Создание торговой панели администратора на MQL5 (Часть IX): Организация кода (IV). Класс для панели управления торговлей
Обновляем панель управления торговлей (TradeManagementPanel), используемую в нашем советнике New_Admin_Panel. В новой версии будем использовать встроенные классы и получим более удобный интерфейс управления сделками. В частности, добавим кнопки для открытия позиций, а также элементы для управления открытыми сделками и отложенными ордерами. Кроме того, в панели будет встроенная система управления рисками, чтобы устанавливать значения стоп-лосса и тейк-профита непосредственно через ее интерфейс. В целом обновление улучшает организацию самого кода, что важно для таких больших программ, а также упрощает доступ к инструментам управления ордерами — в определенных моментах это будет сделать проще, чем через интерфейс терминала.