English Deutsch 日本語
preview
Внедрение в MQL5 практических модулей из других языков (Часть 05): Модуль Logging из Python — ведите логи профессионально

Внедрение в MQL5 практических модулей из других языков (Часть 05): Модуль Logging из Python — ведите логи профессионально

MetaTrader 5Трейдинг |
193 0
Omega J Msigwa
Omega J Msigwa

Разделы


Введение

Ведение логов имеет решающее значение в любом современном устройстве, программе или программном обеспечении. Это просто процесс ведения записей обо всем, что произошло за время существования конкретной операции.

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

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

источник изображения: pexels.com

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

  1. отслеживать торговые решения, то есть мы можем видеть, что произошло и когда и почему советники открыли, изменили или закрыли позицию и т. д.;
  2. проверять свою логику и убеждаться, что она работает точно так, как задумано, при любых рыночных условиях;
  3. отслеживать сложную логику, чтобы понять, где произошла ошибка в расчетах или почему сделка была отклонена, а также многое другое.

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


Проблемы с механизмом ведения логов в MetaTrader 5.

Все логи перемешаны с логами, сгенерированными системой.

Вкладка «Эксперты» не отображает информацию о конкретной программе; все логи выводятся в одну и ту же консоль и сохраняются в одном лог-файле за определенный день.

Это затрудняет мониторинг логов для конкретной программы или функции.

Их сложно отформатировать.

Поскольку в MQL5 нет специального или конкретного способа вывода информации, все логи могут выглядеть по-разному. Эта непоследовательность затрудняет выявление ошибок и обнаружение недостатков.

Вы практически не можете контролировать уровень детализации логов.

Если вы не добавите пару операторов if перед каждым «Вызовом функции Print» в явном виде, нет возможности контролировать информацию, которая отображается на вкладке «Эксперты».

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


Система ведения логов для Python в MQL5

Согласно документации Python:

Модуль logging определяет функции и классы, которые реализуют гибкую систему регистрации событий для приложений и библиотек.

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

Пример ниже.

import logging
logger = logging.getLogger(__name__)

def do_something():
    logger.info('Doing something important')

def main():
    logging.basicConfig(filename='myapp.log', level=logging.INFO)
    
    logger.info('Started')
    do_something()
    logger.info('Finished')

if __name__ == '__main__':
    main()

Результаты (myapp.log).

INFO:__main__:Finished
INFO:__main__:Started
INFO:__main__:Doing something important
INFO:__main__:Finished

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

Но в аналогичном классе MQL5 нам не нужна функция с именем getLogger, потому что все, что она делает, это извлекает (или создает) устройство ведения логов с определенным именем.

Это можно сделать в конструкторе классов с возможностью присвоить имя. Конструктор классов может возвращать объект CLogger.

class CLogger
  {
private:

   string            m_name;
   LogLevels         m_loglevel;
   string            m_filename;
   int               m_filehandle; // Handle of log file
   bool              m_iscommon;
   string            m_logs_format;
   bool              m_console_on;

   int               m_fileflags;
   bool              is_configured;

public:

   void              CLogger(const string name);
   void              CLogger(void); // Constructor
   void             ~CLogger(void); // Destructor
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CLogger::CLogger(void):
   m_filename("logs.log"),
   is_configured(false),
   m_filehandle(-1),
   m_console_on(true),
   m_iscommon(false),
   m_logs_format(DEFAULT_MSG_FORMAT),
   m_loglevel(LOG_LEVEL_INFO)
  {

  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CLogger::CLogger(const string name):
   m_name(name)
  {
   CLogger();
  }

Одна из важнейших функций в этом классе — это функция с именем basicConfig.


Основные настройки механизма ведения логов

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

Имя файла (filename)

Это имя файла, в который вы хотите записать все логи.

Расширение имени файла должно быть либо .txt, либо .log.

bool CLogger::basicConfig(LogLevels log_level = LOG_LEVEL_INFO,
                          string filename = "logs.log",
                          bool console_on = true,
                          string format = DEFAULT_MSG_FORMAT,
                          bool file_common = false)
  {
   m_filename = filename;
   m_logs_format = format;
   m_console_on = console_on;
   m_iscommon = file_common;
   m_loglevel = log_level;


//--- Before reading the file check if the extension is ok

   if(!checkFileExtenstion(filename))
     {
      is_configured = false;
      return false;
     }

Проверка расширения имени файла.

bool CLogger::checkFileExtenstion(string filename)
  {
   if(StringFind(filename, ".txt") > 0 || StringFind(filename, ".log") > 0)
      return true;

   printf("Unsupported file extension, the logger expects a file [.txt, .log] file extensions (types)");

   return false;
  }

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

bool CLogger::basicConfig(LogLevels log_level = LOG_LEVEL_INFO,
                          string filename = "logs.log",
                          bool console_on = true,
                          string format = DEFAULT_MSG_FORMAT,
                          bool file_common = false)
  {
   m_filename = filename;
   m_logs_format = format;
   m_console_on = console_on;
   m_iscommon = file_common;
   m_loglevel = log_level;


//--- Before reading the file check if the extension is ok

   if(!checkFileExtenstion(filename))
     {
      is_configured = false;
      return false;
     }

//--- Open the file for writting

   m_fileflags = FILE_READ | FILE_WRITE | FILE_SHARE_WRITE | FILE_TXT | FILE_ANSI;

   if(m_iscommon)
      m_fileflags |= FILE_COMMON;

   m_filehandle = FileOpen(filename, m_fileflags); // Open or create file

   if(m_filehandle == INVALID_HANDLE)
     {
      printf("func=%s line=%d, failed to open a '%s'. Error = %d", __FUNCTION__, __LINE__, filename, GetLastError());
      is_configured = false;
      return false;
     }

   FileSeek(m_filehandle, 0, SEEK_END); //Move to the end of the file

//--- Handle large files than accepted

   fileRotate(m_filehandle, m_filename, m_fileflags, m_iscommon);

   is_configured = true;
   return true;
  }

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

// Max file size in megabytes
#define MAX_FILE_SIZEMB 10
// The maximum number of files of FILE_SIZEMB to create before the system stop writting for good
#define MAX_LOG_FILES 1000

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

Каждый раз, когда размер файла превысит этот лимит, будет автоматически создаваться новый лог-файл с тем же базовым именем + _[существующие лог-файлы с тем же именем++]. Например, если существует лог-файл с именем mylogs.log, будет создан новый файл с именем mylogs_1.log.

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

Функция с именем fileRotate справится с этой задачей.

void CLogger::fileRotate(int &handle, string &filename, int flags, bool is_common)
  {
   if (!isFileSizeLimitReached(handle))
      return;   // No rotation

//--- Close the current larger file

   FileClose(handle);
   
//---

   if(!checkFileExtenstion(filename))
      return;

//--- Get the base name of the file

   string extension = StringFind(filename, ".log") < 0 ? ".txt" : ".log";
   int ext_start = MathMax(StringFind(filename, ".log"), StringFind(filename, ".txt"));
   string base_name = StringSubstr(filename, 0, ext_start);

//--- Get the incremented file names

   string new_name = "";
   for(int i = 1; i <= MAX_LOG_FILES; i++)
     {
      new_name = base_name + "_" + string(i) + extension;

      if (!FileIsExist(new_name, is_common))
        {
         handle = FileOpen(new_name, flags);
         if(handle == INVALID_HANDLE)
           {
            printf("Failed to rotate into a new file");
            return;
           }
           
          break;
        }
      else //Check whether an existing file is full or not, if it's not log into that file until it's full
        {
         int temp_handle = FileOpen(new_name, flags);
         if(temp_handle == INVALID_HANDLE)
            continue;
            
         if (!isFileSizeLimitReached(temp_handle))
            {
               handle = temp_handle;
               break;
            }
         else
             FileClose(temp_handle); //Close a temporarily opened file
        }
     }

//---
   
   FileSeek(handle, 0, SEEK_END); //Move to the end of the file
  }
   bool              isFileSizeLimitReached(int handle)
     {
      int size = (int)FileSize(handle);
      if(size <= MAX_FILE_SIZEMB * 1000000)
         return false;   // No rotation
      
      //---
      
      return true;
     }

Примеры результатов.

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

console_on

Если установить значение этого параметра на true, все записи будут выводиться в консоль (вкладка «Эксперты») после их сохранения в указанный лог-файл.

Это помогает нам избежать написания дополнительной строки кода только для вывода информации.

file_common

Эта логическая переменная рассказывает, находится ли указанный «лог-файл» в Общем каталоге (если установлено значение true) или по обычному пути передачи данных MQL5 (если установлено значение false).

//--- Open the file for writting

   m_fileflags = FILE_READ | FILE_WRITE | FILE_SHARE_WRITE | FILE_TXT | FILE_ANSI;

   if(m_iscommon)
      m_fileflags |= FILE_COMMON;

log_level

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

enum LogLevels
  {
   LOG_LEVEL_DEBUG    = 10,
   LOG_LEVEL_INFO     = 20,
   LOG_LEVEL_WARNING  = 30,
   LOG_LEVEL_ERROR    = 40,
   LOG_LEVEL_CRITICAL = 50
  };

Что делает с классом переменная LOG_LEVEL?

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

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

Функция Уровень Будет ли информация регистрироваться в лог или выводиться на экран?
CLogger.debug()
10 нет
CLogger.info()
20 ДА
CLogger.warning()
30 ДА
CLogger.error()
40 ДА
CLogger.critical()
50 ДА 

Таким образом, несмотря на то что функция debug() запускает процесс, он ни на что не влияет, поскольку уровень ниже минимально допустимого.

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

format

Переменная format — одна из важнейших переменных в классе; это единственное место, где можно управлять процессом отображения логов на вкладке «Эксперты» и при сохранении в файлах.

В таблице ниже приведено описание заполнителей переменной format и результатов их работы.

Заполнитель Описание Примечания и пример 
%(asctime)s
Локальная метка времени записи в логе TimeLocal, отформатированная как ГГГГ.ММ.ДД ЧЧ:ММ:СС. Значение datetime: 2025.01.01 00:00:05.
%(levelname)s
Имя уровня записи в логе в виде текста Это может быть INFO, DEBUG, ERROR, WARNING, CRITICAL
%(programname)s
Название программы или компонента. Его можно задать с помощью опционального конструктора класса, передав соответствующий аргумент name. Example, My Indicator.
%(functionname)s
Название функции, в которой генерируется лог. Может быть задано вручную с помощью функций создания логов, таких как OnTick или OnInit.
%(linenumber)d
Номер строки кода, где генерируется лог. Например, строка 118. Только если номер строки считан правильно, иначе возвращает empty.
%(programtype)s

Тип запускаемой программы. Его можно задать с помощью пользовательского конструктора. Зависит от ENUM_PROGRAM_TYPE.

CLogger::CLogger(const string name,ENUM_PROGRAM_TYPE program_type):
   m_name(name),
   m_program_type(ProgramTypeToSTring(program_type))
 {
 
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string CLogger::ProgramTypeToSTring(ENUM_PROGRAM_TYPE prog_type)
{
   switch(prog_type)
   {
      case PROGRAM_SCRIPT:
         return "Script";

      case PROGRAM_EXPERT:
         return "EA";

      case PROGRAM_INDICATOR:
         return "Indicator";

      case PROGRAM_SERVICE:
         return "Service";

      default:
         return "Unknown";
   }
}
Это может быть Script (скрипт), EA (советник), Indicator (индикатор), Service (служба) или Unknown (неизвестное имя).
%(message)s
Само сообщение в логе. Например, Не удалось создать отложенный ордер.

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

Пример формата.

string format = "%(asctime)s: %(levelname)s:%(programname)s:%(programtype)s:%(functionname)s:%(linenumber)d:%(message)s";
logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);

Результаты.

2025.12.02 09:13:54:INFO:Logging Test:Script:OnStart:36:The script has started
2025.12.02 09:13:54:ERROR:Logging Test:Script:OnStart:40:Some operation has failed Error = 0

Пример формата.

string format = "%(asctime)s | [%(levelname)s] [%(programname)s] [%(programtype)s] func:%(functionname)s line:%(linenumber)d --> [%(message)s]";
logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);

Результаты.

2025.12.02 09:15:41 | [INFO] [Logging Test] [Script] func:OnStart line:37 --> [The script has started]
2025.12.02 09:15:41 | [ERROR] [Logging Test] [Script] func:OnStart line:41 --> [Some operation has failed Error = 0]


Запись некоторой информации в логи

Закрытая функция в классе с именем Log — функция, отвечающая за форматирование, создание и запись сообщения (лога) в файл, не говоря уже об отображении информации на вкладке «Эксперты» (печать).

void              Log(LogLevels level, string msg, string func_name = "", int line_no = -1);

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

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

void CLogger::Log(LogLevels level,
                  string msg,
                  string func_name = "",
                  int line_no = -1)
  {
//---

   if(!is_configured)  //Auto-configure if the function basicConfig wasn't called
      basicConfig();

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

//--- Level filtering

   if(level < m_loglevel)
      return;

Форматирование логов

Нам необходимо удалить все заполнители и заменить их желаемым значением.

// Standard placeholders

   StringReplace(entry, "%(asctime)s", t);
   StringReplace(entry, "%(levelname)s", LevelToString(level));
   StringReplace(entry, "%(message)s", msg);

// Custom placeholders

// ---- Custom placeholders ----

// Program name

   if(m_name != "")
       StringReplace(entry, "%(programname)s", m_name);
   else
      StringReplace(entry, "%(programname)s", "");

// Function name
   if(func_name != "")
      StringReplace(entry, "%(functionname)s", func_name);
   else
      StringReplace(entry, "%(functionname)s", "");

// Program type

   if(m_program_type != "")
      StringReplace(entry, "%(programtype)s", m_program_type);
   else
      StringReplace(entry, "%(programtype)s", "");

// Line number
   if(line_no >= 0)
      StringReplace(entry, "%(linenumber)d", IntegerToString(line_no));
   else
      StringReplace(entry, "%(linenumber)d", "");

   entry += "\n";

Обработка ротации файлов

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

//--- Handle file rotations before writing

   fileRotate(m_filehandle, m_filename, m_fileflags, m_iscommon);

Запись и печать логов

//--- Write to log file (plain text)

   FileWriteString(m_filehandle, entry);
   FileFlush(m_filehandle);

   if(m_console_on)
      Print(entry);

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


Отдельная функция для каждого сообщения в логе

Логи для отладки

   void              debug(string msg, string func_name = "", int line_no = -1)
     {
      this.Log(LOG_LEVEL_DEBUG, msg, func_name, line_no);
     }

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

Пример использования.

string format = "%(asctime)s | [%(levelname)s] [%(programname)s] [%(programtype)s] func:%(functionname)s line:%(linenumber)d --> [%(message)s]";
logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);

bool num_a = 10;
bool num_b = -10;
  
logger.debug("num_a>num_b "+(string)bool(num_a>num_b), __FUNCTION__, __LINE__);  

Результаты.

2025.12.02 09:26:06 | [DEBUG] [Logging Test] [Script] func:OnStart line:43 --> [num_a>num_b false]

Информативные логи

Чаще всего они представляют собой такой тип сообщений, который используется для обозначения какого-либо текущего процесса или деятельности.

void OnStart()
  {
//---
      
   logger.info("The script has started");
   
   // some activity   

   logger.info("End of the script!");
  }

Результаты.

2025.12.02 09:26:06 | [INFO] [Logging Test] [Script] func:OnStart line:38 --> [The script has started]
2025.12.02 09:26:06 | [INFO] [Logging Test] [Script] func: line: --> [End of the script!]

Отображение ошибок для пользователей

Это логи, предназначенные для отображения сбоев в работе программы.

   void              error(string msg, string func_name = "", int line_no = -1)
     {
      this.Log(LOG_LEVEL_ERROR, msg, func_name, line_no);
     }

Результаты.

void OnStart()
  {
//---

   if (!doSomething())
      {
        logger.error(StringFormat("Some operation has failed Error = %d",GetLastError()), __FUNCTION__, __LINE__);
      }
     
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool doSomething()
 {
   return false;
 }

Результаты.

2025.12.02 09:29:26 | [ERROR] [Logging Test] [Script] func:OnStart line:47 --> [Some operation has failed Error = 0]

Ведение логов некоторых предупреждений

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

Пример использования.

input int risk_per_trade = 50; //Risk Per Trade
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
   string format = "%(asctime)s | [%(levelname)s] [%(programname)s] [%(programtype)s] func:%(functionname)s line:%(linenumber)d --> [%(message)s]";
   logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);
 
   logger.info("The script has started",__FUNCTION__,__LINE__);
   
   if (risk_per_trade>10) //if a user has set the risk higher than 10% of the account balance
      logger.warning(StringFormat("You have risked too much for a single trade. Risk percentage = %d", risk_per_trade));
  }

Результаты.

2025.12.02 09:15:41 | [WARNING] [Logging Test] [Script] func: line: --> [You have risked too much for a single trade. Risk percentage = 50]

Логи фатальных или критических ошибок

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

   void              critical(string msg, string func_name = "", int line_no = -1)
     {
      this.Log(LOG_LEVEL_CRITICAL, msg, func_name, line_no);
     }

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

#include <PyMQL5\logging.mqh>
CLogger logger(PROG_NAME, PROG_TYPE);

int important_indicator_handle;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
   string format = "%(asctime)s | [%(levelname)s] [%(programname)s] [%(programtype)s] func:%(functionname)s line:%(linenumber)d --> [%(message)s]";
   logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);
 
   important_indicator_handle = iMA(Symbol(), Period(), -1, 0, MODE_SMA, PRICE_CLOSE); //An indicator with a negative period
   
   if (important_indicator_handle == INVALID_HANDLE)
     {
       logger.critical("Failed to load the Moving Average indicator, Error = "+(string)GetLastError(), __FUNCTION__, __LINE__);
       return;
     }
 }

Результаты.

2025.12.02 09:34:54 | [CRITICAL] [Logging Test] [Script] func:OnStart line:56 --> [Failed to load the Moving Average indicator, Error = 4002]


Оптимизация процесса создания и ведения логов

Процесс чтения и записи в текстовые файлы (операции ввода/вывода) является одним из самых ресурсоемких процессов в языке MQL5, не говоря уже о встроенной функции с именем Print, которую мы используем для отображения информации на вкладке «Эксперты».

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

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

class CLogger
  {
private:
   
   //--- Caching
   
   bool              m_cache_mode;
   string            m_logs_cache[];
   uint              m_logs_count;
   
public:

   void              CLogger(const string name);
   void              CLogger(const string name, ENUM_PROGRAM_TYPE program_type);
   void              CLogger(void); // Constructor
   void             ~CLogger(void); // Destructor

   bool              basicConfig(LogLevels log_level = LOG_LEVEL_INFO, 
                                 string filename = "logs.log",
                                 bool console_on = true,
                                 string format = DEFAULT_MSG_FORMAT,
                                 bool file_common = false,
                                 bool cache_mode = false);

   //---
   
   void              WriteCache()
     {
       for (uint i=0; i<m_logs_count; i++)
         { 
           if (m_filehandle==INVALID_HANDLE)
             DebugBreak();
           
           fileRotate(m_filehandle, m_filename, m_fileflags, m_iscommon);
             
           FileWriteString(m_filehandle, m_logs_cache[i]);
           FileFlush(m_filehandle);
         }
     }
  };
//+------------------------------------------------------------------+
//|         Basic configurations                                     |
//+------------------------------------------------------------------+
bool CLogger::basicConfig(LogLevels log_level = LOG_LEVEL_INFO,
                          string filename = "logs.log",
                          bool console_on = true,
                          string format = DEFAULT_MSG_FORMAT,
                          bool file_common = false,
                          bool cache_mode = false)
  {
   m_filename = filename;
   m_logs_format = format;
   m_console_on = console_on;
   m_iscommon = file_common;
   m_loglevel = log_level;
   m_cache_mode = cache_mode;

//--- some lines of code

   return true;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CLogger::Log(LogLevels level,
                  string msg,
                  string func_name = "",
                  int line_no = -1)
  {
//---

// some lines of code....

//--- Write to log file (plain text)
   
   if (m_cache_mode) //Write to an array 
     {
       this.m_logs_count++;
       if (m_logs_count>m_logs_cache.Size()) 
         ArrayResize(m_logs_cache, m_logs_count+MAX_CACHE_SIZE); 
       
       //---
       
       m_logs_cache[m_logs_count-1] = entry;
     }
   else // write to a file
     {
       FileWriteString(m_filehandle, entry);
       FileFlush(m_filehandle);
     }

   if(m_console_on)
      Print(entry);
  }

Функция под именем WriteCache записывает всю хранящуюся в массиве m_logs_cache информацию в нужный файл, аналогично тому, как информация автоматически записывается в файлы при установлении переменной cache_mode на false внутри функции basicConfig.

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

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

bool CLogger::basicConfig(LogLevels log_level = LOG_LEVEL_INFO,
                          string filename = "logs.log",
                          bool console_on = true,
                          string format = DEFAULT_MSG_FORMAT,
                          bool file_common = false,
                          bool cache_mode = false,
                          bool write_cache_automatically = false)
  {
   m_filename = filename;
   m_logs_format = format;
   m_console_on = console_on;
   m_iscommon = file_common;
   m_loglevel = log_level;
   m_cache_mode = cache_mode;
   m_write_cache_automatically = write_cache_automatically;
CLogger::~CLogger(void)
  {
   if (m_cache_mode && m_write_cache_automatically)
      WriteCache();
   
//---

   if(m_filehandle != INVALID_HANDLE)
      FileClose(m_filehandle); //Close the file, finally
  }

В итоге мне удалось заметить некоторые улучшения в тестере стратегий (примерно на 50% сократилось время тестирования) по сравнению с версией без кэширования. 

Это произошло также после внесения ряда изменений в функцию, отвечающую за ротацию файлов.

void CLogger::fileRotate(int &handle, string &filename, int flags, bool is_common)
{
   //---If first time -> open main file
   if(handle == -1)
   {
      handle = OpenFile(filename, flags);
      if(handle == -1) 
         return;
   }

   //--- Check rotation trigger
   if(!isFileSizeLimitReached(handle))
      return;

   //--- Close current big file 
   FileClose(handle);
   
   //--- Rotate through numbered files
   for(int i = 1; i <= MAX_LOG_FILES; i++)
   {
      string new_name = m_base_name + "_" + (string)i + m_file_extension;

      // File exists → check if it still has space
      if(is_common?FileIsExist(new_name, FILE_COMMON):FileIsExist(new_name))
      {
         int temp = OpenFile(filename, flags);
         
         //---
         
         if (MQLInfoInteger(MQL_DEBUG))  
           printf("Filename %s size MB = %f",new_name, FileSize(temp)/1e6);
          
         if (temp != -1)
          {
            bool too_big = isFileSizeLimitReached(temp);
            FileClose(temp);

            if(too_big)
               continue;   //--- The fill is full try the next one
          }
      }

      // File does not exist or is small, use it
      if (filename == new_name)
        return;
        
      filename = new_name;
      handle = OpenFile(filename, flags);

      if(handle == -1)
         DebugBreak();

      FileSeek(handle, 0, SEEK_END);
      return;   // IMPORTANT: stop rotation here
   }
}

Оптимальное тестирование стратегий с помощью ведения логов

Убедившись, что режим кэширования установлен в значение true, необходимо предотвратить печать, установив в тестере стратегий переменную console_on в значение false, если у вас нет веской причины для вывода на печать; это может помочь сократить общее время выполнения теста.

#define PROG_NAME MQLInfoString(MQL_PROGRAM_NAME)
#define PROG_TYPE (ENUM_PROGRAM_TYPE)MQLInfoInteger(MQL_PROGRAM_TYPE)
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
#include <PyMQL5\logging.mqh>
CLogger logger(PROG_NAME, PROG_TYPE);
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
   string format = "%(asctime)s:%(programname)s:%(programtype)s:%(functionname)s:%(linenumber)d:%(message)s";
   
   bool is_tester = (bool)MQLInfoInteger(MQL_TESTER);
   logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", !is_tester, format, is_tester, true, true);
 
   logger.info("Program started!");
   
//---
   return(INIT_SUCCEEDED);
  }

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

Оставшаяся часть советника.

void OnDeinit(const int reason)
  {
//---
   logger.info("Program stopped. Reason = "+UninitializeReasonDescription(reason), __FUNCTION__, __LINE__);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
   logger.info("Program running");
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string UninitializeReasonDescription(const int reason) 
  { 
   switch(reason) 
     { 
      //--- the EA has stopped working calling the ExpertRemove() function 
      case REASON_PROGRAM : 
        return("Expert Advisor terminated its operation by calling the ExpertRemove() function"); 
      //--- program removed from a chart 
      case REASON_REMOVE : 
        return("Program has been deleted from the chart"); 
      //--- program recompiled 
      case REASON_RECOMPILE : 
        return("Program has been recompiled"); 
      //--- symbol or chart period changed 
      case REASON_CHARTCHANGE : 
        return("Symbol or chart period has been changed"); 
      //--- chart closed 
      case REASON_CHARTCLOSE : 
        return("Chart has been closed"); 
      //--- inputs changed by user 
      case REASON_PARAMETERS : 
        return("Input parameters have been changed by a user"); 
      //--- another account has been activated or reconnection to the trade server has occurred due to changes in the account settings 
      case REASON_ACCOUNT : 
        return("Another account has been activated or reconnection to the trade server has occurred due to changes in the account settings"); 
      //--- another chart template applied 
      case REASON_TEMPLATE : 
        return("A new template has been applied"); 
      //--- OnInit() handler returned a non-zero value 
      case REASON_INITFAILED : 
        return("This value means that OnInit() handler has returned a nonzero value"); 
      //--- terminal closed 
      case REASON_CLOSE : 
        return("Terminal has been closed"); 
     } 
  
//--- deinitialization reason unknown 
   return("Unknown reason"); 
  }

Результаты.

Как и ожидалось, в общей директории было создано несколько файлов, каждый размером около 10 МБ.


Упрощение ведения логов — подход в языке Python

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

import logging
logger = logging.getLogger(__name__)

logging.basicConfig(filename='myapp.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s - file:%(filename)s - line:%(lineno)d - func:%(funcName)s')

def some_function():
    logger.info('Doing something')    
    
some_function()

Результаты.

2025-12-01 20:01:42,542 - INFO - Doing something - file:log.py - line:9 - func:some_function

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

#define logger_info(msg) logger.info(msg, __FUNCTION__, __LINE__)
#define logger_debug(msg) logger.debug(msg, __FUNCTION__, __LINE__)
#define logger_warning(msg) logger.warning(msg, __FUNCTION__, __LINE__)
#define logger_error(msg) logger.error(msg, __FUNCTION__, __LINE__)
#define logger_critical(msg) logger.critical(msg, __FUNCTION__, __LINE__)

Применение.

void OnStart()
  {
//---
   
   string format = "%(asctime)s | [%(levelname)s] [%(programname)s] [%(programtype)s] func:%(functionname)s line:%(linenumber)d --> [%(message)s]";
   logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);
 
   logger_info("The script has started");
   
   bool num_a = 10;
   bool num_b = -10;
     
   logger_info("num_a>num_b "+(string)bool(num_a>num_b));  

   if (!doSomething())
      {
        logger_error(StringFormat("Some operation has failed Error = %d",GetLastError()));
      }
      
   if (risk_per_trade>10) //if a user has set the risk higher than 10% of the account balance
      logger_warning(StringFormat("You have risked too much for a single trade. Risk percentage = %d", risk_per_trade));
      
   important_indicator_handle = iMA(Symbol(), Period(), -1, 0, MODE_SMA, PRICE_CLOSE); //An indicator with a negative period
   
   if (important_indicator_handle == INVALID_HANDLE)
     {
       logger_critical("Failed to load the Moving Average indicator, Error = "+(string)GetLastError());
       //return;
     }
     
//---

   logger_info("End of the script!");
  }

Результаты.

2025.12.02 09:47:49 | [INFO] [Logging Test] [Script] func:OnStart line:43 --> [The script has started]
2025.12.02 09:47:49 | [INFO] [Logging Test] [Script] func:OnStart line:48 --> [num_a>num_b false]
2025.12.02 09:47:49 | [ERROR] [Logging Test] [Script] func:OnStart line:52 --> [Some operation has failed Error = 0]
2025.12.02 09:47:49 | [WARNING] [Logging Test] [Script] func:OnStart line:56 --> [You have risked too much for a single trade. Risk percentage = 50]
2025.12.02 09:47:49 | [CRITICAL] [Logging Test] [Script] func:OnStart line:62 --> [Failed to load the Moving Average indicator, Error = 4002]
2025.12.02 09:47:49 | [INFO] [Logging Test] [Script] func:OnStart line:68 --> [End of the script!]


Заключительные мысли

Ведение логов — это не просто вывод простого текста на вкладку «Эксперты».. Это фундаментальная часть разработки программного обеспечения, помогающая нам понять, как работает наша программа, диагностировать проблемы и отслеживать события во времени.

Путем внедрения структурированного и многократно используемого модуля создания логов на языке MQL5, аналогичного библиотеке logging языка Python. Мы внедряем в свои торговые системы современные методы разработки. Это упрощает сопровождение и отладку нашего кода, а также делает его более согласованным с тем, как профессиональные разработчики по всему миру хранят и интерпретируют логи в системах на основе Python, на веб-серверах и т. д.

Надежный модуль ведения логов — это не просто удобство; это инструмент, который помогает нам оставаться организованными, эффективными и соответствовать отраслевым стандартам программирования.

Репозиторий, содержащий весь код, рассмотренный в этой серии статей, можно найти здесь: https://github.com/MegaJoctan/PyMQL5 для внесения предложений и исправления ошибок.


Таблица вложений

Имя файла Описание и использование
Include\PyMQL5\logging.mqh Класс типа библиотеки logging в Python, предназначенный для отображения и хранения логов. В нем есть класс с именем CLogger.
Scripts\Logging Test.mq5
Простой скрипт для тестирования методов, представленных в классе CLogger.
Experts\Logging Test.mq5 Советник (Expert Advisor, EA), разработанный для тестирования методов, представленных в классе CLogger, в реальных условиях торговли.

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

Прикрепленные файлы |
Attachments.zip (7.35 KB)
Торговые инструменты на MQL5 (Часть 17): Изучение векторных скругленных прямоугольников и треугольников Торговые инструменты на MQL5 (Часть 17): Изучение векторных скругленных прямоугольников и треугольников
В этой статье мы рассматриваем векторные методы для рисования скругленных прямоугольников и треугольников в MQL5 с использованием canvas и суперсэмплирования для сглаживания изображения. Мы реализуем заливку методом сканирования строк, геометрические предварительные вычисления для дуг и касательных, а также рисование границ для создания плавных, настраиваемых фигур. Такой подход закладывает основу для современных элементов пользовательского интерфейса в будущих торговых инструментах, поддерживающего входные параметры для установки размеров, радиусов, рамок и прозрачности.
Нелинейные признаки OHLC из эллиптических кривых Нелинейные признаки OHLC из эллиптических кривых
В статье рассматривается проекция дневных свечей EURUSD на эллиптическую кривую secp256k1 и извлечение 96 признаков (EC+TA) для прогноза направления следующей свечи в CatBoost. Показаны маппинг цен на кривую и конвейер обучения на 2000 барах D1; полная модель достигает AUC на тесте 0,6508, вклад EC-признаков — 60,6%. Материалы пригодны для воспроизведения в Python/MetaTrader 5.
Разработка инструментария для анализа Price Action (Часть 25): Пробой фракталов по двум EMA Разработка инструментария для анализа Price Action (Часть 25): Пробой фракталов по двум EMA
Анализ Price Action – это фундаментальный подход к выявлению прибыльных сетапов. Однако вручную отслеживать движение цены и паттерны бывает сложно и долго. Для решения этой задачи мы разрабатываем инструменты, которые автоматически анализируют Price Action и подают своевременные сигналы при обнаружении потенциальных возможностей. В этой статье представлен надежный инструмент, который использует пробои фракталов в сочетании с EMA 14 и EMA 200 для генерации надежных торговых сигналов, помогая трейдерам принимать более обоснованные решения.
Гипотеза случайности: поиск скрытых паттернов в ценовых рядах Гипотеза случайности: поиск скрытых паттернов в ценовых рядах
В статье описан тест гипотезы случайности для котировок на основе статистики хи-квадрат, построенной по частотам перекрывающихся s-цепочек. Показано, как формировать дискретные состояния и сравнивать наблюдаемые и ожидаемые частоты, чтобы обнаруживать марковскую память в приращениях цены. Подход помогает отделить структурные зависимости от шума и формализовать проверку торговых гипотез.