Deutsch 日本語
preview
Mastering Log Records (Part 5): Optimizing the Handler with Cache and Rotation

Mastering Log Records (Part 5): Optimizing the Handler with Cache and Rotation

MetaTrader 5Examples |
735 0
joaopedrodev
joaopedrodev

Introduction

In the first article of this series, Mastering Log Records (Part 1): Fundamental Concepts and First Steps in MQL5, we embarked on the creation of a custom log library for Expert Advisor (EA) development. In it, we explored the motivation behind creating such an essential tool: to overcome the limitations of MetaTrader 5’s native logs and bring a robust, customizable, and powerful solution to the MQL5 universe.

To recap the main points covered, we laid the foundation for our library by establishing the following fundamental requirements:

  1. Robust structure using the Singleton pattern, ensuring consistency between code components.
  2. Advanced persistence for storing logs in databases, providing traceable history for in-depth audits and analysis.
  3. Flexibility in outputs, allowing logs to be stored or displayed conveniently, whether in the console, in files, in the terminal or in a database.
  4. Classification by log levels, differentiating informative messages from critical alerts and errors.
  5. Customization of the output format, to meet the unique needs of each developer or project.

With this well-established foundation, it became clear that the logging framework we are developing will be much more than a simple event log; it will be a strategic tool for understanding, monitoring and optimizing the behavior of EAs in real time.

So far, we've explored the basics of logs, learned how to format them, and understood how handlers control the destination of messages. In the last article, we learned how to save log records to a file (.txt, .log, or .json). Now, in this fifth article, we'll optimize the process of saving logs to files by implementing caching and file rotation. Let's get started!


Adding a formatter to each handler

So far, our logging library manages message formatting through a single instance of the CFormatter class, which is centralized in the library base (CLogify). This approach works well for simple scenarios, but limits the flexibility of the handlers.

The problem is that by using a single global formatter, all handlers share the same format, which may not be ideal when different targets require different formatting. For example, while a handler that writes logs in JSON may need a specific structure, a handler that prints logs to the console may require a more human-readable format. The solution is to move the formatter responsibility to the handler base class (CLogifyHandler). This way, each handler can have its own independent formatter, allowing more control over the formatting of log messages. Let's implement this change and see how it improves the flexibility of the library.

Going straight to the code, we start by adding an instance of CFormatter inside CLogifyHandler, as this is a simple task for you who have read the previous articles, I will just add the final code highlighting what was added:

//+------------------------------------------------------------------+
//|                                                LogifyHandler.mqh |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/en/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/en/users/joaopedrodev"
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "../LogifyModel.mqh"
#include "../Formatter/LogifyFormatter.mqh"
//+------------------------------------------------------------------+
//| class : CLogifyHandler                                           |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : CLogifyHandler                                     |
//| Heritage    : No heritage                                        |
//| Description : Base class for all log handlers.                   |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyHandler
  {
protected:
   
   string            m_name;
   ENUM_LOG_LEVEL    m_level;
   CLogifyFormatter  *m_formatter;
   
public:
                     CLogifyHandler(void);
                    ~CLogifyHandler(void);
   
   //--- Handler methods
   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
   
   //--- Set/Get
   void              SetLevel(ENUM_LOG_LEVEL level);
   void              SetFormatter(CLogifyFormatter *format);
   string            GetName(void);
   ENUM_LOG_LEVEL    GetLevel(void);
   CLogifyFormatter *GetFormatter(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyHandler::CLogifyHandler(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyHandler::~CLogifyHandler(void)
  {
   //--- Delete formatter
   if(m_formatter != NULL)
     {
      delete m_formatter ;
     }
  }
//+------------------------------------------------------------------+
//| Processes a log message and sends it to the specified destination|
//+------------------------------------------------------------------+
void CLogifyHandler::Emit(MqlLogifyModel &data)
  {
  }
//+------------------------------------------------------------------+
//| Clears or completes any pending operations                       |
//+------------------------------------------------------------------+
void CLogifyHandler::Flush(void)
  {
  }
//+------------------------------------------------------------------+
//| Closes the handler and releases any resources                    |
//+------------------------------------------------------------------+
void CLogifyHandler::Close(void)
  {
  }
//+------------------------------------------------------------------+
//| Set level                                                        |
//+------------------------------------------------------------------+
void CLogifyHandler::SetLevel(ENUM_LOG_LEVEL level)
  {
   m_level = level;
  }
//+------------------------------------------------------------------+
//| Set object formatter                                             |
//+------------------------------------------------------------------+
void CLogifyHandler::SetFormatter(CLogifyFormatter *format)
  {
   m_formatter = GetPointer(format);
  }
//+------------------------------------------------------------------+
//| Get name                                                         |
//+------------------------------------------------------------------+
string CLogifyHandler::GetName(void)
  {
   return(m_name);
  }
//+------------------------------------------------------------------+
//| Get level                                                        |
//+------------------------------------------------------------------+
ENUM_LOG_LEVEL CLogifyHandler::GetLevel(void)
  {
   return(m_level);
  }
//+------------------------------------------------------------------+
//| Get object formatter                                             |
//+------------------------------------------------------------------+
CLogifyFormatter *CLogifyHandler::GetFormatter(void)
  {
   return(m_formatter);
  }
//+------------------------------------------------------------------+

Continuing with the simplest changes, we removed the CFormatter instance in CLogify the parts that were removed from the class are highlighted in red, and the ones that were added are highlighted in green:

//+------------------------------------------------------------------+
//|                                                       Logify.mqh |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/en/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/en/users/joaopedrodev"
#property version   "1.00"

#include "LogifyModel.mqh"
#include "Formatter/LogifyFormatter.mqh"
#include "Handlers/LogifyHandler.mqh"
#include "Handlers/LogifyHandlerConsole.mqh"
#include "Handlers/LogifyHandlerDatabase.mqh"
#include "Handlers/LogifyHandlerFile.mqh"
//+------------------------------------------------------------------+
//| class : CLogify                                                  |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : Logify                                             |
//| Heritage    : No heritage                                        |
//| Description : Core class for log management.                     |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogify
  {
private:
   
   CLogifyFormatter  *m_formatter;
   CLogifyHandler    *m_handlers[];
   
public:
                     CLogify();
                    ~CLogify();
   
   //--- Handler
   void              AddHandler(CLogifyHandler *handler);
   bool              HasHandler(string name);
   CLogifyHandler    *GetHandler(string name);
   CLogifyHandler    *GetHandler(int index);
   int               SizeHandlers(void);
   
   //--- Generic method for adding logs
   bool              Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   
   //--- Specific methods for each log level
   bool              Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   bool              Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0);
   
   //--- Get/Set object formatter
   void              SetFormatter(CLogifyFormatter *format);
   CLogifyFormatter *GetFormatter(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogify::CLogify()
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogify::~CLogify()
  {
   //--- Delete formatter
   if(m_formatter != NULL)
     {
      delete m_formatter;
     }
   
   //--- Delete handlers
   int size_handlers = ArraySize(m_handlers);
   for(int i=0;i<size_handlers;i++)
     {
      delete m_handlers[i];
     }
  }
//+------------------------------------------------------------------+
//| Add handler to handlers array                                    |
//+------------------------------------------------------------------+
void CLogify::AddHandler(CLogifyHandler *handler)
  {
   int size = ArraySize(m_handlers);
   ArrayResize(m_handlers,size+1);
   m_handlers[size] = GetPointer(handler);
  }
//+------------------------------------------------------------------+
//| Checks if handler is already in the array by name                |
//+------------------------------------------------------------------+
bool CLogify::HasHandler(string name)
  {
   int size = ArraySize(m_handlers);
   for(int i=0;i<size;i++)
     {
      if(m_handlers[i].GetName() == name)
        {
         return(true);
        }
     }
   return(false);
  }
//+------------------------------------------------------------------+
//| Get handler by name                                              |
//+------------------------------------------------------------------+
CLogifyHandler *CLogify::GetHandler(string name)
  {
   int size = ArraySize(m_handlers);
   for(int i=0;i<size;i++)
     {
      if(m_handlers[i].GetName() == name)
        {
         return(m_handlers[i]);
        }
     }
   return(NULL);
  }
//+------------------------------------------------------------------+
//| Get handler by index                                             |
//+------------------------------------------------------------------+
CLogifyHandler *CLogify::GetHandler(int index)
  {
   return(m_handlers[index]);
  }
//+------------------------------------------------------------------+
//| Gets the total size of the handlers array                        |
//+------------------------------------------------------------------+
int CLogify::SizeHandlers(void)
  {
   return(ArraySize(m_handlers));
  }
//+------------------------------------------------------------------+
//| Generic method for adding logs                                   |
//+------------------------------------------------------------------+
bool CLogify::Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0)
  {
   //--- If the formatter is not configured, the log will not be recorded.
   if(m_formatter == NULL)
     {
      return(false);
     }
   
   //--- Textual name of the log level
   string levelStr = "";
   switch(level)
     {
      case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break;
      case LOG_LEVEL_INFOR: levelStr = "INFOR"; break;
      case LOG_LEVEL_ALERT: levelStr = "ALERT"; break;
      case LOG_LEVEL_ERROR: levelStr = "ERROR"; break;
      case LOG_LEVEL_FATAL: levelStr = "FATAL"; break;
     }
   
   //--- Creating a log template with detailed information
   datetime time_current = TimeCurrent();
   MqlLogifyModel data("",levelStr,msg,args,time_current,time_current,level,origin,filename,function,line);
   data.formated = m_formatter.FormatLog(data);
   
   //--- Call handlers
   int size = this.SizeHandlers();
   for(int i=0;i<size;i++)
     {
      data.formated = m_handlers[i].GetFormatter().FormatLog(data);
      m_handlers[i].Emit(data);
     }
   
   return(true);
  }
//+------------------------------------------------------------------+
//| Debug level message                                              |
//+------------------------------------------------------------------+
bool CLogify::Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0)
  {
   return(this.Append(LOG_LEVEL_DEBUG,msg,origin,args,filename,function,line));
  }
//+------------------------------------------------------------------+
//| Infor level message                                              |
//+------------------------------------------------------------------+
bool CLogify::Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0)
  {
   return(this.Append(LOG_LEVEL_INFOR,msg,origin,args,filename,function,line));
  }
//+------------------------------------------------------------------+
//| Alert level message                                              |
//+------------------------------------------------------------------+
bool CLogify::Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0)
  {
   return(this.Append(LOG_LEVEL_ALERT,msg,origin,args,filename,function,line));
  }
//+------------------------------------------------------------------+
//| Error level message                                              |
//+------------------------------------------------------------------+
bool CLogify::Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0)
  {
   return(this.Append(LOG_LEVEL_ERROR,msg,origin,args,filename,function,line));
  }
//+------------------------------------------------------------------+
//| Fatal level message                                              |
//+------------------------------------------------------------------+
bool CLogify::Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0)
  {
   return(this.Append(LOG_LEVEL_FATAL,msg,origin,args,filename,function,line));
  }
//+------------------------------------------------------------------+
//| Set object formatter                                             |
//+------------------------------------------------------------------+
void CLogify::SetFormatter(CLogifyFormatter *format)
  {
   m_formatter = GetPointer(format);
  }
//+------------------------------------------------------------------+
//| Get object formatter                                             |
//+------------------------------------------------------------------+
CLogifyFormatter *CLogify::GetFormatter(void)
  {
   return(m_formatter);
  }
//+------------------------------------------------------------------+

The only part that was added was when formatting the message. Before, we used the formatter within the class itself. With the changes, in each handler we use the formatter provided by the handler. By associating a formatter directly with each handler, we eliminate the restriction of a single format and make the library more adaptable to different needs. Now, each destination can have a specific log style, ensuring that the output is more appropriate for the context in which it will be used. In the next topic, we will see how to manage the execution of logs in scheduled cycles with the CIntervalWatcher class, which will be a helper class for file rotation.


Creating the CIntervalWatcher class

The main objective of CIntervalWatcher is to check whether a certain time interval has passed since the last call. This is essential for generating logs that need to be checked at specific time intervals. Whether to avoid writing overload or to better structure the records, a cycle control mechanism is essential, avoiding unnecessary processing at each tick. It allows you to configure:

  • The time interval to be monitored (in seconds).
  • The time source (current time, GMT, local or trading server).
  • Whether it should return true on the first execution.

This way, the class is useful to check when to execute a periodic action within the library. Let's create a new folder called Utils, which will contain this file. In the end, the file browser should look like this:

Moving on to building the class, we first create an enum to support different time sources, we call it ENUM_TIME_ORIGIN

//+------------------------------------------------------------------+
//| Enum for different time sources                                  |
//+------------------------------------------------------------------+
enum ENUM_TIME_ORIGIN
  {
   TIME_ORIGIN_CURRENT = 0, // [0] Current Time
   TIME_ORIGIN_GMT,         // [1] GMT Time
   TIME_ORIGIN_LOCAL,       // [2] Local Time
   TIME_ORIGIN_TRADE_SERVER // [3] Server Time
  };
//+------------------------------------------------------------------+

We added private variables to the class to store the last recorded instant (m_last_time), the desired time interval (m_interval), the time origin (m_time_origin) and a flag (m_first_return) to control the first return. As a consequence we created a Set and a Get for each private attribute. To make it easier to configure intervals, time origin and first return, I decided to create some extra constructors for the class, helping you the developer. Below is the code with constructors and methods for accessing and obtaining private data.

//+------------------------------------------------------------------+
//| class : CIntervalWatcher                                         |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : CIntervalWatcher                                   |
//| Type        : Report                                             |
//| Heritage    : No heredirary.                                     |
//| Description : Monitoring new time periods                        |
//|                                                                  |
//+------------------------------------------------------------------+
class CIntervalWatcher
  {
private:

   //--- Auxiliary attributes
   ulong             m_last_time;
   ulong             m_interval;
   ENUM_TIME_ORIGIN  m_time_origin;
   bool              m_first_return;
   
public:

                     CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true);
                     CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true);
                     CIntervalWatcher(void);
                    ~CIntervalWatcher(void);
   
   //--- Setters
   void              SetInterval(ENUM_TIMEFRAMES interval);
   void              SetInterval(ulong interval);
   void              SetTimeOrigin(ENUM_TIME_ORIGIN time_origin);
   void              SetFirstReturn(bool first_return);
   
   //--- Getters
   ulong             GetInterval(void);
   ENUM_TIME_ORIGIN  GetTimeOrigin(void);
   bool              GetFirstReturn(void);
   ulong             GetLastTime(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CIntervalWatcher::CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true)
  {
   m_interval = PeriodSeconds(interval);
   m_time_origin = time_origin;
   m_first_return = first_return;
   m_last_time = 0;
  }
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CIntervalWatcher::CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true)
  {
   m_interval = interval;
   m_time_origin = time_origin;
   m_first_return = first_return;
   m_last_time = 0;
  }
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CIntervalWatcher::CIntervalWatcher(void)
  {
   m_interval = 10; // 10 seconds
   m_time_origin = TIME_ORIGIN_CURRENT;
   m_first_return = true;
   m_last_time = 0;
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CIntervalWatcher::~CIntervalWatcher(void)
  {
  }
//+------------------------------------------------------------------+
//| Set interval                                                     |
//+------------------------------------------------------------------+
void CIntervalWatcher::SetInterval(ENUM_TIMEFRAMES interval)
  {
   m_interval     = PeriodSeconds(interval);
  }
//+------------------------------------------------------------------+
//| Set interval                                                     |
//+------------------------------------------------------------------+
void CIntervalWatcher::SetInterval(ulong interval)
  {
   m_interval     = interval;
  }
//+------------------------------------------------------------------+
//| Set time origin                                                  |
//+------------------------------------------------------------------+
void CIntervalWatcher::SetTimeOrigin(ENUM_TIME_ORIGIN time_origin)
  {
   m_time_origin = time_origin;
  }
//+------------------------------------------------------------------+
//| Set initial return                                               |
//+------------------------------------------------------------------+
void CIntervalWatcher::SetFirstReturn(bool first_return)
  {
   m_first_return=first_return;
  }
//+------------------------------------------------------------------+
//| Get interval                                                     |
//+------------------------------------------------------------------+
ulong CIntervalWatcher::GetInterval(void)
  {
   return(m_interval);
  }
//+------------------------------------------------------------------+
//| Get time origin                                                  |
//+------------------------------------------------------------------+
ENUM_TIME_ORIGIN CIntervalWatcher::GetTimeOrigin(void)
  {
   return(m_time_origin);
  }
//+------------------------------------------------------------------+
//| Set initial return                                               |
//+------------------------------------------------------------------+
bool CIntervalWatcher::GetFirstReturn(void)
  {
   return(m_first_return);
  }
//+------------------------------------------------------------------+
//| Set last time                                                    |
//+------------------------------------------------------------------+
ulong CIntervalWatcher::GetLastTime(void)
  {
   return(m_last_time);
  }
//+------------------------------------------------------------------+

To help with the main method, let's create the GetTime function that returns the time based on the defined origin:

//+------------------------------------------------------------------+
//| Get time in miliseconds                                          |
//+------------------------------------------------------------------+
ulong CIntervalWatcher::GetTime(ENUM_TIME_ORIGIN time_origin)
  {
   switch(time_origin)
     {
      case(TIME_ORIGIN_CURRENT):
        return(TimeCurrent());
      case(TIME_ORIGIN_GMT):
        return(TimeGMT());
      case(TIME_ORIGIN_LOCAL):
        return(TimeLocal());
      case(TIME_ORIGIN_TRADE_SERVER):
        return(TimeTradeServer());
     }
   return(0);
  }
//+------------------------------------------------------------------+

The most important method of the class is Inspect(), which checks if the defined interval has been reached. The logic is as follows: On the first call, it checks if m_last_time is zero (newly instantiated class), the function stores the current time and returns m_first_return. If the stored timestamp is different from the current timestamp plus the interval, it means that the interval has been reached, so m_last_time is updated and the function returns true. If the timestamp is the same, it means that it has not been reached yet, so the function returns false.

//+------------------------------------------------------------------+
//| Check if there was an update                                     |
//+------------------------------------------------------------------+
bool CIntervalWatcher::Inspect(void)
  {
   //--- Get time
   ulong time_current = this.GetTime(m_time_origin);
   
   //--- First call, initial return
   if(m_last_time == 0)
     {
      m_last_time = time_current;
      return(m_first_return);
     }
   
   //--- Check interval
   if(time_current >= m_last_time + m_interval)
     {
      m_last_time = time_current;
      return(true);
     }
   return(false);
  }
//+------------------------------------------------------------------+

With CIntervalWatcher, we have more fine-grained control over log generation, allowing for programmable cycles and greater processing efficiency. This type of approach will be essential for a logging library that requires periodic execution of tasks. Now, with the periodic execution of log actions configured, we can focus on optimizing the recording process and maintaining system performance.


Optimizing Log Saving: Caching and File Rotation

Although the direct recording of logs to files that we implemented in the last article is a functional solution, it can become inefficient as the volume of logs grows. To avoid the negative impact on performance, it is necessary to optimize this process. In this topic, we will explore how to implement a caching and file rotation system to ensure that logs are written efficiently, without overloading storage and maintaining data integrity.

In the last article we discussed in more detail how these improvements work and their advantages:

“Imagine this scenario: an Expert Advisor running for weeks or months, recording every event, error or notification in the same file. Soon, that log starts to reach considerable sizes, making reading and interpreting information quite complex. This is where rotation comes in. It allows us to divide this information into smaller and organized pieces, making everything much easier to read and analyze.

The two most common ways to do this are:

  1. By Size: You set a size limit, usually in megabytes (MB), for the log file. When this limit is reached, a new file is automatically created, and the cycle starts again. This approach is very practical when the focus is on controlling log growth, without having to stick to a calendar. As soon as the current file reaches the size limit (in megabytes), the following flow occurs: The current log file is renamed, gaining an index, such as "log1.log". The existing files in the directory are also renumbered, such as "log1.log" becoming "log2.log". If the number of files reaches the maximum allowed, the oldest files are deleted. This approach is useful for limiting both the space occupied by the logs and the number of files saved.
  2. By Date: In this case, a new log file is created every day. Each one has in its name the date it was created, for example  log_2025-01-19.log, which already solves much of the headache of organizing logs. This approach is perfect for when you need to take a specific look at a particular day, without getting lost in a single gigantic file. This is the method I use most when saving my Expert Adivisors logs, everything is cleaner, more direct and easier to navigate.

In addition, you can also limit the number of log files stored. This control is very important to prevent unnecessary accumulation of old records. Imagine that you configure it to keep the 30 most recent files. When the 31st appears, the system automatically discards the oldest one, which prevents very old logs from accumulating on the disk, and the most recent ones are kept.

Another crucial detail is the use of a cache. Instead of writing each message directly to the file as soon as it arrives, the messages are temporarily stored in the cache. When the cache reaches a defined limit, it dumps all the content in the file at once. This results in fewer read and write operations on the disk, more performance and a longer lifespan for your storage devices.”

To implement log file rotation, we first need a helper method called SearchForFilesInDirectory(). This method is responsible for searching for all files present in a specific directory and returning their names in an array. It uses the FileFindFirst() function to start the search, and as it finds files, their names are added to this array. After the process is complete, the method closes the search handler using FileFindClose().

But why is this method so important? Simple! It allows us to list the existing log files, ensuring that the class that manages the logs deletes older files when necessary.

class CLogifyHandlerFile : public CLogifyHandler
  {
private:
   bool              SearchForFilesInDirectory(string directory, string &file_names[]);
  };
//+------------------------------------------------------------------+
//| Returns an array with the names of all files in the directory    |
//+------------------------------------------------------------------+
bool CLogifyHandlerFile::SearchForFilesInDirectory(string directory,string &file_names[])
  {
   //--- Search for all log files in the specified directory with the given file extension
   string file_name;
   long search_handle = FileFindFirst(directory,file_name);
   ArrayFree(file_names);
   bool is_found = false;
   if(search_handle != INVALID_HANDLE)
     {
      do
        {
         //--- Add each file name found to the array of file names
         int size_file = ArraySize(file_names);
         ArrayResize(file_names,size_file+1);
         file_names[size_file] = file_name;
         is_found = true;
        }
      while(FileFindNext(search_handle,file_name));
      FileFindClose(search_handle);
     }
   
   return(is_found);
  }
//+------------------------------------------------------------------+

Now that we have the function to fetch the files, we can incorporate it into the main method responsible for emitting the logs, Emit(). Depending on the rotation configuration chosen, the logic will be adjusted.

If log rotation is configured to occur based on file size, the function:

  • Checks if the file size has exceeded the configured limit (m_config.max_file_size_mb).
  • Searches all log files in the directory.
  • Removes old files that exceed the maximum number allowed (m_config.max_file_count).
  • Renames old files, incrementing their indexes numerically (log1.txt, log2.txt, etc.).
  • Renames the current log file as "log1" to maintain the sequence.

If rotation is based on date, the function:

  • Searches all log files in the directory.
  • Deletes the oldest files that exceed the maximum allowed number (m_config.max_file_count).

Now, let's see the implementation of the Emit() method with both rotation logics:

//+------------------------------------------------------------------+
//| Processes a log message and sends it to the specified destination|
//+------------------------------------------------------------------+
void CLogifyHandlerFile::Emit(MqlLogifyModel &data)
  {
   //--- Checks if the configured level allows
   if(data.level >= this.GetLevel())
     {
      //--- Get the full path of the file
      string log_path = this.LogPath();
      
      //--- Open file
      ResetLastError();
      int handle_file = m_file.Open(log_path, FILE_READ | FILE_WRITE | FILE_ANSI);
      if(handle_file == INVALID_HANDLE)
        {
         Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")");
         return;
        }
      
      //--- Write
      m_file.Seek(0, SEEK_END);
      m_file.WriteString(data.formated + "\n");
      
      //--- Size in megabytes
      ulong size_mb = m_file.Size() / (1024 * 1024);
      
      //--- Close file
      m_file.Close();
      
      string file_extension = this.LogFileExtensionToStr(m_config.file_extension);
      
      //--- Check if the log rotation mode is based on file size
      if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE)
        {
         //--- Check if the current file size exceeds the maximum configured size
         if(size_mb >= m_config.max_file_size_mb)
           {
            //--- Search files
            string file_names[];
            if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names))
              {
               //--- Delete files exceeding the configured maximum number of log files
               int size_file = ArraySize(file_names);
               for(int i=size_file-1;i>=0;i--)
                 {
                  //--- Extract the numeric part of the file index
                  string file_index = file_names[i];
                  StringReplace(file_index,file_extension,"");
                  StringReplace(file_index,m_config.base_filename,"");
                  
                  //--- If the file index exceeds the maximum allowed count, delete the file
                  if(StringToInteger(file_index) >= m_config.max_file_count)
                    {
                     FileDelete(m_config.directory + "\\" + file_names[i]);
                    }
                 }
               
               //--- Rename existing log files by incrementing their indices
               for(int i=m_config.max_file_count-1;i>=0;i--)
                 {
                  string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension;
                  string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension;
                  if(FileIsExist(old_file))
                    {
                     FileMove(old_file, 0, new_file, FILE_REWRITE);
                    }
                 }
               
               //--- Rename the primary log file to include the index "1"
               string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension;
               FileMove(log_path, 0, new_primary, FILE_REWRITE);
              }
           }
        }
      //--- Check if the log rotation mode is based on date
      else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE)
        {
         //--- Search files
         string file_names[];
         if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names))
           {
            //--- Delete files exceeding the maximum configured number of log files
            int size_file = ArraySize(file_names);
            for(int i=size_file-1;i>=0;i--)
              {
               if(i < size_file - m_config.max_file_count)
                 {
                  FileDelete(m_config.directory + "\\" + file_names[i]);
                 }
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+


Saving by blocks for better performance

Moving on to another improvement, let's create the logic that I consider the most interesting in the article, saving records by blocks. The central idea is to implement a cache (temporary memory), where log records will be stored until they reach a defined limit. When this limit is reached, all records in the cache are saved in the log file at once.

We will implement this logic in steps. First, we will create the cache structure in the CLogifyHandlerFile class. In the private section of the class, we will add an array of type MqlLogifyModel to temporarily store the log records. We also include a variable to control the current index of the last value saved in the cache. Whenever a new record is added, this index will be incremented. We also create an instance of the CIntervalWatcher class, and set an interval of one day in the constructor. See how it looks:

class CLogifyHandlerFile : public CLogifyHandler
  {
private:
   //--- Update utilities
   CIntervalWatcher  m_interval_watcher;
   
   //--- Cache data
   MqlLogifyModel    m_cache[];
   int               m_index_cache;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyHandlerFile::CLogifyHandlerFile(void)
  {
   m_interval_watcher.SetInterval(PERIOD_D1);
   ArrayFree(m_cache);
   m_index_cache = 0;
  }
//+------------------------------------------------------------------+

With the cache and update structure created, we move on to the next step: modifying the Emit() method to use the cache.

The Emit() method is responsible for processing a log message and sending it to the configured destination (in this case, a file). We will adapt it so that, instead of saving the data directly to the file, it temporarily stores it in the cache. When the cache reaches its configured limit, or the defined interval (one day), the method calls the Flush() function, which saves the accumulated records in the file. This interval is useful because if the data has been cached for more than one day, this mechanism ensures that the data is still saved daily, and also allows the rotation routine to be executed every day.

Here is the modified code:

//+------------------------------------------------------------------+
//| Processes a log message and sends it to the specified destination|
//+------------------------------------------------------------------+
void CLogifyHandlerFile::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();
           }
        }
     }
  }
//+------------------------------------------------------------------+

The Flush() function is responsible for saving the cache data to the file. This process involves opening the file, positioning the pointer at the end and writing all the records stored in the cache.

//+------------------------------------------------------------------+
//| Clears or completes any pending operations                       |
//+------------------------------------------------------------------+
void CLogifyHandlerFile::Flush(void)
  {
   //--- Get the full path of the file
   string log_path = this.LogPath();
   
   //--- Open file
   ResetLastError();
   int handle_file = FileOpen(log_path, FILE_READ|FILE_WRITE|FILE_ANSI, '\t', m_config.codepage);
   if(handle_file == INVALID_HANDLE)
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")");
      return;
     }
   
   //--- Loop through all cached messages
   int size = ArraySize(m_cache);
   for(int i=0;i<size;i++)
     {
      if(m_cache[i].timestamp > 0)
        {
         //--- Point to the end of the file and write the message
         FileSeek(handle_file, 0, SEEK_END);
         FileWrite(handle_file, m_cache[i].formated);
        }
     }
      
   //--- Size in megabytes
   ulong size_mb = FileSize(handle_file) / (1024 * 1024);
   
   //--- Close file
   FileClose(handle_file);
   
   string file_extension = this.LogFileExtensionToStr(m_config.file_extension);
   
   //--- Check if the log rotation mode is based on file size
   if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE)
     {
      //--- Check if the current file size exceeds the maximum configured size
      if(size_mb >= m_config.max_file_size_mb)
        {
         //--- Search files
         string file_names[];
         if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names))
           {
            //--- Delete files exceeding the configured maximum number of log files
            int size_file = ArraySize(file_names);
            for(int i=size_file-1;i>=0;i--)
              {
               //--- Extract the numeric part of the file index
               string file_index = file_names[i];
               StringReplace(file_index,file_extension,"");
               StringReplace(file_index,m_config.base_filename,"");
               
               //--- If the file index exceeds the maximum allowed count, delete the file
               if(StringToInteger(file_index) >= m_config.max_file_count)
                 {
                  FileDelete(m_config.directory + "\\" + file_names[i]);
                 }
              }
            
            //--- Rename existing log files by incrementing their indices
            for(int i=m_config.max_file_count-1;i>=0;i--)
              {
               string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension;
               string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension;
               if(FileIsExist(old_file))
                 {
                  FileMove(old_file, 0, new_file, FILE_REWRITE);
                 }
              }
            
            //--- Rename the primary log file to include the index "1"
            string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension;
            FileMove(log_path, 0, new_primary, FILE_REWRITE);
           }
        }
     }
   //--- Check if the log rotation mode is based on date
   else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE)
     {
      //--- Search files
      string file_names[];
      if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names))
        {
         //--- Delete files exceeding the maximum configured number of log files
         int size_file = ArraySize(file_names);
         for(int i=size_file-1;i>=0;i--)
           {
            if(i < size_file - m_config.max_file_count)
              {
               FileDelete(m_config.directory + "\\" + file_names[i]);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

With this implementation, we have created an efficient and scalable logging solution, capable of handling large volumes of data without compromising the performance of your expert. Finally, we need to ensure that when the program closes, all cached data is saved to the file. To do this, simply call the Flush() method in the Close() method, which is already called in the destructor of the CLogify base class.

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyHandlerFile::~CLogifyHandlerFile(void)
  {
   this.Close();
  }
//+------------------------------------------------------------------+
//| Closes the handler and releases any resources                    |
//+------------------------------------------------------------------+
void CLogifyHandlerFile::Close(void)
  {
   //--- Save cache
   Flush();
  }
//+------------------------------------------------------------------+

By implementing caching and file rotation, we reduce the number of write operations to disk and ensure that our logs are stored more efficiently. This gives our library performance and scalability, making it more robust for real applications. But do these optimizations really make a difference? Let's test it.


Performance Tests: Measuring the Efficiency of the Improvements

Now that we have implemented the optimizations, we need to measure their real impact. Performance tests will help us understand whether the cache is reducing the write load and whether file rotation is working as expected. To do this, we will run the same test performed in the last article, comparing the original version of the library with the optimized version.

To run the test, we will use the same file, with some modifications to the formatter, since now each handler has its own formatter. The changes are highlighted as follows:

  • Green: Additions to the code
  • Red: Removals
  • Yellow: The parameter that defines the cache size. The larger the cache, the faster the processing.
//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleFileConfig m_config;
   m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,10);
   
   //--- Handler File
   CLogifyHandlerFile *handler_file = new CLogifyHandlerFile();
   handler_file.SetConfig(m_config);
   handler_file.SetLevel(LOG_LEVEL_DEBUG);
   handler_file.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));
   
   //--- Add handler in base class
   logify.AddHandler(handler_file);
   logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));
   
   //--- Using logs
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678");
   logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance");
   logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Let's start a test in the strategy tester using the same date and symbol parameters.

Using the "OHLC for 1 minute" model on the EURUSD symbol and a 7-day time frame, the execution time was 26 seconds. It is worth noting that every tick, a new log record is generated, and the cache is set to store 10 messages. Now, let's increase the cache to 100 messages and observe the difference in performance:

With this change, we were able to reduce the test time by seconds, while maintaining the same modeling, date, and symbol settings. If we compare it to the first test performed in the previous article, which took 5 minutes and 11 seconds, the improvement is impressive!

The results demonstrate that small optimizations can generate significant gains in efficiency. The combination of caching and file rotation makes log management more agile and reliable, validating the choices made so far. But how can these improvements be applied in practice? Let's explore some usage examples.


Examples of Using the Log Library

Now that we have improved our log library, it's time to put it into action! Let's explore practical examples of how to use it to create different types of log files, each with its own formatting and severity level.

Example 1: Separate Logs into .log and .json Files

In the first scenario, we set up two log files: one in .log format and the other in .json format. Each has a specific format and a different severity level, making it easier to manage and analyze the logs.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleFileConfig m_config;
   m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1);
   
   //--- Handler File (.log)
   CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile();
   handler_file_log.SetConfig(m_config);
   handler_file_log.SetLevel(LOG_LEVEL_DEBUG);
   handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));
   
   //--- Handler File (.json)
   m_config.CreateNoRotationConfig("expert","logs",LOG_FILE_EXTENSION_JSON,1);
   CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile();
   handler_file_json.SetConfig(m_config);
   handler_file_json.SetLevel(LOG_LEVEL_ALERT);
   handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}"));
   
   //--- Add handler in base class
   logify.AddHandler(handler_file_log);
   logify.AddHandler(handler_file_json);
   
   //--- Using logs
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678");
   logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance");
   logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Here, we use the same m_config configuration variable, changing only the values needed to define both log formats. This makes the configuration simpler and more reusable.

Example 2: Storing Only Errors in a JSON File

Now, let's go a step further and configure a specific log to store only error messages. To do this, we create a separate folder where this .json file will be saved. Additionally, we add a console handler to display logs directly in the terminal.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleFileConfig m_config;
   m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1);
   
   //--- Handler File (.log)
   CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile();
   handler_file_log.SetConfig(m_config);
   handler_file_log.SetLevel(LOG_LEVEL_DEBUG);
   handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));
   
   //--- Handler File (.json)
   m_config.CreateNoRotationConfig("expert","logs\\error",LOG_FILE_EXTENSION_JSON,1);
   CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile();
   handler_file_json.SetConfig(m_config);
   handler_file_json.SetLevel(LOG_LEVEL_ERROR);
   handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}"));
   
   //--- Handler Console
   CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole();
   handler_console.SetLevel(LOG_LEVEL_DEBUG);
   handler_console.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname} | {origin}] {msg}"));
   
   //--- Add handler in base class
   logify.AddHandler(handler_file_log);
   logify.AddHandler(handler_file_json);
   logify.AddHandler(handler_console);
   
   //--- Using logs
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678");
   logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance");
   logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

In this example, we use three log handlers:

  • .log file → Stores logs in traditional format.
  • .json file → Stores only error messages inside a separate folder.
  • Console → Displays logs in a more readable way for the user.

Using a more "human" formatter in the console helps to make the output more understandable, while the JSON of errors makes it easier to analyze later.

With these examples, it is clear how our logging library can be applied in real projects. The flexibility to create different formats and severity levels allows for good management, helping to identify and troubleshoot problems more easily. In addition, the modular structure makes it easy to expand the logging system as needed.

Now all you have to do is adapt this implementation to your needs and ensure that your logs are always well organized and accessible!


Conclusion

In this article, we have evolved our logging library, making it more efficient, scalable and adaptable. We have refined the formatting, allowing each handler to have its own formatter, making the messages more organized and flexible for different needs, such as local debugging and auditing.

We have implemented the CIntervalWatcher class, which controls execution cycles, ensuring that logs are written and rotated at well-defined intervals. We have also optimized writing with caching, reducing disk operations and better managing file growth. We have validated these improvements with performance tests, further refining the solution to support high load. In addition, we have presented practical examples to facilitate the adoption of the library.

If there is one main lesson to be extracted from this article, it is the importance of treating logging as an essential aspect of software development. A well-designed logging system not only facilitates debugging and subsequent auditing, but also helps in the security, traceability and reliability of an Expert Advisor. Implementing good logging practices early in development can save you the headaches of development, making maintenance easier and troubleshooting more efficient. In the next article, we’ll explore how to store logs in a database for advanced analytics. See you there!

File Name
Description
Experts/Logify/LogiftTest.mq5
File where we test the library's features, containing a practical example
Include/Logify/Formatter/LogifyFormatter.mqh
Class responsible for formatting log records, replacing placeholders with specific values
Include/Logify/Handlers/LogifyHandler.mqh
Base class for managing log handlers, including level setting and log sending
Include/Logify/Handlers/LogifyHandlerConsole.mqh
Log handler that sends formatted logs directly to the terminal console in MetaTrader
Include/Logify/Handlers/LogifyHandlerDatabase.mqh
Log handler that sends formatted logs to a database (Currently it only contains a printout, but soon we will save it to a real sqlite database)
Include/Logify/Handlers/LogifyHandlerFile.mqh
Log handler that sends formatted logs to a file
Include/Logify/Utils/IntervalWatcher.mqh Checks if a time interval has passed, allowing you to create routines within the library
Include/Logify/Logify.mqh
Core class for log management, integrating levels, models and formatting
Include/Logify/LogifyLevel.mqh
File that defines the log levels of the Logify library, allowing for detailed control
Include/Logify/LogifyModel.mqh
Structure that models log records, including details such as level, message, timestamp, and context
Attached files |
Logify2Part5c.zip (17.06 KB)
Robustness Testing on Expert Advisors Robustness Testing on Expert Advisors
In strategy development, there are many intricate details to consider, many of which are not highlighted for beginner traders. As a result, many traders, myself included, have had to learn these lessons the hard way. This article is based on my observations of common pitfalls that most beginner traders encounter when developing strategies on MQL5. It will offer a range of tips, tricks, and examples to help identify the disqualification of an EA and test the robustness of our own EAs in an easy-to-implement way. The goal is to educate readers, helping them avoid future scams when purchasing EAs as well as preventing mistakes in their own strategy development.
Automating Trading Strategies in MQL5 (Part 6): Mastering Order Block Detection for Smart Money Trading Automating Trading Strategies in MQL5 (Part 6): Mastering Order Block Detection for Smart Money Trading
In this article, we automate order block detection in MQL5 using pure price action analysis. We define order blocks, implement their detection, and integrate automated trade execution. Finally, we backtest the strategy to evaluate its performance.
Neural Networks in Trading: Using Language Models for Time Series Forecasting Neural Networks in Trading: Using Language Models for Time Series Forecasting
We continue to study time series forecasting models. In this article, we get acquainted with a complex algorithm built on the use of a pre-trained language model.
Neural Networks in Trading: Lightweight Models for Time Series Forecasting Neural Networks in Trading: Lightweight Models for Time Series Forecasting
Lightweight time series forecasting models achieve high performance using a minimum number of parameters. This, in turn, reduces the consumption of computing resources and speeds up decision-making. Despite being lightweight, such models achieve forecast quality comparable to more complex ones.