English
preview
Implementierung von praktischen Modulen aus anderen Sprachen in MQL5 (Teil 05): Das Logging-Modul von Python, Log Like a Pro

Implementierung von praktischen Modulen aus anderen Sprachen in MQL5 (Teil 05): Das Logging-Modul von Python, Log Like a Pro

MetaTrader 5Handel |
14 0
Omega J Msigwa
Omega J Msigwa

Inhalt


Einführung

Die Protokollierung ist in jedem modernen Gerät, Programm oder jeder Software sehr wichtig. Es handelt sich dabei einfach um die Aufzeichnung aller Ereignisse, die während der Laufzeit eines bestimmten Vorgangs stattgefunden haben.

  • Computer führen Aufzeichnungen über Softwarenutzung, Verbindungen und Systemereignisse.
  • Unsere Browser speichern den Verlauf der von uns besuchten Websites und wie wir mit ihnen interagieren.

Die Aufbewahrung dieser Aufzeichnungen ist aus vielen wichtigen Gründen unerlässlich, z. B. zur Fehlersuche, Fehlerbehebung, Prüfung, Leistungsüberwachung und zum Verständnis des Verhaltens unserer Systeme im Laufe der Zeit.

Bildquelle: pexels.com

Im Bereich des algorithmischen Handels ist die Protokollierung sehr wichtig, da sie uns hilft:

  1. Handelsentscheidungen zu überwachen; wir können sehen, was geschehen ist und wann Expert Advisors eine Position eröffnet, geändert oder geschlossen haben – und warum, usw.,
  2. unsere Logik zu validieren und sicherzustellen, dass sie unter allen Marktbedingungen genau wie vorgesehen funktioniert,
  3. die komplexe Logik zu kontrollieren, um herauszufinden, wo eine Berechnung schiefgelaufen ist oder warum ein Handel abgelehnt wurde, und vieles mehr.

MetaTrader 5 verfügt über einen eingebauten Logging-Mechanismus, der recht ordentlich ist und gut funktioniert, aber es gibt einige Einschränkungen.


Probleme mit dem MetaTrader 5 Logging-Mechanismus

Alle Protokolle werden mit systemgenerierten Protokollen gemischt.

Auf der Registerkarte „Experten“ werden keine Informationen über ein bestimmtes Programm angezeigt; alle Protokolle werden in der gleichen Konsole ausgedruckt und in der gleichen Protokolldatei für einen bestimmten Tag gespeichert.

Dies erschwert die Überwachung von Protokollen für ein bestimmtes Programm oder eine bestimmte Funktion.

Sie sind schwer zu formatieren

Da es keine bestimmte Art und Weise gibt, die Informationen in MQL5 zu drucken, können alle Protokolle unterschiedlich sein. Diese Inkonsistenz macht es schwierig, Fehler zu erkennen und Schwachstellen zu identifizieren.

Sie haben wenig bis keine Kontrolle über die Ausführlichkeit

Wenn Sie nicht explizit eine Reihe von if-Anweisungen vor jeden Aufruf der „Druckfunktion“ setzen, gibt es keine Möglichkeit, die Informationen zu kontrollieren, die auf der Registerkarte „Experten“ ausgegeben werden.

Dies sind nur einige der Einschränkungen der integrierten MetaTrader 5-Protokollierung. In Python gibt es ein eingebautes Modul namens logging, das einige der oben genannten Einschränkungen behebt. In diesem Artikel werden wir sehen, worum es bei diesem Modul geht, und eine fast identische Bibliothek in der Programmiersprache MQL5 implementieren.


Die Protokollierungsfunktion für Python in MQL5

Laut Python-Dokumentation:

Das Modul mit dem Namen logging definiert Funktionen und Klassen, die ein flexibles Ereignisprotokollierungssystem für Anwendungen und Bibliotheken implementieren.

Dieses Modul legt den Schwerpunkt auf Flexibilität, wobei die Grundprinzipien der Protokollierung beibehalten werden, sodass die Nutzer eine einfache und universelle Möglichkeit haben, die Ereignisse in ihren Python-Anwendungen aufzuzeichnen.

Beispiel.

import logging
logger = logging.getLogger(__name__)

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

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

if __name__ == '__main__':
    main()

Outputs (myapp.log).

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

In nur wenigen Codezeilen konnten wir die Datei angeben, in der die Protokolle gespeichert werden sollten, und einige Informationen in diese Datei eintragen.

Im Gegensatz dazu benötigen wir in der entsprechenden MQL5-Klasse die Funktion getLogger nicht, da diese lediglich einen Logger mit einem bestimmten Namen abruft (oder erstellt).

Dies kann in unserem Klassenkonstruktor gehandhabt werden, mit einer Option zur Zuweisung eines Namens. Ein Klassenkonstruktor kann das CLogger-Objekt zurückgeben.

class CLogger
  {
private:

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

   int               m_fileflags;
   bool              is_configured;

public:

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

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

Eine der wichtigsten Funktionen in dieser Klasse ist eine Funktion namens basicConfig.


Grundlegende Konfigurationen für einen Logger

Die Möglichkeit, festzulegen, was von Ihren Protokollen erwartet wird, ist von entscheidender Bedeutung und kann nur innerhalb dieser Funktion erfolgen. Im Folgenden sind einige Dinge (Variablen) aufgeführt, die wir konfigurieren müssen.

Der Dateiname (filename)

Dies ist der Name der Datei, in die Sie alle Protokolle schreiben möchten.

Die Dateinamenerweiterung muss entweder .txt oder .log lauten.

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


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

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

Überprüfung der Dateinamenerweiterung.

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

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

   return false;
  }

Auch bei der Protokollierung werden Informationen in einer bestimmten Datei gespeichert. Wir wollen eine bestimmte Textdatei öffnen.

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


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

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

//--- Open the file for writting

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

   if(m_iscommon)
      m_fileflags |= FILE_COMMON;

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

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

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

//--- Handle large files than accepted

   fileRotate(m_filehandle, m_filename, m_fileflags, m_iscommon);

   is_configured = true;
   return true;
  }

Um die Effizienz und die Geschwindigkeit der Protokollierung zu erhöhen, müssen wir den Prozess des Lesens und Schreibens in eine Protokolldatei optimieren. Die Größe einer Protokolldatei muss streng begrenzt werden (große Textdateien sind schwieriger zu lesen und zu schreiben, da sie zu viel Speicherplatz beanspruchen).

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

Der Standardwert ist 10 Megabyte. Wie Sie vielleicht wissen, ist eine Textdatei, die größer als 10 MB ist, riesig. Zu groß für eine einfache Textdatei mit wenigen Bytes an Informationen pro Zeile.

Jedes Mal, wenn eine Datei diese Grenze überschreitet, wird automatisch eine neue Protokolldatei mit demselben Basisnamen + _[bestehende Protokolldateien mit demselben Namen++] erstellt. Wenn zum Beispiel eine Protokolldatei mit dem Namen mylogs.log vorhanden ist, wird eine neue Datei mit dem Namen mylogs_1.log erstellt.

Außerdem gibt es eine Obergrenze für die Anzahl der Dateien, die für einen bestimmten Basisnamen erstellt werden können. Die Obergrenze liegt bei 1000 Dateien (Standardeinstellung).

Die Funktion mit dem Namen fileRotate ist für diese Aufgabe vorgesehen.

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

//--- Close the current larger file

   FileClose(handle);
   
//---

   if(!checkFileExtenstion(filename))
      return;

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

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

//--- Get the incremented file names

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

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

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

Beispielhaft Ausgaben.

Die Schätzung der Dateigröße ist vielleicht nicht die genaueste, aber sie ist sehr nahe dran. Wenn sich eine Datei der 10-Megabyte-Marke nähert, wird eine neue Datei erstellt, in die neue Protokolle geschrieben werden.

console_on

Wenn diese Option auf „true“ gesetzt ist, werden alle Protokolle in der Konsole (Registerkarte „Experten“) ausgedruckt, nachdem sie in einer bestimmten Protokolldatei gespeichert wurden.

Dadurch wird vermieden, dass eine zusätzliche Codezeile nur für den Ausdruck der Informationen geschrieben wird.

file_common

Diese boolesche Variable gibt an, ob sich eine angegebene „Log-Datei“ im Common-Verzeichnis (wenn auf true gesetzt) oder im regulären MQL5-Datenpfad (wenn auf false gesetzt) befindet.

//--- Open the file for writting

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

   if(m_iscommon)
      m_fileflags |= FILE_COMMON;

log_level

Diese Variable teilt dem Logger mit, wie weit/tief wir die Informationen ausgeben wollen. 

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

Was macht LOG_LEVEL mit einer Klasse?

Diese Variable ist wichtig, wie viele Leute vielleicht denken, da sie den Mindestschweregrad vorgibt, den der Logger tatsächlich in die Datei schreibt oder ausgibt, und als Filter fungiert.

Angenommen, der Nutzer hat LOG_LEVEL_INFO ausgewählt, so bedeutet dies, dass alle Ebenen unterhalb dieser Log-Ebene ignoriert werden.

Funktion Stufe Wird es protokolliert/gedruckt?
CLogger.debug()
10 NEIN
CLogger.info()
20 JA
CLogger.warning()
30 JA
CLogger.error()
40 JA
CLogger.critical()
50 JA 

Obwohl die Funktion mit dem Namen debug() ausgeführt wird, bewirkt sie nichts, da der Pegel unter dem zulässigen Minimum liegt.

Dies ist sehr nützlich, da es Ihnen erlaubt, die Ausführlichkeit durch Konfiguration zu kontrollieren. Wenn Sie sich beispielsweise im Entwicklungsmodus befinden, können Sie LOG_LEVEL_DEBUG auswählen, wodurch die Klasse alles protokolliert, was Ihnen hilft, Ihre Programme effektiv zu debuggen. Im Gegensatz dazu können Sie LOG_LEVEL_WARNING im Live-Handelsmodus auswählen, um nur Warnungen, Fehler und die kritischsten/fatalsten Fehler zu protokollieren, die protokolliert werden müssen.

Format

Die Formatvariable ist eine der wichtigsten Variablen in der Klasse, denn sie ist die einzige Stelle, an der Sie steuern können, wie die Protokolle in der Registerkarte „Experten“ und bei der Speicherung in den Dateien erscheinen.

Die folgende Tabelle enthält eine Beschreibung der Formatplatzhalter und ihrer Ausgaben.

Platzhalter Beschreibung Anmerkungen und Beispiele 
%(asctime)s
Ein lokaler Zeitstempel des Protokolleintrags TimeLocal, formatiert als JJJ.MM.TT HH:MM:SS. Eine Zeitabgaben wie: 2025.01.01 00:00:05.
%(levelname)s
Name der Protokollebene als Text Sie kann INFO, DEBUG, ERROR, WARNING oder CRITICAL sein.
%(programname)s
Der Name des Programms oder der Komponente. Er kann über einen optionalen Klassenkonstruktor mit dem Argument name gesetzt werden. Beispiel, Mein Indikator.
%(functionname)s
Name der Funktion, bei der ein Protokoll erstellt wird. Kann manuell über die Protokollierungsfunktionen bereitgestellt werden, z. B. OnTick, OnInit
%(linenumber)d
Nummer der Codezeile, in der das Protokoll erstellt wird. Zum Beispiel, Zeile 118. Nur wenn die Zeilennummer geparst wird, ansonsten wird ein leerer Wert zurückgegeben.
%(programtype)s

Der Typ eines laufenden Programms, der über einen nutzerdefinierten Konstruktor festgelegt werden kann. Sie hängt von ENUM_PROGRAM_TYPE ab.

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

      case PROGRAM_EXPERT:
         return "EA";

      case PROGRAM_INDICATOR:
         return "Indicator";

      case PROGRAM_SERVICE:
         return "Service";

      default:
         return "Unknown";
   }
}
Es kann ein Skript, EA, Indikator, Dienst oder Unbekannt sein.
%(message)s
Die Logmeldung selbst. Zum Beispiel: Fehler bei der Erstellung eines schwebenden Auftrags.

Wie die Formate mit den Protokollen interagieren, werden wir in den späteren Abschnitten dieses Beitrags im Detail sehen.

Beispielformat.

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

Ausgabe:

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

Beispielformat.

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

Ausgabe:

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


Einige Informationen protokollieren

Eine private Funktion in der Klasse Log ist für die Formatierung, die Erstellung und das Schreiben einer Nachricht (Log) in eine Datei verantwortlich, ganz zu schweigen von der Anzeige der Informationen auf der Registerkarte Experten (Drucken).

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

Da alle Klassenkonstruktoren optional sind, können die Nutzer mit der Protokollierung beginnen, ohne viel Konfiguration vornehmen zu müssen.

Die Funktion basicConfig, die die Konfigurationen vornimmt, ist ebenfalls eine optionale Methode. Wir müssen die Standardkonfigurationen verstärken (wenn ein Nutzer seine eigenen nicht angeboten hat), bevor wir versuchen, einige Informationen oder Werte zu schreiben (zu protokollieren).

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

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

Wie bereits bei den Log-Ebenen beschrieben, müssen wir prüfen, ob die aktuelle Ebene eines Logs nicht unter der vom Nutzer festgelegten erforderlichen Ebene liegt.

//--- Level filtering

   if(level < m_loglevel)
      return;

Formatierung der Protokolle

Wir müssen alle Platzhalter entfernen und sie durch den gewünschten Wert ersetzen.

// Standard placeholders

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

// Custom placeholders

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

// Program name

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

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

// Program type

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

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

   entry += "\n";

Handhabung von Datei-Rotationen

Da jede Datei die maximale Größe von 10 MB (standardmäßig) erreichen kann, müssen wir diese Bedingung jedes Mal überprüfen, bevor wir versuchen, neue Informationen in die Datei einzufügen.

//--- Handle file rotations before writing

   fileRotate(m_filehandle, m_filename, m_fileflags, m_iscommon);

Schreiben und Drucken der Protokolle

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

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

   if(m_console_on)
      Print(entry);

Die Funktion mit dem Namen Log ist außerhalb der Klasse nicht zugänglich, da sie dazu gedacht ist, andere Funktionen für ganz bestimmte Protokollmeldungen zu füllen. Im folgenden Abschnitt werden diese Funktionen dargestellt.


Spezifische Funktion für jede Protokollmeldung

Protokolle für Debugging-Zwecke

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

Diese Funktion zielt darauf ab, die Nachricht auf der untersten Ebene auszudrucken. Sie ist normalerweise für Entwickler gedacht, die das gesamte Verhalten einiger Codezeilen und Funktionen verstehen wollen.

Beispiel für die Verwendung:

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

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

Ausgabe:

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

Informative Protokolle

Diese Art von Nachricht wird oft verwendet, um einen laufenden Prozess oder eine Aktivität anzuzeigen.

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

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

Ausgabe:

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

Anzeige der Fehler für den Nutzer

Dies sind Protokolle, die eine Störung im Programm anzeigen sollen.

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

Ausgabe:

void OnStart()
  {
//---

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

Ausgabe:

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

Protokollierung einiger Warnungen

Diese Protokolle werden verwendet, um Nutzer vor etwas zu warnen, das für das Programm zwar nicht fatal ist, aber dennoch zur Kenntnis genommen werden muss.

Beispiel für die Verwendung:

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

Ausgabe:

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

Fatale oder kritische Protokolle

Dies sind die wichtigsten Protokolle aufgrund ihrer Schwere. Sie werden oft verwendet, um einen großen Fehler im System anzuzeigen, der normalerweise bedeutet, dass das Programm nicht ausgeführt werden kann, bis ein bestimmtes Problem behoben ist.

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

Nehmen wir an, Sie haben einen Indikator, der für Ihre Strategie so nützlich ist, dass das gesamte Programm abbricht, sobald er nicht geladen werden kann 

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

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

Ausgabe:

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


Optimierung des Protokollierungsprozesses

Der Prozess des Lesens und Schreibens in Textdateien (E/A-Operationen) ist einer der teuersten Prozesse in der Sprache MQL5, ganz zu schweigen von einer eingebauten Funktion namens Drucken, die wir für die Anzeige der Informationen auf der Registerkarte Experten verwenden.

Anstatt sehr oft (innerhalb weniger Sekunden) in eine Textdatei zu schreiben, können wir den Nutzern die Möglichkeit geben, die Protokolle vorübergehend im Speicher (Cache) zu speichern, bevor sie entscheiden, ob sie diese Informationen in einer bestimmten Datei speichern wollen.

Der Prozess ist einfach: Wir haben ein globales Array, in das wir die Protokolle schreiben wollen, und eine Funktion, die das gesamte Array in eine bestimmte Datei schreibt.

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

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

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

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

//--- some lines of code

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

// some lines of code....

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

   if(m_console_on)
      Print(entry);
  }

Die Funktion WriteCache schreibt alle im Array m_logs_cache gespeicherten Informationen in eine gewünschte Datei, ähnlich wie die Informationen automatisch in die Dateien geschrieben werden, wenn die Variable cache_mode in der Funktion basicConfig auf false gesetzt ist.

Da die Nutzer in der Lage sind, diese Funktion nach Belieben aufzurufen, können wir die Dinge vereinfachen, indem wir eine boolesche Variable namens write_cache_automatically in die Funktion basicConfig einführen. Wenn diese Variable auf true gesetzt wird, werden alle in einem temporären Cache-Array gespeicherten Informationen im Destruktor der Klasse in eine angegebene Datei geschrieben.

Angenommen, wir wollen die Protokolle speichern, nachdem alle Operationen abgeschlossen sind, d. h. ein Expert Advisor wird aus dem entfernt oder die Strategietester-Operation ist beendet.

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

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

Schließlich konnte ich einige Verbesserungen im Strategietester feststellen (etwa eine 50 %ige Verringerung der Testzeit) im Vergleich zur Version ohne Zwischenspeicherung. 

Dies geschah auch, nachdem mehrere Änderungen an der Funktion vorgenommen wurden, die für die Rotation der Dateien zuständig ist.

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

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

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

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

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

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

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

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

Optimale Strategieprüfung mit Protokollierung

Nachdem wir sichergestellt haben, dass der Zwischenspeichermodus auf true gesetzt ist, müssen wir das Drucken verhindern, indem wir die Variable console_on im Strategietester auf false setzen, es sei denn, wir haben einen triftigen Grund, dies zu tun.

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

Da der Strategy Tester alle in Dateien gespeicherten Informationen in einem anderen Pfad für die Dateien speichert, müssen wir die Variable file_common auf true setzen, damit wir alle im gemeinsamen Ordner gespeicherten Protokolle abrufen können.

Der Rest eines EA.

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

Ausgabe:

Wie erwartet wurden unter dem gemeinsamen Verzeichnis mehrere Dateien mit einer Größe von jeweils knapp 10 MB erstellt.


Die Protokollierung stark vereinfachen – der Python-Weg

Wenn Sie mit dem Logging-Modul in Python vertraut sind, werden Sie feststellen, dass es nicht erforderlich ist, den Namen der Funktion und eine bestimmte Codezeile, die einen Fehler erzeugt, zu analysieren.

import logging
logger = logging.getLogger(__name__)

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

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

Ausgabe:

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

In MQL5 müssen wir die meisten Werte hart kodieren (ein Zeilen- und Funktionsname für jede Logmeldung, Programmname (Dateiname) im Klassenkonstruktor). Um diesen lästigen/wiederholten Prozess zu vermeiden, können wir Makros mit #define verwenden.

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

Verwendung:

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

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

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

Ausgabe:

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


Abschließende Überlegungen

Die Protokollierung ist mehr als nur der Ausdruck von einfachem Text auf der Registerkarte Experten. Sie ist ein grundlegender Bestandteil der Softwareentwicklung und hilft uns zu verstehen, wie sich unser Programm verhält, Probleme zu diagnostizieren und Ereignisse über die Zeit zu verfolgen.

Durch die Implementierung eines strukturierten und wiederverwendbaren Logging-Moduls in MQL5, ähnlich wie die Logging-Bibliothek von Python. Wir bringen moderne Entwicklungspraktiken in unsere Handelssysteme ein. Dadurch ist unser Code einfacher zu pflegen, leichter zu debuggen und entspricht eher der Art und Weise, wie professionelle Entwickler weltweit Protokolle in Python-basierten Systemen, Webservern usw. speichern und interpretieren.

Ein zuverlässiges Protokollierungsmodul ist nicht nur eine Bequemlichkeit; es ist ein Werkzeug, das uns hilft, organisiert und effizient zu arbeiten und den Industriestandard bei der Programmierung einzuhalten.

Ein Repository, das den gesamten in dieser Artikelserie besprochenen Code enthält, ist hier zu finden: https://github.com/MegaJoctan/PyMQL5 für Beiträge und Fehlerkorrekturen.


Tabelle der Anhänge

Dateiname Beschreibung und Verwendung
Include\PyMQL5\logging.mqh Python-ähnliche Protokollierungsklasse zur Anzeige und Speicherung der Protokolle. Es hat die Klasse CLogger.
Scripts\Logging Test.mq5
Ein einfaches Skript zum Testen der in der Klasse CLogger vorgestellten Methoden.
Experts\Logging Test.mq5 Ein Expert Advisor (EA), der entwickelt wurde, um die in der CLogger-Klasse vorgestellten Methoden in der realen Handelsumgebung zu testen.

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

Beigefügte Dateien |
Attachments.zip (7.35 KB)
Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Einführung in MQL5 (Teil 30): Beherrschung der API- und WebRequest-Funktion in MQL5 (IV) Einführung in MQL5 (Teil 30): Beherrschung der API- und WebRequest-Funktion in MQL5 (IV)
Entdecken Sie eine Schritt-für-Schritt-Anleitung, das die Extraktion, Konvertierung und Organisation von Kerzendaten aus API-Antworten innerhalb der MQL5-Umgebung vereinfacht. Dieser Leitfaden ist ideal für Einsteiger, die ihre Programmierkenntnisse verbessern und solide Strategien zur effizienten Verwaltung von Marktdaten entwickeln möchten.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Verstärkte Gewinnarchitektur: Mehrschichtiger Kontoschutz Verstärkte Gewinnarchitektur: Mehrschichtiger Kontoschutz
In dieser Diskussion stellen wir ein strukturiertes, mehrschichtiges Verteidigungssystem vor, das darauf ausgelegt ist, aggressive Gewinnziele zu verfolgen und gleichzeitig das Risiko katastrophaler Verluste zu minimieren. Der Schwerpunkt liegt auf der Verbindung einer offensiven Handelslogik mit Schutzmaßnahmen auf jeder Ebene der Handelskette. Die Idee ist, einen EA zu entwickeln, der sich wie ein „risikobewusstes Raubtier“ verhält – fähig, hochwertige Gelegenheiten zu ergreifen,jedoch stets mit einem mehrschichtigen Schutz, um zu verhindern, dass man von plötzlichen Marktturbulenzen überrascht wird.