English 中文 日本語
preview
Nutzerdefinierte Debugging- und Profiling-Tools für die MQL5-Entwicklung (Teil I): Erweiterte Protokollierung

Nutzerdefinierte Debugging- und Profiling-Tools für die MQL5-Entwicklung (Teil I): Erweiterte Protokollierung

MetaTrader 5Integration |
145 0
Sahil Bagdi
Sahil Bagdi

Der Plan sieht folgendermaßen aus:

  1. Einführung
  2. Aufbau eines nutzerdefinierten Logging Frameworks
  3. Verwendung des Logging Frameworks
  4. Vorteile des Custom Logging Framework
  5. Schlussfolgerung



Einführung

Jeder, der Zeit damit verbracht hat, Expert Advisors, Indikatoren oder Skripte in MQL5 zu schreiben, kennt die Frustration: Ein Live-Handel verhält sich seltsam, eine komplexe Formel spuckt die falsche Zahl aus, oder Ihr EA kommt zum Stillstand, gerade wenn der Markt sich aufheizt. Die übliche schnelle Lösung - das Verteilen von Print()-Anweisungen, das Starten des Strategietesters und das Beten, dass das Problem auftaucht - bricht zusammen, sobald Ihre Codebasis größer wird.

MQL5 stellt Debugging-Hürden auf, die normale Programmiersprachen nicht haben. Handelsprogramme laufen in Echtzeit (daher ist das Timing wichtig), handeln mit echtem Geld (daher sind Fehler kostspielig) und müssen auch in volatilen Märkten blitzschnell sein. Die integrierten Funktionen des MetaEditors - ein Schritt-für-Schritt-Debugger, Print() und Comment() für grundlegende Ausgaben und ein High-Level-Profiler - sind hilfreich, aber allgemein gehalten. Sie wurden einfach nicht für die präzise Diagnose entwickelt, die Ihre Handelsalgorithmen benötigen.

Deshalb ist die Entwicklung eines eigenen Debugging- und Profiling-Toolkits von entscheidender Bedeutung. Maßgeschneiderte Hilfsprogramme können den feinkörnigen Einblick und die nutzerdefinierten Arbeitsabläufe liefern, die im Standardsatz fehlen, sodass Sie Fehler früher erkennen, die Leistung optimieren und die Codequalität sichern können.

Diese Serie wird Sie bei der Zusammenstellung eines solchen Instrumentariums unterstützen. Wir beginnen mit dem Grundstein - einem vielseitigen Logging-Framework, das weitaus leistungsfähiger ist als vereinzelte Print()-Aufrufe - und setzen dann fortgeschrittene Debugger, nutzerdefinierte Profiler, ein Unit-Testing-Harness und statische Code-Checker ein. Am Ende verfügen Sie über eine vollständige Suite, die aus der „Brandbekämpfung“ eine proaktive Qualitätskontrolle macht.

Jede Folge ist praxisorientiert: vollständige, sofort einsetzbare MQL5-Beispiele, detaillierte Erklärungen, wie sie funktionieren, und Begründungen für jede Designentscheidung. Sie erhalten Tools, die Sie sofort einsetzen können, und das Know-how, um sie an Ihre eigenen Projekte anzupassen.

Erstens: die grundlegendste diagnostische Notwendigkeit überhaupt, genau zu sehen, was Ihr Programm tut, Moment für Moment. Lassen Sie uns das nutzerdefinierte Logging-Framework erstellen.


Aufbau eines nutzerdefinierten Logging Frameworks

In diesem Abschnitt werden wir ein flexibles, leistungsfähiges Logging-Framework entwickeln, das weit über die grundlegende Print()-Funktion von MQL5 hinausgeht. Unser nutzerdefinierter Logger unterstützt mehrere Ausgabeformate, Schweregrade und kontextbezogene Informationen, die das Debugging komplexer Handelssysteme wesentlich effektiver machen.

Warum das übliche Print() zu kurz greift

Bevor wir die Ärmel hochkrempeln und das neue System aufbauen, ist es hilfreich zu verstehen, warum Print() allein für professionelle Projekte nicht ausreicht:

  1. Keine Schweregradhierarchie - jede Nachricht landet in der gleichen Gruppe, sodass kritische Alarme unter dem Routinegeschwätz begraben werden.
  2. Spärlicher Kontext - Print kann Ihnen nicht sagen, welche Funktion die Nachricht ausgelöst hat oder wie der Zustand der Anwendung zu diesem Zeitpunkt war.
  3. Einspurige Ausgabe - alles fließt in die Registerkarte „Experten“; es gibt keinen integrierten Pfad zu Dateien oder alternativen Zielen.
  4. Zero Filtering - Sie können ausführliche Debug-Protokolle in der Produktion nicht unterdrücken, ohne auch die Fehler zu unterdrücken, die Sie interessieren.
  5. Unstrukturierter Text - die Freiformausgabe ist für Tools schwer automatisch zu analysieren.

Unser maßgeschneidertes Logging-Framework behebt alle diese Probleme und schafft eine solide Grundlage für die Fehlersuche in anspruchsvollem Handelscode.

Die Architektur des Loggers

Wir werden ein sauberes, modulares, objektorientiertes System um drei Kernstücke herum aufbauen:
  1. LogLevels: ein Enum, das die Schweregrade angibt (DEBUG, INFO, WARN, ERROR, FATAL).
  2. ILogHandler: eine Schnittstelle, die es ermöglicht, verschiedene Senken einzubinden, wie z. B. FileLogHandler oder ConsoleLogHandler.
  3. CLogger: ein Singleton-Orchestrator, der die Handler enthält und die Protokollierungs-API bereitstellt.

Wir werden jeden Teil als nächstes auspacken.

Log-Stufen

Zunächst definieren wir die Schweregrade in LogLevels.mqh:

enum LogLevel
{
   LOG_LEVEL_DEBUG = 0, // Detailed information for debugging purposes.
   LOG_LEVEL_INFO  = 1, // General information about the system's operation.
   LOG_LEVEL_WARN  = 2, // Warnings about potential issues that are not critical.
   LOG_LEVEL_ERROR = 3, // Errors that affect parts of the system but allow continuity.
   LOG_LEVEL_FATAL = 4, // Serious problems that interrupt the system's execution.
   LOG_LEVEL_OFF   = 5  // Turn off logging.
};

Diese Ebenen ermöglichen es uns, Nachrichten nach Wichtigkeit zu kategorisieren und entsprechend zu filtern. Während der Entwicklung möchten Sie vielleicht alle Meldungen (einschließlich DEBUG) sehen, aber in der Produktion möchten Sie vielleicht nur WARN und höher sehen.

Die Handler-Schnittstelle

Als nächstes definieren wir eine Schnittstelle für Log-Handler in ILogHandler.mqh :

#property strict

#include "LogLevels.mqh"
#include <Arrays/ArrayObj.mqh> // For managing handlers

//+------------------------------------------------------------------+
//| Interface: ILogHandler                                           |
//| Description: Defines the contract for log handling mechanisms.   |
//|              Each handler is responsible for processing and      |
//|              outputting log messages in a specific way (e.g., to |
//|              console, file, database).                           |
//+------------------------------------------------------------------+
interface ILogHandler
  {
//--- Method to configure the handler with specific settings
   virtual bool      Setup(const string settings="");

//--- Method to process and output a log message
   virtual void      Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0);

//--- Method to perform any necessary cleanup
   virtual void      Shutdown();
  };
//+------------------------------------------------------------------+

Diese Header-Datei, ILogHandler.mqh, definiert eine wichtige Komponente des Logging-Frameworks: die ILogHandler-Schnittstelle. Eine Schnittstelle in MQL5 fungiert als Blaupause oder Vertrag und spezifiziert eine Reihe von Methoden, die jede Klasse, die sie implementiert, bereitstellen muss. Der Zweck von ILogHandler ist es, zu standardisieren, wie verschiedene Protokollausgabemechanismen (wie das Schreiben auf die Konsole oder in eine Datei) mit der Hauptloggerklasse interagieren.

Die Schnittstelle von ILogHandler selbst deklariert drei virtuelle Methoden, die konkrete Handler-Klassen implementieren müssen:
  • virtual bool Setup(const string settings=““): Diese Methode ist für die Initialisierung und Konfiguration des spezifischen Log-Handlers vorgesehen. Sie akzeptiert ein optionales String-Argument (settings), mit dem Konfigurationsparameter (wie Dateipfade, Formatierungszeichenfolgen oder Mindestprotokollstufen) während der Einrichtungsphase an den Handler übergeben werden können. Die Methode gibt true zurück, wenn die Einrichtung erfolgreich war, und andernfalls false, sodass der Hauptlogger weiß, ob der Handler einsatzbereit ist.
  • virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0): Dies ist die Hauptmethode, die für die Verarbeitung und Ausgabe einer einzelnen Protokollmeldung zuständig ist. Es erhält alle notwendigen Details über das Log-Ereignis: den Zeitstempel (time), den Schweregrad (level aus LogLevels.mqh), die Quelle oder den Ursprung der Nachricht (origin), den eigentlichen Text der Log-Nachricht (message) und eine optionale Expertenberater-ID (expert_id). Jede implementierende Klasse legt fest, wie und wohin diese Informationen je nach ihrem spezifischen Zweck (z. B. Drucken auf der Konsole, Schreiben in eine Datei) zu senden sind.
  • virtual void Shutdown(): Diese Methode ist für die Durchführung von Aufräumarbeiten vorgesehen, wenn der Log-Handler nicht mehr benötigt wird, typischerweise während der Shutdown-Sequenz des Hauptloggers oder der Anwendung. Implementierungen können diese Methode verwenden, um offene Dateihandles zu schließen, zugewiesene Ressourcen freizugeben oder alle gepufferten Ausgaben zu leeren, um sicherzustellen, dass alle Protokolle vor der Beendigung gespeichert werden.

Durch die Definition dieser Standardschnittstelle erreicht das Logging Framework Flexibilität und Erweiterbarkeit. Die Hauptklasse CLogger kann eine Sammlung verschiedener ILogHandler-Objekte verwalten und Protokollnachrichten an jedes einzelne Objekt senden, ohne die spezifischen Details der Funktionsweise jedes Handlers kennen zu müssen. Neue Ausgabeziele können einfach hinzugefügt werden, indem neue Klassen erstellt werden, die die ILogHandler-Schnittstelle implementieren.

Konsolen-Log-Handler

Diese Header-Datei enthält die Klasse ConsoleLogHandler, eine konkrete Implementierung der Schnittstelle ILogHandler. Sein spezifischer Zweck besteht darin, formatierte Protokollnachrichten an die Registerkarte „Experts“ der MetaTrader 5-Plattform zu leiten, die als Konsolenausgabebereich während der Ausführung von Expert Advisor (EA) oder Skripten dient.

#property strict

#include "ILogHandler.mqh"
#include "LogLevels.mqh"

//+------------------------------------------------------------------+
//| Class: ConsoleLogHandler                                         |
//| Description: Implements ILogHandler to output log messages to    |
//|              the MetaTrader 5 Experts tab (console).             |
//+------------------------------------------------------------------+
class ConsoleLogHandler : public ILogHandler
  {
private:
   LogLevel          m_min_level;       // Minimum level to log
   string            m_format;          // Log message format string

   //--- Helper to format the log message
   string            FormatMessage(const datetime time, const LogLevel level, const string origin, const string message);
   //--- Helper to get string representation of LogLevel
   string            LogLevelToString(const LogLevel level);

public:
                     ConsoleLogHandler(const LogLevel min_level = LOG_LEVEL_INFO, const string format = "[{time}] {level}: {origin} - {message}");
                    ~ConsoleLogHandler();

   //--- ILogHandler implementation
   virtual bool      Setup(const string settings="") override;
   virtual void      Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) override;
   virtual void      Shutdown() override;

   //--- Setters
   void              SetMinLevel(const LogLevel level) { m_min_level = level; }
   void              SetFormat(const string format)    { m_format = format; }
  };

Die Klasse ConsoleLogHandler erbt öffentlich von ILogHandler, d. h. sie verspricht, Implementierungen für die in der Schnittstelle definierten Methoden Setup, Log und Shutdown bereitzustellen. Sie enthält zwei private Mitgliedsvariablen: m_min_level vom Typ LogLevel speichert den minimalen Schweregrad, den eine Nachricht haben muss, um von diesem Handler protokolliert zu werden, und m_format vom Typ string enthält die Vorlage, die zur Formatierung der Ausgabemeldung verwendet wird. Sie deklariert auch private Hilfsmethoden FormatMessage und LogLevelToString sowie öffentliche Methoden für die Schnittstellenimplementierung und Setter für ihre privaten Mitglieder.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
ConsoleLogHandler::ConsoleLogHandler(const LogLevel min_level = LOG_LEVEL_INFO, const string format = "[{time}] {level}: {origin} - {message}")
  {
   m_min_level = min_level;
   m_format = format;
   // No specific setup needed for console logging initially
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
ConsoleLogHandler::~ConsoleLogHandler()
  {
   // No specific cleanup needed
  }

Der Konstruktor initialisiert ein neues ConsoleLogHandler-Objekt. Er akzeptiert zwei optionale Argumente: min_level (standardmäßig LOG_LEVEL_INFO) und format (standardmäßig eine Standardvorlage „[{time}] {level}: {Ursprung} - {Nachricht}“). Diese Argumente werden verwendet, um die Anfangswerte der Mitgliedsvariablen m_min_level und m_format zu setzen. Dies ermöglicht es den Nutzern, die Filterstufe und das Erscheinungsbild der Ausgabe bei der Erstellung des Handlers zu konfigurieren.

Der Destruktor ist für das Aufräumen der Ressourcen verantwortlich, wenn ein ConsoleLogHandler-Objekt zerstört wird. In dieser speziellen Implementierung gibt es keine dynamisch zugewiesenen Ressourcen oder offene Handles, die direkt von dieser Klasse verwaltet werden, sodass der Destruktorkörper leer ist, was darauf hinweist, dass keine besonderen Aufräumaktionen für diesen Handler erforderlich sind.

//+------------------------------------------------------------------+
//| Setup                                                            |
//+------------------------------------------------------------------+
bool ConsoleLogHandler::Setup(const string settings="")
  {
   // Settings could be used to parse format or min_level, but we use constructor args for now
   // Example: Parse settings string if needed
   return true;
  }
//+------------------------------------------------------------------+
//| Log                                                              |
//+------------------------------------------------------------------+
void ConsoleLogHandler::Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0)
  {
   // Check if the message level meets the minimum requirement
   if(level >= m_min_level && level < LOG_LEVEL_OFF)
     {
      // Format and print the message to the Experts tab
      Print(FormatMessage(time, level, origin, message));
     }
  }
//+------------------------------------------------------------------+
//| Shutdown                                                         |
//+------------------------------------------------------------------+
void ConsoleLogHandler::Shutdown()
  {
   // No specific shutdown actions needed for console logging
   PrintFormat("%s: ConsoleLogHandler shutdown.", __FUNCTION__);
  }

  1. Setup-Methode (ConsoleLogHandler::Setup):
    Diese Methode implementiert die von der ILogHandler-Schnittstelle benötigte Setup-Funktion. Obwohl für die Konfiguration vorgesehen, verwendet die aktuelle Implementierung den Parameter „settings string“ nicht, da die primäre Konfiguration (Mindeststufe und Format) über den Konstruktor erfolgt. Es wird einfach true zurückgegeben und das zeigt damit an, dass der Handler nach der Erstellung immer als einsatzbereit betrachtet wird.

  2. Die Methode Log (ConsoleLogHandler::Log):
    Dies ist die Kernimplementierung der Protokollierungsaktion für die Konsole. Beim Aufruf durch den Haupt-CLogger wird zunächst geprüft, ob der bereitgestellte Level der eingehenden Nachricht größer oder gleich dem vom Handler konfigurierten m_min_level ist und außerdem kleiner als LOG_LEVEL_OFF. Wenn die Nachricht diesen Filter besteht, ruft die Methode die private Hilfsfunktion FormatMessage auf, um die endgültige Ausgabezeichenfolge auf der Grundlage der Vorlage m_format und der angegebenen Protokolldetails (Zeit, Ebene, Ursprung, Nachricht) zu erstellen. Schließlich wird die integrierte MQL5-Druckfunktion verwendet, um die formatierte Zeichenfolge in der Registerkarte Experten anzuzeigen.

  3. Methode zum Herunterfahren (ConsoleLogHandler::Shutdown):
    Diese Methode implementiert die Funktion Shutdown der Schnittstelle. Ähnlich wie der Destruktor erfordert die Konsolenprotokollierung in der Regel keine speziellen Beendigungsaktionen wie das Schließen von Dateien. Diese Implementierung gibt einfach eine Meldung aus, die anzeigt, dass der Konsolenhandler heruntergefahren wird, und liefert so eine Bestätigung während der Beendigungssequenz der Anwendung.

//+------------------------------------------------------------------+
//| FormatMessage                                                    |
//+------------------------------------------------------------------+
string ConsoleLogHandler::FormatMessage(const datetime time, const LogLevel level, const string origin, const string message)
  {
   string formatted_message = m_format;

   // Replace placeholders
   StringReplace(formatted_message, "{time}", TimeToString(time, TIME_DATE | TIME_SECONDS));
   StringReplace(formatted_message, "{level}", LogLevelToString(level));
   StringReplace(formatted_message, "{origin}", origin);
   StringReplace(formatted_message, "{message}", message);

   return formatted_message;
  }
//+------------------------------------------------------------------+
//| LogLevelToString                                                 |
//+------------------------------------------------------------------+
string ConsoleLogHandler::LogLevelToString(const LogLevel level)
  {
   switch(level)
     {
      case LOG_LEVEL_DEBUG: return "DEBUG";
      case LOG_LEVEL_INFO:  return "INFO";
      case LOG_LEVEL_WARN:  return "WARN";
      case LOG_LEVEL_ERROR: return "ERROR";
      case LOG_LEVEL_FATAL: return "FATAL";
      default:              return "UNKNOWN";
     }
  }
//+------------------------------------------------------------------+
  1. Hilfsmethode (FormatMessage):
    Diese private Hilfsfunktion nimmt die rohen Logdetails (Zeit, Level, Ursprung, Nachricht) und den Formatstring des Handlers (m_format) als Eingabe. Sie ersetzt Platzhalter wie {time}, {level}, {origin} und {message} in der Formatzeichenkette durch die entsprechenden tatsächlichen Werte. Es verwendet TimeToString zur Formatierung des Zeitstempels und ruft LogLevelToString auf, um die String-Darstellung des Schweregrads zu erhalten. Die resultierende vollständig formatierte Zeichenfolge wird dann zum Drucken an die Log-Methode zurückgegeben.

  2. Hilfsmethode (LogLevelToString):
    Diese private Hilfsfunktion konvertiert einen LogLevel-Enum-Wert in seine entsprechende String-Darstellung (z.B. wird LOG_LEVEL_INFO zu „INFO“). Sie verwendet eine Switch-Anweisung, um die definierten Protokollebenen zu behandeln und gibt bei unerwarteten Werten „UNKNOWN“ zurück. Dadurch werden in der formatierten Protokollausgabe für den Menschen lesbare Log-Ausdrucke bereitgestellt.

  3. Setter-Methoden (SetMinLevel, SetFormat): Diese öffentlichen Methoden ermöglichen es dem Nutzer, die Konfiguration des Handlers zu ändern, nachdem er erstellt worden ist. SetMinLevel aktualisiert die Membervariable m_min_level und ändert damit die Filterschwelle für nachfolgende Protokollmeldungen. SetFormat aktualisiert die Membervariable m_format und ändert damit die Vorlage, die für die Formatierung künftiger Protokollmeldungen verwendet wird.

Die Klasse FileLogHandler


Diese Header-Datei enthält die Klasse FileLogHandler, eine weitere konkrete Implementierung der ILogHandler-Schnittstelle. Dieser Handler ist für die dauerhafte Protokollierung konzipiert und schreibt formatierte Protokollmeldungen in Dateien. Im Vergleich zum Konsolenhandler bietet er erweiterte Funktionen, wie z. B. die automatische Rotation der Protokolldateien auf der Grundlage von Datum und Größe sowie die Verwaltung der Anzahl der aufbewahrten Protokolldateien.

#property strict

#include "ILogHandler.mqh"
#include "LogLevels.mqh"

//+------------------------------------------------------------------+
//| Class: FileLogHandler                                            |
//| Description: Implements ILogHandler to output log messages to    |
//|              files with rotation capabilities.                   |
//+------------------------------------------------------------------+
class FileLogHandler : public ILogHandler
  {
private:
   LogLevel          m_min_level;       // Minimum level to log
   string            m_format;          // Log message format string
   string            m_file_path;       // Base path for log files
   string            m_file_prefix;     // Prefix for log file names
   int               m_file_handle;     // Current file handle
   datetime          m_current_day;     // Current day for rotation
   int               m_max_size_kb;     // Maximum file size in KB before rotation
   int               m_max_files;       // Maximum number of log files to keep
   
   //--- Helper to format the log message
   string            FormatMessage(const datetime time, const LogLevel level, const string origin, const string message);
   //--- Helper to get string representation of LogLevel
   string            LogLevelToString(const LogLevel level);
   //--- Helper to create or rotate log file
   bool              EnsureFileOpen();
   //--- Helper to generate file name based on date
   string            GenerateFileName(const datetime time);
   //--- Helper to perform log rotation
   void              RotateLogFiles();
   //--- Helper to check if file size exceeds limit
   bool              IsFileSizeExceeded();
   // Add custom helper function to sort string arrays
   void              SortStringArray(string &arr[]);
   //--- New helper to clean file paths
   string CleanPath(const string path);

public:
   FileLogHandler(const string file_path="MQL5\\Logs", 
                  const string file_prefix="EA_Log", 
                  const LogLevel min_level=LOG_LEVEL_INFO, 
                  const string format="[{time}] {level}: {origin} - {message}",
                  const int max_size_kb=1024,
                  const int max_files=5);
   virtual ~FileLogHandler();
   //--- ILogHandler implementation
   virtual bool      Setup(const string settings="") override;
   virtual void      Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) override;
   virtual void      Shutdown() override;

   //--- Setters
   void SetFilePath(const string path)    { m_file_path = CleanPath(path); }
   void              SetMinLevel(const LogLevel level) { m_min_level = level; }
   void              SetFormat(const string format)    { m_format = format; }
   void              SetFilePrefix(const string prefix){ m_file_prefix = prefix; }
   void              SetMaxSizeKB(const int size)      { m_max_size_kb = size; }
   void              SetMaxFiles(const int count)      { m_max_files = count; }
  };

Die Klasse FileLogHandler wird von ILogHandler abgeleitet. Er verwaltet mehrere private Member-Variablen, um seinen Status und seine Konfiguration zu verwalten: m_min_level und m_format (ähnlich wie beim Konsolen-Handler), m_file_path (das Verzeichnis, in dem die Protokolle gespeichert werden), m_file_prefix (ein Basisname für Protokolldateien), m_file_handle (der Handle für die aktuell geöffnete Protokolldatei), m_current_day (wird für die tägliche Rotationslogik verwendet), m_max_size_kb (die Größenbeschränkung in Kilobyte für eine einzelne Protokolldatei) und m_max_files (die maximale Anzahl der zu speichernden Protokolldateien).

Sie deklariert auch mehrere private Hilfsmethoden für Formatierung, Dateiverwaltung und Rotation (FormatMessage, LogLevelToString, EnsureFileOpen, GenerateFileName, RotateLogFiles, IsFileSizeExceeded, SortStringArray, CleanPath). Zu den öffentlichen Methoden gehören der Konstruktor, der Destruktor, Schnittstellenimplementierungen und Setter für die Konfiguration.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
FileLogHandler::FileLogHandler(const string file_path, 
                               const string file_prefix, 
                               const LogLevel min_level, 
                               const string format,
                               const int max_size_kb,
                               const int max_files)
  {
   m_min_level = min_level;
   m_format = format;
   m_file_path = CleanPath(file_path);
   m_file_prefix = file_prefix;
   m_file_handle = INVALID_HANDLE;
   m_current_day = 0;
   m_max_size_kb = max_size_kb;
   m_max_files = max_files;
   
   // Create directory if it doesn't exist
   if(!FolderCreate(m_file_path))
     {
      if(GetLastError() != 0)
         Print("FileLogHandler: Failed to create directory: ", m_file_path, ", error: ", GetLastError());
     }
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
FileLogHandler::~FileLogHandler()
  {
   Shutdown();
  }
  1. Konstruktor (FileLogHandler::FileLogHandler):
    Der Konstruktor initialisiert den FileLogHandler. Sie nimmt Argumente für den Dateipfad, das Präfix, die minimale Protokollstufe, die Formatzeichenfolge, die maximale Dateigröße und die maximale Anzahl von Dateien entgegen und setzt die entsprechenden Mitgliedsvariablen. Es verwendet die CleanPath-Hilfe, um sicherzustellen, dass der Dateipfad korrekte Verzeichnistrennzeichen verwendet. Wichtig ist, dass auch versucht wird, das angegebene Protokollverzeichnis (m_file_path relativ zum Datenpfad des Terminals) mit FolderCreate zu erstellen, wenn es noch nicht existiert, um sicherzustellen, dass der Handler einen Ort zum Schreiben der Dateien hat.

  2. Destruktor (FileLogHandler::~FileLogHandler):
    Der Destruktor sorgt für eine ordnungsgemäße Bereinigung, indem er die Methode Shutdown aufruft. Dadurch wird gewährleistet, dass der derzeit geöffnete Handle der Protokolldatei geschlossen wird, wenn das FileLogHandler-Objekt zerstört wird, um Ressourcenlecks zu vermeiden.
//+------------------------------------------------------------------+
//| Setup                                                            |
//+------------------------------------------------------------------+
bool FileLogHandler::Setup(const string settings)
  {
   // Parse settings if provided
   // Format could be: "path=MQL5/Logs;prefix=MyEA;min_level=INFO;max_size=2048;max_files=10"
   if(settings != "")
     {
      string parts[];
      int count = StringSplit(settings, ';', parts);
      
      for(int i = 0; i < count; i++)
        {
         string key_value[];
         if(StringSplit(parts[i], '=', key_value) == 2)
           {
            string key = key_value[0];
            StringTrimLeft(key);
            StringTrimRight(key);
            string value = key_value[1];
            StringTrimLeft(value);
            StringTrimRight(value);
            
            if(key == "path")
               m_file_path = CleanPath(value);
            else if(key == "prefix")
               m_file_prefix = value;
            else if(key == "min_level")
              {
               if(value == "DEBUG")
                  m_min_level = LOG_LEVEL_DEBUG;
               else if(value == "INFO")
                  m_min_level = LOG_LEVEL_INFO;
               else if(value == "WARN")
                  m_min_level = LOG_LEVEL_WARN;
               else if(value == "ERROR")
                  m_min_level = LOG_LEVEL_ERROR;
               else if(value == "FATAL")
                  m_min_level = LOG_LEVEL_FATAL;
              }
            else if(key == "max_size")
               m_max_size_kb = (int)StringToInteger(value);
            else if(key == "max_files")
               m_max_files = (int)StringToInteger(value);
           }
        }
     }
   
   return true;
  }
//+------------------------------------------------------------------+
//| Log                                                              |
//+------------------------------------------------------------------+
void FileLogHandler::Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0)
  {
   // Check if the message level meets the minimum requirement
   if(level >= m_min_level && level < LOG_LEVEL_OFF)
     {
      // Ensure file is open and ready for writing
      if(EnsureFileOpen())
        {
         // Format the message
         string formatted_message = FormatMessage(time, level, origin, message);
         
         // Write to file
         FileWriteString(m_file_handle, formatted_message + "\r\n");
         
         // Flush to ensure data is written immediately
         FileFlush(m_file_handle);
         
         // Check if rotation is needed
         if(IsFileSizeExceeded())
           {
            FileClose(m_file_handle);
            m_file_handle = INVALID_HANDLE;
            RotateLogFiles();
            EnsureFileOpen();
           }
        }
     }
  }
//+------------------------------------------------------------------+
//| Shutdown                                                         |
//+------------------------------------------------------------------+
void FileLogHandler::Shutdown()
  {
   if(m_file_handle != INVALID_HANDLE)
     {
      FileClose(m_file_handle);
      m_file_handle = INVALID_HANDLE;
     }
  }
  1. Setup-Methode (FileLogHandler::Setup):
    Diese Methode implementiert die Funktion Setup der Schnittstelle. Es bietet eine alternative Möglichkeit, den Handler nach der Erstellung mit einem einzigen Einstellungsstring zu konfigurieren (z. B. „path=MQL5/Logs;prefix=MyEA;max_size=2048“). Sie analysiert diese Zeichenkette, teilt sie in Schlüssel-Wert-Paare auf und aktualisiert die entsprechenden Mitgliedsvariablen wie m_file_path, m_file_prefix, m_min_level, m_max_size_kb, und m_max_files. Dies ermöglicht bei Bedarf das Laden der Konfiguration aus externen Quellen. Sie gibt nach dem Parsen true zurück.

  2. Log-Methode (FileLogHandler::Log):
    Diese Methode behandelt die Hauptlogik der Dateiprotokollierung. Zunächst wird geprüft, ob der Pegel der Nachricht die Anforderung m_min_level erfüllt. Ist dies der Fall, wird EnsureFileOpen aufgerufen, um sicherzustellen, dass eine gültige Protokolldatei geöffnet ist (ggf. mit täglicher Rotation). Wenn die Datei erfolgreich geöffnet wurde, wird die Nachricht mit FormatMessage formatiert und die formatierte Zeichenfolge gefolgt von einem Zeilenumbruch (\r\n) mit FileWriteString in die Datei geschrieben. Anschließend wird FileFlush aufgerufen, um sicherzustellen, dass die Daten sofort auf die Festplatte geschrieben werden, was für die Erfassung von Protokollen wichtig ist, selbst wenn die Anwendung abstürzt. Schließlich wird mit IsFileSizeExceeded geprüft, ob die aktuelle Dateigröße den Grenzwert m_max_size_kb überschreitet. Wird das Limit überschritten, schließt es die aktuelle Datei, löst RotateLogFiles aus, um alte Dateien zu verwalten, und öffnet eine neue Datei mit EnsureFileOpen.

  3. Shutdown-Methode (FileLogHandler::Shutdown):
    Diese Methode implementiert die Shutdown-Anforderung der Schnittstelle. Seine Hauptaufgabe ist es, den derzeit geöffneten Log-Datei-Handle (m_file_handle) mit FileClose zu schließen, wenn er gültig ist (!= INVALID_HANDLE). Dadurch wird sichergestellt, dass die Datei ordnungsgemäß geschlossen wird und alle gepufferten Daten geschrieben werden, wenn der Logger heruntergefahren wird.
//+------------------------------------------------------------------+
//| FormatMessage                                                    |
//+------------------------------------------------------------------+
string FileLogHandler::FormatMessage(const datetime time, const LogLevel level, const string origin, const string message)
  {
   string formatted_message = m_format;

   // Replace placeholders
   StringReplace(formatted_message, "{time}", TimeToString(time, TIME_DATE | TIME_SECONDS));
   StringReplace(formatted_message, "{level}", LogLevelToString(level));
   StringReplace(formatted_message, "{origin}", origin);
   StringReplace(formatted_message, "{message}", message);

   return formatted_message;
  }
//+------------------------------------------------------------------+
//| LogLevelToString                                                 |
//+------------------------------------------------------------------+
string FileLogHandler::LogLevelToString(const LogLevel level)
  {
   switch(level)
     {
      case LOG_LEVEL_DEBUG: return "DEBUG";
      case LOG_LEVEL_INFO:  return "INFO";
      case LOG_LEVEL_WARN:  return "WARN";
      case LOG_LEVEL_ERROR: return "ERROR";
      case LOG_LEVEL_FATAL: return "FATAL";
      default:              return "UNKNOWN";
     }
  }

Hilfsmethoden (FormatMessage, LogLevelToString): Diese privaten Helfer funktionieren identisch mit ihren Gegenstücken im ConsoleLogHandler, indem sie Nachrichtenformatierung basierend auf der Zeichenkette m_format bereitstellen und LogLevel-Enums in lesbare Zeichenketten konvertieren.

//+------------------------------------------------------------------+
//| EnsureFileOpen                                                   |
//+------------------------------------------------------------------+
bool FileLogHandler::EnsureFileOpen()
  {
   datetime current_time = TimeCurrent();
   MqlDateTime time_struct;
   TimeToStruct(current_time, time_struct);
   
   // Create a datetime that represents just the current day (time set to 00:00:00)
   MqlDateTime day_struct;
   day_struct.year = time_struct.year;
   day_struct.mon = time_struct.mon;
   day_struct.day = time_struct.day;
   day_struct.hour = 0;
   day_struct.min = 0;
   day_struct.sec = 0;
   datetime current_day = StructToTime(day_struct);
   
   // Check if we need to open a new file (either first time or new day)
   if(m_file_handle == INVALID_HANDLE || m_current_day != current_day)
     {
      // Close existing file if open
      if(m_file_handle != INVALID_HANDLE)
        {
         FileClose(m_file_handle);
         m_file_handle = INVALID_HANDLE;
        }
      
      // Update current day
      m_current_day = current_day;
      
      // Generate new file name
      string file_name = GenerateFileName(current_time);
      
      // Open file for writing (append if exists)
      m_file_handle = FileOpen(file_name, FILE_WRITE | FILE_READ | FILE_TXT);
      
      if(m_file_handle == INVALID_HANDLE)
        {
         Print("FileLogHandler: Failed to open log file: ", file_name, ", error: ", GetLastError());
         return false;
        }
      
      // Move to end of file for appending
      FileSeek(m_file_handle, 0, SEEK_END);
     }
   
   return true;
  }
//+------------------------------------------------------------------+
//| GenerateFileName                                                 |
//+------------------------------------------------------------------+
string FileLogHandler::GenerateFileName(const datetime time)
  {
   MqlDateTime time_struct;
   TimeToStruct(time, time_struct);
   
   string date_str = StringFormat("%04d%02d%02d", 
                                 time_struct.year, 
                                 time_struct.mon, 
                                 time_struct.day);
   
   return m_file_path + "\\" + m_file_prefix + "_" + date_str + ".log";
  }
//+------------------------------------------------------------------+
//| IsFileSizeExceeded                                               |
//+------------------------------------------------------------------+
bool FileLogHandler::IsFileSizeExceeded()
  {
   if(m_file_handle != INVALID_HANDLE)
     {
      // Get current position (file size)
      ulong size = FileSize(m_file_handle);
      
      // Check if size exceeds limit (convert KB to bytes)
      return (size > (ulong)m_max_size_kb * 1024);
     }
   
   return false;
  }
  1. Hilfsmethode (EnsureFileOpen):
    Diese wichtige Hilfsmethode verwaltet das Öffnen und die tägliche Rotation der Protokolldateien. Sie vergleicht das aktuelle Datum (abgeleitet von TimeCurrent()) mit dem gespeicherten m_current_day. Wenn das Dateihandle ungültig ist oder sich der Tag geändert hat, wird ein eventuell vorhandenes Handle geschlossen, m_current_day aktualisiert, mit GenerateFileName ein neuer Dateiname erzeugt (der das Datum enthält) und diese neue Datei im Schreib-/Lesemodus (FILE_WRITE | FILE_READ | FILE_TXT) geöffnet. Er verwendet FileSeek, um an das Ende der Datei zu gelangen und sicherzustellen, dass neue Protokolle angehängt werden. Sie gibt true zurück, wenn eine Datei erfolgreich geöffnet wurde oder bereits geöffnet ist, und false bei einem Fehler.

  2. Hilfsmethode (GenerateFileName):
    Dieses Dienstprogramm generiert den vollständigen Pfad für eine Protokolldatei auf der Grundlage der aktuellen Zeit. Es formatiert den Datumsteil der Zeit in eine YYYYMMDD-Zeichenkette und kombiniert sie mit dem konfigurierten m_file_path, m_file_prefix und der .log-Erweiterung.

  3. Hilfsmethode (IsFileSizeExceeded):
    Diese Funktion prüft, ob die Größe der aktuell geöffneten Protokolldatei (m_file_handle) die konfigurierte Grenze m_max_size_kb überschritten hat. Es ruft die Dateigröße mit FileSize ab und vergleicht sie mit dem Limit (umgerechnet in Bytes). Sie gibt true zurück, wenn die Größe überschritten wird, andernfalls false.
//+------------------------------------------------------------------+
//| RotateLogFiles                                                   |
//+------------------------------------------------------------------+
void FileLogHandler::RotateLogFiles()
  {
   // Get list of log files
   string terminal_path = TerminalInfoString(TERMINAL_DATA_PATH);
   string full_path = terminal_path + "\\" + m_file_path;
   string file_pattern = m_file_prefix + "_*.log";
   
   string files[];
   int file_count = 0;
   
   long search_handle = FileFindFirst(full_path + "\\" + file_pattern, files[file_count]);
   if(search_handle != INVALID_HANDLE)
     {
      file_count++;
      
      // Find all matching files
      while(FileFindNext(search_handle, files[file_count]))
        {
         file_count++;
         ArrayResize(files, file_count + 1);
        }
      
      // Close search handle
      FileFindClose(search_handle);
     }
   
   // Resize array to actual number of found files before sorting
   ArrayResize(files, file_count);
   // Sort the string array using the custom sorter
   SortStringArray(files);
   
   // Delete oldest files if we have too many
   int files_to_delete = file_count - m_max_files + 1; // +1 for the new file we'll create
   
   if(files_to_delete > 0)
     {
      for(int i = 0; i < files_to_delete; i++)
        {
         if(!FileDelete(m_file_path + "\\" + files[i]))
            Print("FileLogHandler: Failed to delete old log file: ", files[i], ", error: ", GetLastError());
        }
     }
  }
//+------------------------------------------------------------------+
//| SortStringArray                                                  |
//+------------------------------------------------------------------+
void FileLogHandler::SortStringArray(string &arr[])
  {
   int n = ArraySize(arr);
   for(int i = 0; i < n - 1; i++)
     {
      for(int j = i + 1; j < n; j++)
        {
         if(arr[i] > arr[j])
           {
            string temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
           }
        }
     }
  }
//+------------------------------------------------------------------+
//| New implementation: CleanPath                                    |
//+------------------------------------------------------------------+
string FileLogHandler::CleanPath(const string path)
  {
   string result = path;
   // Replace all "/" with "\\"
   StringReplace(result, "/", "\\");
   return result;
  }
//+------------------------------------------------------------------+
  1. Hilfsmethode (RotateLogFiles):
    Mit dieser Methode wird die Richtlinie zur Aufbewahrung von Protokolldateien umgesetzt. Sie findet alle Dateien im Protokollverzeichnis, die dem Muster (m_file_prefix_*.log) entsprechen, mit FileFindFirst und FileFindNext. Es speichert die Dateinamen in einem String-Array, sortiert sie alphabetisch (was in der Regel aufgrund des Datumsformats im Dateinamen einer chronologischen Reihenfolge entspricht) und verwendet dazu die SortStringArray-Hilfe. Anschließend wird berechnet, wie viele Dateien den Grenzwert m_max_files überschreiten, und die ältesten (die in der sortierten Liste am frühesten auftauchen) werden mit FileDelete gelöscht.

  2. Hilfsmethode (SortStringArray):
    Dies ist eine einfache Bubble-Sort-Implementierung speziell für das Sortieren des Arrays von Log-Dateinamen, die in RotateLogFiles erhalten werden. Es wird verwendet, weil der Standardbibliothek von MQL5 eine integrierte Sortierfunktion für String-Arrays fehlt.

  3. Hilfsmethode (CleanPath):
    Dieses Dienstprogramm stellt sicher, dass Verzeichnispfade das von den MQL5-Dateifunktionen erwartete Backslash-Trennzeichen (\) verwenden und ersetzt alle Schrägstriche (/), die in der Eingabepfadzeichenfolge gefunden werden.

  4. Setter-Methoden (SetFilePath, SetMinLevel, usw.):
    Diese öffentlichen Methoden ermöglichen die Änderung der Konfigurationsparameter des Handlers (Pfad, Präfix, Ebene, Format, Größenbeschränkungen) nach seiner anfänglichen Erstellung und bieten somit Flexibilität.

CLogger

Diese Header-Datei definiert die Klasse CLogger, die als zentraler Orchestrator für das gesamte Logging-Framework fungiert. Er ist nach dem Singleton-Design-Muster implementiert, wodurch sichergestellt wird, dass in der gesamten Anwendung nur eine Instanz des Loggers existiert. Diese einzelne Instanz verwaltet alle registrierten Log-Handler und stellt die primäre Schnittstelle für den Code des Anwenders zur Übermittlung von Log-Meldungen dar.
#property strict

#include "LogLevels.mqh"
#include "ILogHandler.mqh"

//+------------------------------------------------------------------+
//| Class: CLogger                                                   |
//| Description: Singleton class for managing and dispatching log    |
//|              messages to registered handlers.                    |
//+------------------------------------------------------------------+
class CLogger
  {
private:
   static CLogger   *s_instance;
   ILogHandler*     m_handlers[];  
   LogLevel          m_global_min_level;
   long              m_expert_magic;
   string            m_expert_name;

   //--- Private constructor for Singleton
                     CLogger();
                    ~CLogger();

public:
   //--- Get the singleton instance
   static CLogger*   Instance();
   //--- Cleanup the singleton instance
   static void       Release();

   //--- Handler management
   bool              AddHandler(ILogHandler *handler);
   void              ClearHandlers();

   //--- Configuration
   void              SetGlobalMinLevel(const LogLevel level);
   void              SetExpertInfo(const long magic, const string name);

   //--- Logging methods
   void              Log(const LogLevel level, const string origin, const string message);
   void              Debug(const string origin, const string message);
   void              Info(const string origin, const string message);
   void              Warn(const string origin, const string message);
   void              Error(const string origin, const string message);
   void              Fatal(const string origin, const string message);
   
   //--- Formatted logging methods
   void              LogFormat(const LogLevel level, const string origin, const string formatted_message);
   void              DebugFormat(const string origin, const string formatted_message);
   void              InfoFormat(const string origin, const string formatted_message);
   void              WarnFormat(const string origin, const string formatted_message);
   void              ErrorFormat(const string origin, const string formatted_message);
   void              FatalFormat(const string origin, const string formatted_message);
  };

Die Klasse CLogger enthält mehrere private Mitglieder. s_instance ist ein statischer Zeiger, der die einzelne Instanz der Klasse selbst enthält. m_handlers ist ein dynamisches Array von ILogHandler-Zeigern, das Verweise auf alle aktiven Log-Handler (wie Konsolen- oder Datei-Handler) speichert. m_global_min_level legt einen globalen Filterschwellenwert fest; Nachrichten unterhalb dieses Schwellenwerts werden ignoriert, noch bevor sie an die einzelnen Handler gesendet werden. m_expert_magic und m_expert_name speichern optionale Informationen über den Expert Advisor, der den Logger verwendet, die zum besseren Kontext in die Log-Meldungen aufgenommen werden können.

Der Konstruktor und der Destruktor sind privat, um das Singleton-Muster durchzusetzen. Öffentliche Methoden ermöglichen den Zugriff auf die Instanz, die Verwaltung der Handler, die Konfiguration und verschiedene Protokollierungsfunktionen.

//+------------------------------------------------------------------+
//| Static instance initialization                                   |
//+------------------------------------------------------------------+
CLogger *CLogger::s_instance = NULL;
//+------------------------------------------------------------------+
//| Get Singleton Instance                                           |
//+------------------------------------------------------------------+
CLogger* CLogger::Instance()
  {
   if(s_instance == NULL)
     {
      s_instance = new CLogger();
     }
   return s_instance;
  }
//+------------------------------------------------------------------+
//| Release Singleton Instance                                       |
//+------------------------------------------------------------------+
void CLogger::Release()
  {
   if(s_instance != NULL)
     {
      delete s_instance;
      s_instance = NULL;
     }
  }
//+------------------------------------------------------------------+
//| Constructor (Private)                                            |
//+------------------------------------------------------------------+
CLogger::CLogger()
  {
   m_global_min_level = LOG_LEVEL_DEBUG;
   m_expert_magic = 0;
   m_expert_name = "";
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogger::~CLogger()
  {
   ClearHandlers();
  }
  1. Singleton-Implementierung (Instanz, Freigabe, privater Konstrukteur):
    Das Singleton-Muster wird durch die statische Methode Instance() implementiert, die beim ersten Aufruf das CLogger-Objekt erzeugt und bei nachfolgenden Aufrufen die gleiche Instanz zurückgibt. Der Konstruktor (CLogger::CLogger) ist privat, um eine direkte Instanziierung von außerhalb der Klasse zu verhindern; er initialisiert Standardwerte für das globale Mindestniveau und die Experteninformationen. Die statische Release()-Methode wird bereitgestellt, um die Singleton-Instanz explizit zu löschen und die Ressourcen zu bereinigen; sie wird in der Regel beim Herunterfahren der Anwendung aufgerufen.

  2. Destruktor (CLogger::~CLogger):
    Der Destruktor wird aufgerufen, wenn die Singleton-Instanz mit der Methode Release() gelöscht wird. Seine Hauptaufgabe besteht darin, die verwalteten Handler zu bereinigen, indem er die ClearHandlers-Methode aufruft und sicherstellt, dass die Shutdown-Methode jedes Handlers aufgerufen wird und die Handler-Objekte selbst gelöscht werden.
//+------------------------------------------------------------------+
//| AddHandler                                                       |
//+------------------------------------------------------------------+
bool CLogger::AddHandler(ILogHandler *handler)
  {
   if(CheckPointer(handler) == POINTER_INVALID)
     {
      Print("CLogger::AddHandler - Error: Invalid handler pointer.");
      return false;
     }
   int size = ArraySize(m_handlers);
   ArrayResize(m_handlers, size + 1);
   m_handlers[size] = handler;
   return true;
  }
//+------------------------------------------------------------------+
//| ClearHandlers                                                    |
//+------------------------------------------------------------------+
void CLogger::ClearHandlers()
  {
   for(int i = 0; i < ArraySize(m_handlers); i++)
     {
      ILogHandler *handler = m_handlers[i];
      if(CheckPointer(handler) != POINTER_INVALID)
        {
         handler.Shutdown();
         delete handler;
        }
     }
   ArrayResize(m_handlers, 0);
  }
//+------------------------------------------------------------------+
//| SetGlobalMinLevel                                                |
//+------------------------------------------------------------------+
void CLogger::SetGlobalMinLevel(const LogLevel level)
  {
   m_global_min_level = level;
  }
//+------------------------------------------------------------------+
//| SetExpertInfo                                                    |
//+------------------------------------------------------------------+
void CLogger::SetExpertInfo(const long magic, const string name)
  {
   m_expert_magic = magic;
   m_expert_name = name;
  }
  1. Handler-Verwaltung (AddHandler, ClearHandlers):
    Die Methode AddHandler ermöglicht das Hinzufügen eines neuen Log-Handlers (jedes Objekt, das ILogHandler implementiert) zur internen Liste des Loggers (m_handlers). Sie prüft, ob ein gültiger Zeiger vorhanden ist, ändert die Größe des dynamischen Arrays und fügt den Handler hinzu. Die Methode ClearHandlers durchläuft das Array m_handlers, ruft die Methode Shutdown für jeden gültigen Handler auf, löscht das Handler-Objekt selbst (unter der Annahme, dass der Logger den Besitz übernimmt) und leert schließlich das Array. Dies ist entscheidend für die ordnungsgemäße Bereinigung der Ressourcen.

  2. Konfiguration (SetGlobalMinLevel, SetExpertInfo):
    Mit diesen Methoden kann das Verhalten des Loggers angepasst werden. SetGlobalMinLevel passt den globalen Filterschwellenwert (m_global_min_level) an und wirkt sich auf alle Nachrichten aus, bevor sie die Handler erreichen. SetExpertInfo ermöglicht das Festlegen der magischen Nummer und des Namens des EA, die dann zur besseren Identifizierung automatisch von den Handlern in die Protokollmeldungen aufgenommen werden können, insbesondere wenn mehrere EAs gleichzeitig protokollieren.
//+------------------------------------------------------------------+
//| Log                                                              |
//+------------------------------------------------------------------+
void CLogger::Log(const LogLevel level, const string origin, const string message)
  {
   // Check global level first
   if(level < m_global_min_level || level >= LOG_LEVEL_OFF)
      return;

   datetime current_time = TimeCurrent();
   string effective_origin = origin;
   if(m_expert_name != "")
      effective_origin = m_expert_name + "::" + origin;
      
   // Dispatch to all registered handlers
   for(int i = 0; i < ArraySize(m_handlers); i++)
     {
      ILogHandler *handler = m_handlers[i];
      if(CheckPointer(handler) != POINTER_INVALID)
        {
         handler.Log(current_time, level, effective_origin, message, m_expert_magic);
        }
     }
  }
//+------------------------------------------------------------------+
//| Convenience Logging Methods                                      |
//+------------------------------------------------------------------+
void CLogger::Debug(const string origin, const string message) { Log(LOG_LEVEL_DEBUG, origin, message); }
void CLogger::Info(const string origin, const string message)  { Log(LOG_LEVEL_INFO, origin, message); }
void CLogger::Warn(const string origin, const string message)  { Log(LOG_LEVEL_WARN, origin, message); }
void CLogger::Error(const string origin, const string message) { Log(LOG_LEVEL_ERROR, origin, message); }
void CLogger::Fatal(const string origin, const string message) { Log(LOG_LEVEL_FATAL, origin, message); }

//+------------------------------------------------------------------+
//| LogFormat                                                        |
//+------------------------------------------------------------------+
void CLogger::LogFormat(const LogLevel level, const string origin, const string formatted_message)
  {
   // Check global level first
   if(level < m_global_min_level || level >= LOG_LEVEL_OFF)
      return;
   Log(level, origin, formatted_message);
  }
//+------------------------------------------------------------------+
//| Convenience Formatted Logging Methods                            |
//+------------------------------------------------------------------+
void CLogger::DebugFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_DEBUG, origin, formatted_message); }
void CLogger::InfoFormat(const string origin, const string formatted_message)  { LogFormat(LOG_LEVEL_INFO, origin, formatted_message); }
void CLogger::WarnFormat(const string origin, const string formatted_message)  { LogFormat(LOG_LEVEL_WARN, origin, formatted_message); }
void CLogger::ErrorFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_ERROR, origin, formatted_message); }
void CLogger::FatalFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_FATAL, origin, formatted_message); }
//+------------------------------------------------------------------+
  1. Kernmethode der Protokollierung (Log):
    Dies ist die zentrale Methode, die Protokollanforderungen empfängt. Zunächst wird geprüft, ob der Pegel der Nachricht dem m_global_min_level entspricht. Wenn sie erfolgreich ist, wird die aktuelle Zeit abgerufen und eine effective_origin-Zeichenkette konstruiert, der möglicherweise der konfigurierte m_expert_name vorangestellt wird. Anschließend durchläuft es das Array m_handlers und ruft die Log-Methode jedes gültigen Handlers auf, wobei es den Zeitstempel, die Ebene, den Ursprung, die Nachricht und die magische Zahl des Experten weitergibt. Dadurch wird die Protokollnachricht tatsächlich an alle aktiven Ausgabeziele gesendet.

  2. Komfortable Protokollierungsmethoden (Debug, Info, Warn, Error, Fatal):
    Diese öffentlichen Methoden bieten eine einfachere Schnittstelle für die Protokollierung von Meldungen mit bestimmten Schweregraden. Jede Methode (z. B. Debug, Info) ruft einfach die Hauptlog-Methode mit dem entsprechenden LogLevel-Enum-Wert (LOG_LEVEL_DEBUG, LOG_LEVEL_INFO usw.) auf, wodurch die Menge an Code, die in der Anwendung des Nutzers für die Protokollierung einer Nachricht erforderlich ist, reduziert wird.

  3. Formatierte Protokollierungsmethoden (LogFormat, DebugFormat, etc.):
    Diese Methoden bieten eine alternative Möglichkeit, bereits formatierte Nachrichten zu protokollieren. LogFormat nimmt eine vorformatierte Zeichenkette und ruft die Hauptmethode Log auf. Die Komfortmethoden wie DebugFormat, InfoFormat usw. rufen einfach LogFormat mit dem entsprechenden Schweregrad auf. Diese sind nützlich, wenn die Logik der Nachrichtenformatierung komplex ist und vor dem Aufruf des Loggers an anderer Stelle behandelt wird.

Nachdem die Implementierung von CLogger abgeschlossen ist, ist es nun an der Zeit, das System in Aktion zu erleben.


Verwendung des Logging Frameworks

Dieser EA dient als praktische Demonstration der Integration und Nutzung des nutzerdefinierten MQL5-Protokollierungsrahmens (bestehend aus CLogger, ILogHandler, ConsoleLogHandler und FileLogHandler). Es zeigt die Einrichtung, Konfiguration, Verwendung im Betrieb und Bereinigung der Protokollierungskomponenten innerhalb einer Standard-EA-Struktur. 

Im ersten Abschnitt von LoggingExampleEA.mq5 werden die Standardeigenschaften von Expert Advisor festgelegt und die erforderlichen Komponenten des nutzerdefinierten Logging-Frameworks hinzugefügt.

Nach den Eigenschaften sind die #include-Anweisungen entscheidend für die Integration der Protokollierungsfunktionen. CLogger.mqh enthält die Definition der Hauptklasse des Loggers. ConsoleLogHandler.mqh enthält die Klasse für die Protokollierung in der MetaTrader-Konsole (Registerkarte Experten). FileLogHandler.mqh enthält die Klasse, die für die Protokollierung in Dateien zuständig ist. Diese Includes machen die in diesen Header-Dateien definierten Klassen und Funktionen für die Verwendung in diesem EA verfügbar.

Eingabeparameter (input):

// Input parameters
input int      MagicNumber = 654321;         // EA Magic Number
input double   LotSize     = 0.01;           // Fixed lot size
input int      StopLossPips = 50;            // Stop Loss in pips
input int      TakeProfitPips = 100;         // Take Profit in pips
input LogLevel ConsoleLogLevel = LOG_LEVEL_INFO; // Minimum level for console output
input LogLevel FileLogLevel = LOG_LEVEL_DEBUG;   // Minimum level for file output

In diesem Abschnitt werden die externen Parameter definiert, die Nutzer konfigurieren können, wenn sie den Expert Advisor an einen Chart anhängen. Diese Eingaben ermöglichen die Anpassung des Handelsverhaltens des EAs und vor allem seiner Protokollierungseinstellungen.

  • input int MagicNumber = 654321; : Dies ist ein Standard-EA-Parameter, der verwendet wird, um Aufträge zu identifizieren, die von dieser speziellen Instanz des EA erteilt wurden. Es hilft, seine Trades von denen anderer EAs oder manueller Trades zu unterscheiden.
  • input double LotSize = 0.01; : Legt das feste Handelsvolumen (Lotgröße) fest, das für die vom EA erteilten Aufträge verwendet wird.
  • input int StopLossPips = 50; : Legt den Stop-Loss-Abstand in Pips für Aufträge fest.
  • input int TakeProfitPips = 100; : Legt den Take-Profit-Abstand in Pips für Aufträge fest.
Entscheidend ist, dass die folgenden Eingaben das Verhalten des nutzerdefinierten Protokollierungsrahmens direkt steuern:
  • input LogLevel ConsoleLogLevel = LOG_LEVEL_INFO; : Mit diesem Parameter kann der Nutzer den Mindestschweregrad für Meldungen auswählen, die in der Registerkarte MetaTrader Experts (Konsole) angezeigt werden sollen. Sie verwendet den Enumerationstyp LogLevel, der in LogLevels.mqh definiert ist. Standardmäßig ist er auf LOG_LEVEL_INFO eingestellt, was bedeutet, dass INFO-, WARN-, ERROR- und FATAL-Meldungen auf der Konsole angezeigt werden, während DEBUG-Meldungen unterdrückt werden.
  • input LogLevel FileLogLevel = LOG_LEVEL_DEBUG; : In ähnlicher Weise wird mit dieser Eingabe der Mindestschweregrad für die in die Protokolldatei geschriebenen Meldungen festgelegt. Sie verwendet auch die Enumeration LogLevel. Die Standardeinstellung ist LOG_LEVEL_DEBUG, was bedeutet, dass alle Meldungen, einschließlich detaillierter Debug-Informationen, in der Protokolldatei gespeichert werden. Dies ermöglicht eine weniger ausführliche Konsolenausgabe während des normalen Betriebs, während detaillierte Protokolle für eine spätere Analyse oder Fehlerbehebung aufbewahrt werden.

Diese protokollierungsspezifischen Eingaben zeigen, wie einfach das Framework extern konfiguriert werden kann, sodass die Nutzer die Protokollierungsintensität anpassen können, ohne den Code des EA zu ändern.

// Global logger pointer (optional, can use CLogger::Instance() directly)
CLogger *g_logger = NULL;

Der EA deklariert eine einzige globale Variable:  CLogger *g_logger = NULL; : Diese Zeile deklariert einen Zeiger namens g_logger, der auf ein Objekt der Klasse CLogger zeigen kann. Sie wird auf NULL initialisiert, d.h. sie verweist zunächst auf kein gültiges Objekt. Dieser globale Zeiger soll die einzelne Instanz des CLogger enthalten, die über das Singleton-Muster (CLogger::Instance()) erhalten wird.

Es ist zwar möglich, die statische Methode CLogger::Instance() direkt dort zu verwenden, wo eine Protokollierung erforderlich ist, aber das Speichern der Instanz in dieser globalen Variablen nach dem Abrufen in OnInit() bietet eine bequeme Möglichkeit, von verschiedenen Funktionen (OnTick, OnDeinit, OnChartEvent) auf das Logger-Objekt zuzugreifen, ohne CLogger::Instance() wiederholt aufrufen zu müssen. Er dient als zwischengespeicherter Zeiger auf den Singleton-Logger.

OnInit():

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Get the logger instance
   g_logger = CLogger::Instance();
   if(CheckPointer(g_logger) == POINTER_INVALID)
     {
      Print("Critical Error: Failed to get Logger instance!");
      return(INIT_FAILED);
     }

//--- Set EA information for context in logs
   g_logger.SetExpertInfo(MagicNumber, MQL5InfoString(MQL5_PROGRAM_NAME));

//--- Configure Handlers ---
   // 1. Console Handler
   ConsoleLogHandler *console_handler = new ConsoleLogHandler(ConsoleLogLevel);
   if(CheckPointer(console_handler) != POINTER_INVALID)
     {
      // Optionally customize format
      // console_handler.SetFormat("[{level}] {message}"); 
      if(!g_logger.AddHandler(console_handler))
        {
         Print("Warning: Failed to add ConsoleLogHandler.");
         delete console_handler; // Clean up if not added
        }
     }
   else
     {
      Print("Warning: Failed to create ConsoleLogHandler.");
     }

   // 2. File Handler
   string log_prefix = MQL5InfoString(MQL5_PROGRAM_NAME) + "_" + IntegerToString(MagicNumber);
   FileLogHandler *file_handler = new FileLogHandler("MQL5/Logs/EA_Logs", // Directory relative to MQL5/Files
                                                   log_prefix,          // File name prefix
                                                   FileLogLevel,        // Minimum level to log to file
                                                   "[{time}] {level} ({origin}): {message}", // Format
                                                   2048, // Max file size in KB (e.g., 2MB)
                                                   10);  // Max number of log files to keep
   if(CheckPointer(file_handler) != POINTER_INVALID)
     {
      if(!g_logger.AddHandler(file_handler))
        {
         Print("Warning: Failed to add FileLogHandler.");
         delete file_handler; // Clean up if not added
        }
     }
   else
     {
      Print("Warning: Failed to create FileLogHandler.");
     }

//--- Log initialization message
   g_logger.Info(__FUNCTION__, "Expert Advisor initialized successfully.");
   g_logger.Debug(__FUNCTION__, StringFormat("Settings: Lots=%.2f, SL=%d, TP=%d, ConsoleLevel=%s, FileLevel=%s",
                                           LotSize, StopLossPips, TakeProfitPips, 
                                           EnumToString(ConsoleLogLevel),
                                           EnumToString(FileLogLevel)));

//--- succeed
   return(INIT_SUCCEEDED);
  }

In diesem Beispiel ist OnInit() entscheidend für die Einrichtung und Konfiguration des nutzerdefinierten Logging-Frameworks. Der erste Schritt bei OnInit ist das Abrufen der Singleton-Instanz des Loggers:

g_logger = CLogger::Instance();

Diese statische Methode stellt sicher, dass nur ein CLogger-Objekt existiert. Der zurückgegebene Zeiger wird in der globalen Variable g_logger gespeichert, um den späteren Zugriff zu erleichtern. Es folgt eine grundlegende Fehlerprüfung mit CheckPointer, um sicherzustellen, dass die Instanz erfolgreich abgerufen wurde. Wenn dies nicht der Fall ist, wird ein kritischer Fehler in das Standardprotokoll geschrieben und die Initialisierung schlägt fehl (INIT_FAILED).

g_logger.SetExpertInfo(MagicNumber, MQL5InfoString(MQL5_PROGRAM_NAME));

Diese Zeile konfiguriert den Logger mit Kontext über den EA, der ihn verwendet. Er übergibt die MagicNumber (aus den Eingabeparametern) und den Namen des EA (abgerufen mit MQL5InfoString(MQL5_PROGRAM_NAME)). Diese Informationen können von den Handlern automatisch in die Protokollnachrichten aufgenommen werden (abhängig von ihrem Formatstring), was die Identifizierung von Protokollen von bestimmten EAs erleichtert, insbesondere wenn mehrere EAs laufen.

Ein ConsoleLogHandler wird dynamisch mit „new“ erstellt:

ConsoleLogHandler *console_handler = new ConsoleLogHandler(ConsoleLogLevel);
Sie wird direkt im Konstruktor mit der minimalen Protokollstufe konfiguriert, die durch den Eingabeparameter ConsoleLogLevel festgelegt wird. Der Code enthält ein auskommentiertes Beispiel (console_handler.SetFormat(“[{level}] {message}“);), das zeigt, wie das Ausgabeformat bei Bedarf nach der Erstellung angepasst werden kann. Der Handler wird dann dem Hauptlogger hinzugefügt:
if(!g_logger.AddHandler(console_handler))

Wenn das Hinzufügen des Handlers fehlschlägt (false), wird eine Warnung ausgegeben, und das erstellte Handler-Objekt wird mit delete gelöscht, um Speicherlecks zu vermeiden. Auch bei der erstmaligen Erstellung (neu) des Handlers ist eine Fehlerprüfung vorgesehen.

In ähnlicher Weise wird ein FileLogHandler erstellt:
   // 2. File Handler
   string log_prefix = MQL5InfoString(MQL5_PROGRAM_NAME) + "_" + IntegerToString(MagicNumber);
   FileLogHandler *file_handler = new FileLogHandler("MQL5/Logs/EA_Logs", // Directory relative to MQL5/Files
                                                   log_prefix,          // File name prefix
                                                   FileLogLevel,        // Minimum level to log to file
                                                   "[{time}] {level} ({origin}): {message}", // Format
                                                   2048, // Max file size in KB (e.g., 2MB)
                                                   10);  // Max number of log files to keep

Zur eindeutigen Identifizierung wird ein Präfix für die Protokolldatei aus dem EA-Namen und der magischen Zahl gebildet. Der FileLogHandler-Konstruktor wird mit mehreren Argumenten aufgerufen: dem Verzeichnispfad („MQL5/Logs/EA_Logs“, relativ zum Verzeichnis MQL5/Files des Terminals), dem generierten Präfix, dem minimalen Level aus der FileLogLevel-Eingabe, einem nutzerdefinierten Format-String, der maximalen Dateigröße in KB (2048 KB = 2MB) und der maximalen Anzahl der zu speichernden Log-Dateien (10). Wie der Konsolen-Handler wird er dem Logger mit g_logger.AddHandler() hinzugefügt, mit ähnlicher Fehlerbehandlung und Bereinigung (Löschen), wenn die Erstellung oder Hinzufügung fehlschlägt.

Nach dem Einrichten der Handler protokolliert der EA Meldungen zur Bestätigung der Initialisierung:
g_logger.Info(__FUNCTION__, \"Expert Advisor initialized successfully.\");
g_logger.Debug(__FUNCTION__, StringFormat(\"Settings: ...\"));

Eine Meldung auf Info-Ebene bestätigt den Erfolg. Eine Meldung der Debug-Ebene protokolliert die wichtigsten Eingabeparameter im StringFormat. Als Ursprungszeichenfolge wird __FUNCTION__ verwendet, wodurch automatisch der Name der aktuellen Funktion (OnInit) angegeben wird. Diese Nachrichten werden von den hinzugefügten Handlern auf der Grundlage ihrer konfigurierten Mindestwerte verarbeitet.

Wenn schließlich alle Initialisierungen erfolgreich waren, gibt die Funktion INIT_SUCCEEDED zurück und signalisiert dem Terminal, dass der EA bereit ist, mit der Verarbeitung von Ticks zu beginnen. Wenn ein kritischer Fehler auftritt (z. B. wenn die Logger-Instanz nicht abgerufen werden kann), wird INIT_FAILED zurückgegeben.

OnDeinit():

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Log deinitialization
   if(CheckPointer(g_logger) != POINTER_INVALID)
     {
      string reason_str = "Unknown reason";
      switch(reason)
        {
         case REASON_ACCOUNT: reason_str = "Account change"; break;
         case REASON_CHARTCHANGE: reason_str = "Chart symbol or period change"; break;
         case REASON_CHARTCLOSE: reason_str = "Chart closed"; break;
         case REASON_PARAMETERS: reason_str = "Input parameters changed"; break;
         case REASON_RECOMPILE: reason_str = "Recompiled"; break;
         case REASON_REMOVE: reason_str = "EA removed from chart"; break;
         case REASON_TEMPLATE: reason_str = "Template applied"; break;
         case REASON_CLOSE: reason_str = "Terminal closed"; break;
        }
      g_logger.Info(__FUNCTION__, "Expert Advisor shutting down. Reason: " + reason_str + " (" + IntegerToString(reason) + ")");
      
      // Release the logger instance (this calls Shutdown() on all handlers)
      CLogger::Release();
      g_logger = NULL; // Set pointer to NULL after release
     }
   else
     {
      Print("Logger instance was already invalid during Deinit.");
     }
//--- Print to standard log just in case logger failed
   Print(MQL5InfoString(MQL5_PROGRAM_NAME) + ": Deinitialized. Reason code: " + IntegerToString(reason));
  }

In LoggingExampleEA.mq5 konzentriert sich OnDeinit auf das ordnungsgemäße Beenden des Logging-Frameworks:

if(CheckPointer(g_logger) != POINTER_INVALID)

Die Funktion prüft zunächst, ob der globale Logger-Zeiger g_logger noch gültig ist. Dies verhindert Fehler, wenn OnDeinit aufgerufen wird, nachdem der Logger bereits freigegeben wurde, oder wenn die Initialisierung fehlgeschlagen ist.

Innerhalb des if-Blocks ermittelt der Code mit Hilfe einer switch-Anweisung eine für den Menschen lesbare Zeichenfolge, die dem an OnDeinit übergebenen Grundcode entspricht. Dies gibt Aufschluss darüber, warum der EA anhält. Anschließend wird mit g_logger.Info() eine Informationsmeldung protokolliert, die die ermittelte Grundzeichenfolge und den ursprünglichen Grundcode enthält.

string reason_str = "Unknown reason";
      switch(reason)
        {
         case REASON_ACCOUNT: reason_str = "Account change"; break;
         case REASON_CHARTCHANGE: reason_str = "Chart symbol or period change"; break;
...
...
         case REASON_CLOSE: reason_str = "Terminal closed"; break;
        }
      g_logger.Info(__FUNCTION__, "Expert Advisor shutting down. Reason: " + reason_str + " (" + IntegerToString(reason) + ")");

Dadurch wird sichergestellt, dass die letzten Momente des EA-Betriebs, einschließlich des Grundes für das Anhalten, in den Protokollen aufgezeichnet werden (sowohl in der Konsole als auch in der Datei, je nach den konfigurierten Ebenen).

Dies ist der wichtigste Schritt bei der Bereinigung des Loggers:

CLogger::Release();

Der Aufruf der statischen Methode Release() der Klasse CLogger löst die Löschung der Singleton-Logger-Instanz aus. Als Teil seines Zerstörungsprozesses durchläuft der CLogger Destruktor alle hinzugefügten Handler (in diesem Fall die Konsolen- und Datei-Handler), ruft ihre jeweiligen Shutdown()-Methoden auf (was für den FileLogHandler das Schließen der geöffneten Protokolldatei bedeutet) und löscht dann die Handler-Objekte selbst. Dadurch wird sichergestellt, dass alle Ressourcen ordnungsgemäß freigegeben und die Dateien korrekt geschlossen werden.

Setzen des globalen Zeigers auf Null:

g_logger = NULL;

Nach Freigabe der Instanz wird der globale Zeiger g_logger explizit auf NULL zurückgesetzt. Dies ist eine gute Praxis, um anzuzeigen, dass der Zeiger nicht mehr auf ein gültiges Objekt zeigt.

Ein else-Block behandelt den Fall, dass g_logger zum Zeitpunkt des Aufrufs von OnDeinit bereits ungültig war, und gibt eine Meldung an das Standard-Expertenprotokoll aus. Zusätzlich sorgt eine abschließende Print-Anweisung außerhalb der Logger-Logik dafür, dass eine Deinitialisierungsmeldung immer im Standardprotokoll aufgezeichnet wird, selbst wenn der nutzerdefinierte Logger vollständig fehlgeschlagen ist.

Diese Implementierung demonstriert das korrekte Verfahren zum Herunterfahren des nutzerdefinierten Protokollierungs-Frameworks und stellt sicher, dass die Protokolldateien ordnungsgemäß geschlossen und die Ressourcen freigegeben werden, wenn der EA beendet wird.

OnTick():

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Ensure logger is valid
   if(CheckPointer(g_logger) == POINTER_INVALID)
     {
      // Attempt to re-initialize logger if it became invalid unexpectedly
      // This is defensive coding, ideally it shouldn't happen if OnInit succeeded.
      Print("Error: Logger instance invalid in OnTick! Attempting re-init...");
      if(OnInit() != INIT_SUCCEEDED)
        {
         Print("Critical Error: Failed to re-initialize logger in OnTick. Stopping EA.");
         ExpertRemove(); // Stop the EA
         return;
        }
     }

//--- Log tick arrival
   MqlTick latest_tick;
   if(SymbolInfoTick(_Symbol, latest_tick))
     {
      g_logger.Debug(__FUNCTION__, StringFormat("New Tick: Time=%s, Bid=%.5f, Ask=%.5f, Volume=%d",
                                             TimeToString(latest_tick.time, TIME_DATE|TIME_SECONDS),
                                             latest_tick.bid, latest_tick.ask, (int)latest_tick.volume_real));
     }
   else
     {
      g_logger.Warn(__FUNCTION__, "Failed to get latest tick info. Error: " + IntegerToString(GetLastError()));
     }

//--- Example Logic: Check for a simple crossover
   // Note: Use more robust indicator handling in a real EA
   double ma_fast[], ma_slow[];
   int copied_fast = CopyBuffer(iMA(_Symbol, _Period, 10, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_fast);
   int copied_slow = CopyBuffer(iMA(_Symbol, _Period, 50, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_slow);
   
   if(copied_fast < 3 || copied_slow < 3)
     {
      g_logger.Warn(__FUNCTION__, "Failed to copy enough indicator data.");
      return; // Not enough data yet
     }
     
   // ArraySetAsSeries might be needed depending on how you access indices
   // ArraySetAsSeries(ma_fast, true);
   // ArraySetAsSeries(ma_slow, true);
   
   bool cross_up = ma_fast[1] > ma_slow[1] && ma_fast[2] <= ma_slow[2];
   bool cross_down = ma_fast[1] < ma_slow[1] && ma_fast[2] >= ma_slow[2];

   if(cross_up)
     {
      g_logger.Info(__FUNCTION__, "MA Cross Up detected. Potential Buy Signal.");
      // --- Add trading logic here ---
      // Example: SendBuyOrder();
     }
   else if(cross_down)
     {
      g_logger.Info(__FUNCTION__, "MA Cross Down detected. Potential Sell Signal.");
      // --- Add trading logic here ---
      // Example: SendSellOrder();
     }
     
   // Log account info periodically
   static datetime last_account_log = 0;
   if(TimeCurrent() - last_account_log >= 3600) // Log every hour
     {
      g_logger.Info(__FUNCTION__, StringFormat("Account Update: Balance=%.2f, Equity=%.2f, Margin=%.2f, FreeMargin=%.2f",
                                            AccountInfoDouble(ACCOUNT_BALANCE),
                                            AccountInfoDouble(ACCOUNT_EQUITY),
                                            AccountInfoDouble(ACCOUNT_MARGIN),
                                            AccountInfoDouble(ACCOUNT_MARGIN_FREE)));
      last_account_log = TimeCurrent();
     }
  }

Heranzoomen...

//--- Ensure logger is valid
   if(CheckPointer(g_logger) == POINTER_INVALID)
     {
      // Attempt to re-initialize logger if it became invalid unexpectedly
      // This is defensive coding, ideally it shouldn't happen if OnInit succeeded.
      Print("Error: Logger instance invalid in OnTick! Attempting re-init...");
      if(OnInit() != INIT_SUCCEEDED)
        {
         Print("Critical Error: Failed to re-initialize logger in OnTick. Stopping EA.");
         ExpertRemove(); // Stop the EA
         return;
        }
     }

Ähnlich wie bei OnDeinit beginnt die Funktion mit der Überprüfung mit CheckPointer, ob der Zeiger g_logger-Zeiger gültig ist. Wenn sich der Logger als ungültig erweist (was idealerweise nach einem erfolgreichen OnInit nicht passieren sollte), wird als Schutzmaßnahme versucht, den Logger durch erneuten Aufruf von OnInit() neu zu initialisieren. Wenn die Neuinitialisierung fehlschlägt, wird mit der Standardfunktion Print ein kritischer Fehler protokolliert und der EA mit ExpertRemove() angehalten.

Außerdem versucht der EA, die neuesten Tick-Informationen mit SymbolInfoTick() abzurufen.
//--- Log tick arrival
   MqlTick latest_tick;
   if(SymbolInfoTick(_Symbol, latest_tick))
     {
      g_logger.Debug(__FUNCTION__, StringFormat("New Tick: Time=%s, Bid=%.5f, Ask=%.5f, Volume=%d",
                                             TimeToString(latest_tick.time, TIME_DATE|TIME_SECONDS),
                                             latest_tick.bid, latest_tick.ask, (int)latest_tick.volume_real));
     }
   else
     {
      g_logger.Warn(__FUNCTION__, "Failed to get latest tick info. Error: " + IntegerToString(GetLastError()));
     }

Bei Erfolg wird eine Debug-Meldung protokolliert, die den Zeitstempel des Tickers, den Geldkurs, den Briefkurs und das Volumen enthält, formatiert mit StringFormat. Dies liefert eine detaillierte Aufzeichnung der eingehenden Preisdaten, die für die Fehlersuche nützlich ist. Wenn SymbolInfoTick fehlschlägt, wird eine Warnmeldung mit dem über GetLastError() ermittelten Fehlercode protokolliert.

Der Code enthält außerdem ein einfaches Beispiel für die Überprüfung auf einen gleitenden Durchschnitt (MA):
//--- Example Logic: Check for a simple crossover
   // Note: Use more robust indicator handling in a real EA
   double ma_fast[], ma_slow[];
   int copied_fast = CopyBuffer(iMA(_Symbol, _Period, 10, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_fast);
   int copied_slow = CopyBuffer(iMA(_Symbol, _Period, 50, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_slow);
   
   if(copied_fast < 3 || copied_slow < 3)
     {
      g_logger.Warn(__FUNCTION__, "Failed to copy enough indicator data.");
      return; // Not enough data yet
     }
     
   // ArraySetAsSeries might be needed depending on how you access indices
   // ArraySetAsSeries(ma_fast, true);
   // ArraySetAsSeries(ma_slow, true);
   
   bool cross_up = ma_fast[1] > ma_slow[1] && ma_fast[2] <= ma_slow[2];
   bool cross_down = ma_fast[1] < ma_slow[1] && ma_fast[2] >= ma_slow[2];

   if(cross_up)
     {
      g_logger.Info(__FUNCTION__, "MA Cross Up detected. Potential Buy Signal.");
      // --- Add trading logic here ---
      // Example: SendBuyOrder();
     }
   else if(cross_down)
     {
      g_logger.Info(__FUNCTION__, "MA Cross Down detected. Potential Sell Signal.");
      // --- Add trading logic here ---
      // Example: SendSellOrder();
     }

Zunächst wird versucht, die Daten von zwei iMA-Indikatoren zu kopieren. Wenn nicht genügend Daten kopiert werden, wird eine Warnmeldung protokolliert, und die Funktion kehrt zurück. Wenn Daten verfügbar sind, wird geprüft, ob sich die schnellen und langsamen MAs der beiden vorangegangenen Balken gekreuzt haben. Wenn ein Kreuzen (cross_up oder cross_down) erkannt wird, wird eine Info-Level-Meldung protokolliert, die auf das potenzielle Handelssignal hinweist. Dies zeigt, dass wichtige Ereignisse im Rahmen der Handelsstrategie aufgezeichnet werden.

Schließlich protokollieren wir die Informationen in regelmäßigen Abständen und nicht bei jedem Tick:

   // Log account info periodically
   static datetime last_account_log = 0;
   if(TimeCurrent() - last_account_log >= 3600) // Log every hour
     {
      g_logger.Info(__FUNCTION__, StringFormat("Account Update: Balance=%.2f, Equity=%.2f, Margin=%.2f, FreeMargin=%.2f",
                                            AccountInfoDouble(ACCOUNT_BALANCE),
                                            AccountInfoDouble(ACCOUNT_EQUITY),
                                            AccountInfoDouble(ACCOUNT_MARGIN),
                                            AccountInfoDouble(ACCOUNT_MARGIN_FREE)));
      last_account_log = TimeCurrent();
     }

Eine statische Variable last_account_log speichert das letzte Mal, dass Kontoinformationen protokolliert wurden. Der Code prüft, ob die aktuelle Zeit (TimeCurrent()) mindestens 3600 Sekunden (1 Stunde) größer ist als die letzte Protokollzeit. Wenn dies der Fall ist, wird eine Info-Meldung mit dem aktuellen Kontostand, dem Eigenkapital, der Marge und der freien Marge protokolliert und last_account_log wird aktualisiert. Dadurch wird verhindert, dass die Protokolle mit sich wiederholenden Informationen überflutet werden, während gleichzeitig regelmäßige Statusaktualisierungen bereitgestellt werden.

Insgesamt zeigt die OnTick-Funktion, wie der Logger während der EA-Ausführung für verschiedene Zwecke eingesetzt werden kann: detailliertes Debugging (Debug-Ticks), Warnungen vor potenziellen Problemen (Warnung bei fehlgeschlagener Datenkopie), Informationsmeldungen zu wichtigen Ereignissen (Info über Signale) und regelmäßige Statusaktualisierungen (Info über Kontostatus).

OnChartEvent():

Die Funktion OnChartEvent() ist ein MQL5-Event-Handler, der dazu dient, verschiedene Ereignisse zu verarbeiten, die direkt auf dem Chart auftreten, auf dem der EA läuft. Diese Ereignisse können Nutzerinteraktionen wie Tastatureingaben oder Mausbewegungen, Klicks auf grafische Objekte oder nutzerdefinierte Ereignisse umfassen, die vom EA oder anderen MQL5-Programmen erzeugt werden.

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Ensure logger is valid
   if(CheckPointer(g_logger) == POINTER_INVALID) return;
   
//--- Log chart events
   string event_name = "Unknown Chart Event";
   switch(id)
     {
      case CHARTEVENT_KEYDOWN: event_name = "KeyDown"; break;
      case CHARTEVENT_MOUSE_MOVE: event_name = "MouseMove"; break;
      // Add other CHARTEVENT cases as needed
      case CHARTEVENT_OBJECT_CLICK: event_name = "ObjectClick"; break;
      case CHARTEVENT_CUSTOM+1: event_name = "CustomEvent_1"; break; // Example custom event
     }
     
   g_logger.Debug(__FUNCTION__, StringFormat("Chart Event: ID=%d (%s), lparam=%d, dparam=%.5f, sparam='%s'",
                                           id, event_name, lparam, dparam, sparam));
  }
//+------------------------------------------------------------------+

Wie bei OnTick und OnDeinit beginnt die Funktion mit der Überprüfung, ob der globale Logger-Zeiger g_logger gültig ist:

if(CheckPointer(g_logger) == POINTER_INVALID) return;

Wenn der Logger nicht gültig ist, kehrt die Funktion einfach zurück und verhindert eine weitere Verarbeitung oder mögliche Fehler.

Der Kern der Funktion identifiziert die Art des Ereignisses und protokolliert seine Einzelheiten:

//--- Log chart events
   string event_name = "Unknown Chart Event";
   switch(id)
     {
      case CHARTEVENT_KEYDOWN: event_name = "KeyDown"; break;
      case CHARTEVENT_MOUSE_MOVE: event_name = "MouseMove"; break;
      // Add other CHARTEVENT cases as needed
      case CHARTEVENT_OBJECT_CLICK: event_name = "ObjectClick"; break;
      case CHARTEVENT_CUSTOM+1: event_name = "CustomEvent_1"; break; // Example custom event
     }
     
   g_logger.Debug(__FUNCTION__, StringFormat("Chart Event: ID=%d (%s), lparam=%d, dparam=%.5f, sparam='%s'",
                                           id, event_name, lparam, dparam, sparam));

Eine switch-Anweisung nimmt jede eingehende Ereignis-ID und wandelt sie in einen sprechenden Ereignisnamen um, wie CHARTEVENT_KEYDOWN, CHARTEVENT_MOUSE_MOVE oder CHARTEVENT_OBJECT_CLICK. Es wird sogar gezeigt, wie auf ein nutzerdefiniertes Signal (CHARTEVENT_CUSTOM + 1) reagiert werden kann.

Als Nächstes geben wir mit g_logger.Debug() eine Meldung auf Debug-Ebene aus. Dieser Eintrag erfasst die Ereignis-ID, den aufgelösten Ereignisnamen und die Parameterwerte (lparam, dparam, sparam), formatiert durch StringFormat. Diese Informationen sind während der Entwicklung und des Testens von unschätzbarem Wert, da sie die Verfolgung von Chart-Aktionen und die Verfolgung von nutzerdefinierten Ereignisflüssen in Ihrer Anwendung ermöglichen.



Vorteile des Custom Logging Framework

Unser maßgeschneidertes Protokollierungssystem bietet mehrere Verbesserungen gegenüber der einfachen Print()-Funktion:

  • Schweregradfilterung: Es werden nur die Nachrichten angezeigt, die wichtig sind, geordnet nach Priorität.
  • Mehrere Ausgänge: Es werden die Protokolle gleichzeitig an die Konsole, Dateien oder andere Ziele gesendet.
  • Reichhaltiger Kontext: Zeitstempel, Quelle und EA-Details werden automatisch hinzugefügt.
  • Flexible Formatierung: Sie können das Nachrichtenlayout an Ihre Lesevorlieben anpassen.
  • Dateirotation: Es wird verhindert, dass Protokolldateien unbegrenzt wachsen.
  • Zentralisierte Kontrolle: Die Protokollierung wird global oder für einzelne Handlers ein- oder ausgeschaltet.

Diese Funktionen machen die Fehlersuche in komplexen Handelssystemen wesentlich effizienter. Sie können Probleme schnell ausfindig machen, das Verhalten im Laufe der Zeit beobachten und sich auf die wirklich wichtigen Daten konzentrieren.



Schlussfolgerung

Sobald dieses nutzerdefinierte Logging-Framework eingerichtet ist, können Sie die willkürlichen Print()-Anweisungen über Bord werfen und eine Welt betreten, in der Ihr Code in klaren, kontextreichen und vollständig anpassbaren Meldungen spricht. Kritische Fehler springen heraus, ausführliche Traces stehen für eine spätere Überprüfung bereit, und die Protokolldateien bleiben aufgeräumt. Noch besser: Das System passt sich Ihren Gewohnheiten an: Sie können die Handler austauschen, die Formate umgestalten oder die Ausführlichkeit erhöhen oder verringern, wann immer Sie wollen. Der nächste Artikel befasst sich mit Profiling- und Unit-Testing-Tools, mit denen Sie Leistungsprobleme und Logikfehler erkennen können, lange bevor sie in einem Live-Chart auftauchen. So sieht echte MQL5-Handwerkskunst aus.

Und vergessen Sie nicht, dass dies nur die erste Etappe der Reise ist. Wir haben immer noch fortschrittliche Debugging-Tricks, nutzerdefinierte Profiler, ein felsenfestes Unit-Test-Kabelbaum und automatische Code-Qualitäts-Scans auf der Tagesordnung. Am Ende der Serie werden Sie reaktive Fehlerjagden gegen eine disziplinierte, proaktive Qualitätsroutine eintauschen.

Bis dahin: Viel Spaß beim Tauschen und Programmieren!

Datei-Übersicht:

Dateiname Beschreibung der Datei
LogLevels.mqh Definiert die Enumeration LogLevel mit den im gesamten Framework verwendeten Schweregraden DEBUG→OFF.
ILogHandler.mqh Deklariert die ILogHandler-Schnittstelle (Setup/Log/Shutdown), die alle konkreten Log-Output-Klassen implementieren.
ConsoleLogHandler.mqh Implementiert ILogHandler, um formatierte Logmeldungen auf der MetaTrader-Registerkarte „Experts“ mit Level-basierter Filterung zu drucken.
FileLogHandler.mqh Implementiert ILogHandler, um Protokolle in täglich rotierende/größenbegrenzte Dateien zu schreiben und eine konfigurierbare Dateihistorie zu führen.
CLogger.mqh Singleton-Logger, der Handler speichert, globale Schweregradfilterung anwendet und komfortable Log-Methoden bietet.
LoggingExampleEA.mq5 Beispiel für einen Expert Advisor, der zeigt, wie das nutzerdefinierte Protokollierungs-Framework in der Praxis eingerichtet, verwendet und abgeschaltet wird.

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17933

Beigefügte Dateien |
LogLevels.mqh (0.84 KB)
ILogHandler.mqh (1.08 KB)
FileLogHandler.mqh (14.38 KB)
CLogger.mqh (8.51 KB)
Datenwissenschaft und ML (Teil 40): Verwendung von Fibonacci-Retracements in Daten des maschinellen Lernens Datenwissenschaft und ML (Teil 40): Verwendung von Fibonacci-Retracements in Daten des maschinellen Lernens
Fibonacci-Retracements sind ein beliebtes Instrument der technischen Analyse, das Händlern hilft, potenzielle Umkehrzonen zu identifizieren. In diesem Artikel werden wir untersuchen, wie diese Retracement-Levels in Zielvariablen für maschinelle Lernmodelle umgewandelt werden können, damit diese den Markt mit Hilfe dieses leistungsstarken Tools besser verstehen können.
Handel mit dem MQL5 Wirtschaftskalender (Teil 9): Bessere Interaktion mit Nachrichten durch eine dynamische Bildlaufleiste und eine optimierte Anzeige Handel mit dem MQL5 Wirtschaftskalender (Teil 9): Bessere Interaktion mit Nachrichten durch eine dynamische Bildlaufleiste und eine optimierte Anzeige
In diesem Artikel erweitern wir den MQL5-Wirtschaftskalender um eine dynamische Bildlaufleiste für eine intuitive Nachrichtennavigation. Wir sorgen für eine reibungslose Darstellung der Ereignisse und eine effiziente Aktualisierungen. Wir validieren die reaktionsschnelle Bildlaufleiste und das ausgefeilte Dashboard durch Tests.
Handel mit dem MQL5 Wirtschaftskalender (Teil 10): Bewegliches Dashboard und interaktive Hover-Effekte für eine reibungslose Nachrichten-Navigation Handel mit dem MQL5 Wirtschaftskalender (Teil 10): Bewegliches Dashboard und interaktive Hover-Effekte für eine reibungslose Nachrichten-Navigation
In diesem Artikel verbessern wir den MQL5-Wirtschaftskalender, indem wir ein bewegliches Dashboard einführen, mit dem wir die Schnittstelle für eine bessere Sichtbarkeit der Charts neu positionieren können. Wir implementieren Hover-Effekte für Schaltflächen, um die Interaktivität zu verbessern und eine nahtlose Navigation mit einer dynamisch positionierten Bildlaufleiste zu gewährleisten.
Die Grenzen des maschinellen Lernens überwinden (Teil 2): Mangelnde Reproduzierbarkeit Die Grenzen des maschinellen Lernens überwinden (Teil 2): Mangelnde Reproduzierbarkeit
Der Artikel geht der Frage nach, warum die Handelsergebnisse bei verschiedenen Brokern selbst bei Verwendung derselben Strategie und desselben Finanzsymbols aufgrund dezentraler Preisfestsetzung und Datenabweichungen erheblich voneinander abweichen können. Der Artikel hilft MQL5-Entwicklern zu verstehen, warum ihre Produkte auf dem MQL5-Marktplatz gemischte Bewertungen erhalten können, und fordert die Entwickler auf, ihre Ansätze auf bestimmte Makler zuzuschneiden, um transparente und reproduzierbare Ergebnisse zu gewährleisten. Dies könnte sich zu einer wichtigen bereichsgebundenen Best Practice entwickeln, die unserer Gemeinschaft gute Dienste leisten würde, wenn sie auf breiter Ebene übernommen würde.