Implementierung von praktischen Modulen aus anderen Sprachen in MQL5 (Teil 05): Das Logging-Modul von Python, Log Like a Pro
Inhalt
- Einführung
- Probleme mit dem MetaTrader 5 Logging-Mechanismus
- Die Protokollierungsfunktion für Python in MQL5
- Grundlegende Konfigurationen für einen Logger
- Einige Informationen protokollieren
- Spezifische Funktion für jede Protokollmeldung
- Optimierung des Protokollierungsprozesses
- Die Protokollierung stark vereinfachen – der Python-Weg
- Schlussfolgerung
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.

Im Bereich des algorithmischen Handels ist die Protokollierung sehr wichtig, da sie uns hilft:
- 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.,
- unsere Logik zu validieren und sicherzustellen, dass sie unter allen Marktbedingungen genau wie vorgesehen funktioniert,
- 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
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
Einführung in MQL5 (Teil 30): Beherrschung der API- und WebRequest-Funktion in MQL5 (IV)
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Verstärkte Gewinnarchitektur: Mehrschichtiger Kontoschutz
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.