Die Fehlerverarbeitung und Protokollierung in MQL5

Sergey Eremin | 27 Mai, 2016

Einführung

Während der Arbeit können in den meisten Programmen von Zeit zu Zeit Fehler auftreten. Sie angemessen zu verarbeiten - ist einer der wichtigsten Aspekte der Qualitätsvolle- und Nachhaltigen-Software. In diesem Artikel werden die wichtigsten Methoden der Fehlerverarbeitung betrachtet, werden Ratschläge über ihre Verwendung gegeben, und wird auch die Protokollierung MQL5 betrachtet.

Die Fehlerverarbeitung ist ein schwieriges und umstrittenes Thema. Es gibt viele Arten für die Fehlerverarbeitung, und jede von denen besitzt bestimmte Vor- und Nachteile. Viele dieser Methoden erlauben die gemeinsame Verwendung, aber es gibt kein allgemeines Rezept - für jede spezifische Aufgabe sollte ein angemessener Ansatz gewählt werden.


Die Hauptmethoden für die Fehlerverarbeitung

Wenn das Programm während seiner Arbeit einige Fehler hat, dann muss er am häufigsten für die korrekte Funktionalität eine Aktion (oder mehrere Aktionen) durchführen. Als Beispiel können wir für solche Aktionen die folgenden Aktionen aufführen:

Die Ausführung des Programms stoppen. Falls ein Fehler auftritt, ist das richtige Verhalten, das Programm zu beenden. In der Regel sind es die kritischen Fehler, nach deren Scheidung die Ausführung des Programms nicht mehr möglich ist, ist sinnlos oder einfach gefährlich. In MQL5 für eine Reihe von Laufzeitfehlern ist ein regelmäßiger Arbeitsbruch-Mechanismus vorgesehen: Wenn zum Beispiel im Programm dort eine Division durch Null oder außerhalb der Grenzen des Arrays auftritt, stoppt das Programm die Arbeit. In anderen Fällen muss sich der Programmierer selbst um die Beendigung der Arbeit kümmern. Zum Beispiel können Sie die Funktion ExpertRemove() für den EA verwenden:

Ein Beispiel für die Beendigung des EAs durch ExpertRemove()

void OnTick()
  {
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      Alert("fail");
      ExpertRemove();
      return;
     }
  }


Bringen Sie falsche Werte zum Bereich der richtigen Werte. Oft muss ein Wert innerhalb des angegebenen Bereichs liegen. Jedoch kann in einigen Fällen Werte außerhalb dieses Bereichs erscheinen. Dann ist es möglich, die Werten zu den zulässigen Grenzen zwanghaft zurückzubringen. Als Beispiel wird die Berechnung des Volumens der offenen Position gebracht. Wenn der resultierende Volumen außerhalb der Minimal- und Maximalwert geraten ist, kann es zu den Grenzen zwanghaft zurückgebracht:

Ein Beispiel eines falschen Wertes, der zum richtigen Bereich gebracht wurde

double VolumeCalculation()
  {
   double result=...;

   result=MathMax(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN));
   result=MathMin(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX));

   return result;
  }

Wenn jedoch aus irgendeinem Grund der Volumen größer als die maximale Grenze ist, und das Deposit kann keine solche Last tragen, wird das ein weises Verhalten, wenn alles aufgezeichnet wird und die Ausführung des Programms unterbrechen. Oft dieser spezielle Fehler ist sehr gefährlich für das Konto.


Bringen Sie die Fehlerwerte zurück. In diesem Fall, wenn ein Fehler auftritt, muss ein Verfahren oder eine Funktion einen vorbestimmten Wert zurückgeben, der über den Fehler signalisiert. Das Beispiel, wenn unsere Methode oder Funktion falsche Werte string zurückgeben muss, dann beim Fehlerauftriit das NULL zurückgebt.

Ein Beispiel, wie ein falscher Wert zurückgegeben werden kann

#define SOME_STR_FUNCTION_FAIL_RESULT (NULL)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      return SOME_STR_FUNCTION_FAIL_RESULT;
     }
   return result;
  }

void OnTick()
  {
   string someStr=SomeStrFunction();

   if(someStr==SOME_STR_FUNCTION_FAIL_RESULT)
     {
      Print("fail");
      return;
     }
  }

Allerdings kann dieser Ansatz zu Fehlern vom Programmierer führen. Wenn dieses Verhalten nicht dokumentiert ist, oder wenn der Programmierer nicht auf die Umsetzung des Codes oder auf die Dokumentation passt, dann wird er nichts über den möglichen fehlerhaften Wert wissen. Außerdem kann es zu Problemen kommen, wenn das Verfahren oder die Funktion im normalen Modus einen beliebigen Wert zurückgeben kann, darunter einen, der als fehlerhafter betrachtet ist.


Weisen Sie das Ergebnis einer speziellen globalen Variable. Oft wird dieser Ansatz auf Methoden oder Funktionen angewendet, die nicht alle Werte zurückgeben. Die Idee ist, dass in einer globalen Variable das Ergebnis dieser Methode oder Funktion gespeichert wird, und dann wird der Wert dieser Variable im aufrufenden Code überprüft. In MQL5 gibt es dafür eine standardmäßige Funktionalität (SetUserError()).

Ein Beispiel, wie der Fehlercode mit SetUserError() zugeordnet wird

#define SOME_STR_FUNCTION_FAIL_CODE (123)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE);
      return "";
     }
   return result;
  }

void OnTick()
  {
   ResetLastError();
   string someStr=SomeStrFunction();

   if(GetLastError()==ERR_USER_ERROR_FIRST+SOME_STR_FUNCTION_FAIL_CODE)
     {
      Print("fail");
      return;
     }
  }

In diesem Fall kann der Programmierer nicht über mögliche Fehler wissen, aber dieser Ansatz ermöglicht nicht nur die Tatsache von Fehlern zu berichten, sondern zeigt noch ihren spezifischen Code. Dies ist besonders wichtig, wenn die Fehlerursachen mehrere sind.


Bringen Sie das Ergebnis sowohl wie Bool, und auch den resultierenden Wert in Form einer Variable, die über den Link übergeben werden kann. Dieser Ansatz ist etwas besser als die beiden vorhergehenden, denn es erlaubt eine geringere Wahrscheinlichkeit des Fehlers vom Programmierer. Es ist schwer, nicht zu bemerken, dass die Methode oder Funktion seine Arbeit nicht richtig machen kann:

Ein Beispiel, wie das Ergebnis der Funktion wie bool zurückgegeben kann

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fail");
      return;
     }
  }

Wenn es eine Reihe von verschiedenen Fehlern auftreten kann, und wir müssen genau wissen, welcher Fehler stattgefunden hat, dann kann diese Variante mit dem vorherigen kombiniert werden. Sie können false zurückbringen, und der globalen Variable einen Fehlercode zuzuordnen.

Ein Beispiel, wie das Ergebnis der Funktion wie bool zurückgegeben werden kann und einem Fehlercode mit SetUserError() zugeordnet wird

#define SOME_STR_FUNCTION_FAIL_CODE_1 (123)
#define SOME_STR_FUNCTION_FAIL_CODE_2 (124)

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";

      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_1);

      return false;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_2);
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fail, code = "+(string)(GetLastError()-ERR_USER_ERROR_FIRST));
      return;
     }
  }

Allerdings ist diese Variante schwer zu verstehen (mit Codelesung) und bei fortwährender Unterstützung.


Bringen Sie das Ergebnis aus (enum), und auch den resultierenden Wert (wenn es vorgesehen ist) in Form einer Variable, die über den Link übergeben werden kann. Diese Variante ermöglicht bei Abbruch den Typ des Fehlers ohne Verwendung der globalen Variablen zurückzubringen, wenn ein paar Arten von möglichen Fehlern gibt. Es wird nur ein Wert auf der korrekten Ausführung entsprechen, und der Rest ist dann fehlerhaft.

Ein Beispiel, wie das Ergebnis der Funktion wie der Wert aus (enum) zurückgegeben werden kann

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

ENUM_SOME_STR_FUNCTION_RESULT SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_1;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_2;
     }

   value=resultValue;
   return SOME_STR_FUNCTION_SUCCES;
  }

void OnTick()
  {
   string someStr="";

   ENUM_SOME_STR_FUNCTION_RESULT result=SomeStrFunction(someStr);

   if(result!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fail, error = "+EnumToString(result));
      return;
     }
  }

Die Befreiung von der globalen Variablen ist ein sehr wichtiger Vorteil dieses Ansatzes, da sie unter inkompetenter oder unaufmerksamer Handhabung große Probleme verursachen können.


Bringen Sie das Ergebnis als ein Instanz einer Struktur, die aus einer booleschen Variable oder ENUM-Wert (ENUM) und dem resultierenden Wert besteht. Es ist eine Variante zum Thema der bisherigen Methode, die hier keine Notwendigkeit hat, für die Übertragung der Variablen nach einem Link. So ist es bevorzugt, enum zu benutzen, da dies die Leistung der Liste der möglichen Ergebnisse in der Zukunft erhöht.

Ein Beispiel für den Rückkehr des Ergebnisses von der Funktion als eine Instanz der Struktur, die aus den Werten (enum) und dem resultierenden Wert besteht

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

struct SomeStrFunctionResult
  {
   ENUM_SOME_STR_FUNCTION_RESULT code;
   char              value[255];
  };

SomeStrFunctionResult SomeStrFunction()
  {
   SomeStrFunctionResult result;

   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_1;
      return result;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_2;
      return result;
     }

   result.code=SOME_STR_FUNCTION_SUCCES;
   StringToCharArray(resultValue,result.value);
   return result;
  }

void OnTick()
  {
   SomeStrFunctionResult result=SomeStrFunction();

   if(result.code!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fail, error = "+EnumToString(result.code));
      return;
     }
   string someStr=CharArrayToString(result.value);
  }


Versuchen Sie, die Operation mehrmals auszuführen. Oft lohnt es sich, zu versuchen, eine Operation mehrmals durchzuführen, bevor sie erfolglos betrachtet wird. Zum Beispiel, wenn Sie die Datei nicht lesen können, da sie von einem anderen Prozess verwendet wird, können Sie es mit zunehmenden Zeitabständen mehrmals versuchen. Die Chancen stehen gut, dass ein anderer Prozess die Datei freigibt, und unsere Methode oder Funktion kann es aufrufen.

Das Beispiel der mehreren Versuchen, die Datei zu öffnen

string fileName="test.txt";
int fileHandle=INVALID_HANDLE;

for(int iTry=0; iTry<=10; iTry++)
  {
   fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);

   if(fileHandle!=INVALID_HANDLE)
     {
      break;
     }
   Sleep(iTry*200);
  }

Hinweis: Dieses Beispiel zeigt nur das Wesen des Ansatzes, in der praktischen Anwendung muss man die auftretenden Fehler analysieren. Zum Beispiel, wenn ein Fehler 5002 (Ungültiger Dateiname) oder 5003 (der Dateiname ist zu lang) auftritt, dann haben die nachfolgenden Versuche keinen Sinn. Darüber hinaus beachten Sie, dass dieser Ansatz nicht in den Systemen verwendet werden soll, wo eine Abschwächung der Gesamtleistung nicht erwünscht ist.


Benachrichtigen den Benutzer explizit. Der Benutzer sollte von einigen Fehlern explizit (Pop-up-Fenster, eine Inschrift auf dem Chart, etc.) informiert werden. Oft macht es Sinn, explizite Benachrichtigungen mit Aussetzung oder vollständigem Stopp des Programms zu kombinieren. Zum Beispiel, wenn am Konto nicht genug Geld ist, oder wenn der Benutzer einen eindeutigen falschen Wert der Eingangsparameter eingegeben hat, soll er darüber informiert werden.

Das Beispiel der Benachrichtigungen eines Benutzers über ungültige Eingabeparameter

input uint MAFastPeriod = 10;
input uint MASlowPeriod = 200;

int OnInit()
  {
//---
   if(MAFastPeriod>=MASlowPeriod)
     {
      Alert("Die Periode des schnellen gleitenden Mittelwerts muss kleiner sein als die Periode des langsamen gleitenden Mittelwerts!");
      return INIT_PARAMETERS_INCORRECT;
     }
//---
   return(INIT_SUCCEEDED);
  }

Natürlich gibt es auch andere Fehlerverarbeitungsmethoden, diese Liste zeigt nur die häufigsten von ihnen.


Allgemeine Empfehlungen für die Verarbeitung der Fehler

Wählen Sie ein angemessenes Maß der Fehlerverarbeitung. Zu unterschiedlichen Programmen gibt es unterschiedliche Anforderungen, bezüglich des Levels ihre Fehlerverarbeitungen. Wenn Sie einen kleinen Skript entwickeln, der nur ein paar Mal benutzt wird, wenn eine witzige Idee überprüft wird und wird nie zu einem Dritten übergeben, dann ist es möglich, keine Fehlerverarbeitung zu benutzen. Im Gegenteil, wenn es um das Projekt mit potentiellen Hunderttausenden Nutzern geht, wäre der Ansatz vernünftig, in dem alle denkbaren und undenkbaren Fehler verarbeitet werden. Versuchen Sie immer, den Grad der Fehlerverarbeitung zu verstehen, die in jedem konkreten Fall erforderlich ist.

Wählen Sie ein angemessenes Maß der Beteiligung des Nutzers. Für einige Fehler ist es erforderlich, die offensichtliche Teilnahme des Benutzers, für andere nicht: das Programm kann ganz unabhängig ohne Benachrichtigung des Benutzers weiter arbeiten. Wir müssen die goldene Mitte suchen: nicht dem Benutzer mit Nachrichten "überfluten", aber auch nicht "schweigen", wenn das Programm kritischen Situationen hat. Eine gute Lösung wäre der folgende Ansatz: eindeutig den Benutzer über kritische Fehler oder seine erforderlichen Teilnahme melden, und für alle anderen eine Log-Datei eingeben.

Versuchen Sie die Ergebnisse aller Funktionen und Methoden zu überprüfen, die sie zurückgegeben. Wenn eine Funktion oder Methode Werte zurückgeben kann, von denen auch fehlerhafte sein können, ist es am besten, sie zu überprüfen. Es sollte nicht die Gelegenheit vernachlässigt werden, die Programmqualität zu verbessern.

Soweit es möglich ist, überprüfen Sie bitte die Bedingungen, bevor die Operationen durchführt werden. Zum Beispiel überprüfen Sie, bevor es versucht wird, einen Trade zu öffnen:

  1. Ob Trading mit Robotern auf der Seite des Terminals erlaubt ist: TerminalInfoInteger (TERMINAL_TRADE_ALLOWED).
  2. Ob Trading mit Robotern für das Konto erlaubt ist: AccountInfoInteger (ACCOUNT_TRADE_EXPERT).
  3. Gibt es eine Verbindung zu dem Handelsserver: TerminalInfoInteger (TERMINAL_CONNECTED).
  4. ob die Parameter der Handelsoperation richtig sind: OrderCheck().

Achten Sie auf die Ausführungsregelmäßigkeit der verschiedenen Teile des Programms. Oft ist ein Beispiel eines Codes, wo nicht in Betracht die Häufigkeit der Anfragen zum Handelsserver zieht - ein Trailing Stop-Loss. Normalerweise ist der Aufruf einer solchen Funktion wird auf jedem Tick realisiert. Wenn es eine lange unidirektionale Bewegung ist, oder wenn beim Versuch, Transaktionen zu ändern, gibt es Fehler, dann kann diese Funktion eine Anfrage zur Modernisierung des Trades praktisch einmal im Tick senden (oder mehrere Anfragen für mehrere Transaktionen).

Wenn die Notierungen nicht sehr oft kommen, wird diese Funktion keine Problemen verursachen. Aber sonst sind große Schwierigkeiten möglich: Sehr häufige Anfragen zur Modernisierung der Trades können zum Deaktivieren des automatisierten Handels für dieses Konto führen und zu unangenehmen Gesprächen mit Kunden-Support. Der einfachste Weg - die Häufigkeit der Anfrage-Versuche zu Transaktionen zu begrenzen: eine frühere Anfrage zu speichern, und wenn es als XX Sekunden dauerte, dann keinen Versuch durchzuführen, das Trade anzufragen.

Ein Ausführungsbeispiel einer Funktion Trailing Stop-Loss nicht häufiger als einmal alle 30 Sekunden

const int TRAILING_STOP_LOSS_SECONDS_INTERVAL=30;

void TrailingStopLoss()
  {
   static datetime prevModificationTime=0;

   if((int)TimeCurrent() -(int)prevModificationTime<=TRAILING_STOP_LOSS_SECONDS_INTERVAL)
     {
      return;
     }

//--- Die Modifizierung Stop Loss
     {
      ...
      ...
      ...
      prevModificationTime=TimeCurrent();
     }
  }

Das gleiche Problem kann auftreten, wenn Sie versuchen, innerhalb einer kurzen Zeit zu viele Pending Orders zu platzieren, eine solche Erfahrung hat schon der Autor erlebt.


Das Ziel ist ein angemessenes Verhältnis von Stabilität und Korrektheit. In einem allgemeinen Fall muss man beim Schreiben des Programms ein Kompromiss zwischen der Stabilität und der Korrektheit des Codes suchen. Stabilität bedeutet, dass das Programm sogar bei Fehlern weiter arbeiten wird, auch wenn das zu leicht ungenauen Ergebnissen führt. Korrektheit erlaubt keine ungenauen Ergebnisse oder die Durchführung falscher Aktionen zurück zu haben. Sie müssen entweder genau sein oder vollständig abwesend, was bedeutet, es ist besser, das Programm zu beenden, als ungenaue Ergebnisse zurück zu haben, oder etwas anderes falsch zu machen.

Zum Beispiel, wenn ein Indikator irgendwas nicht berechnen kann, ist es besser, wenn er kein Signal gibt, als deswegen vollständig herunterzufahren. Im Gegenteil, wenn es um einen Trading-Roboter geht, ist es am besten, wenn seine Arbeit beendet, als er einen Trade mit hohem Volumen öffnet. Darüber hinaus kann ein Trading-Roboter, bevor er seine Arbeit beendet, dem Benutzer durch PUSH-Benachrichtigung informieren, damit der Benutzer über Probleme wissen könnte und auf sie schnell reagieren.


Zeigen Sie nützliche Informationen über Fehler. Versuchen Sie, Fehlermeldungen informativ zu machen. Es ist schlimm, wenn das Programm einen Fehler nicht zeigt - z.B «nicht in der Lage, ein Trade zu öffnen» - ohne weitere Erklärung. Es ist viel besser, eine bestimmte Nachricht zu haben, wie «nicht in der Lage, ein Trade zu öffnen: falsche Volumen der geöffneten Position (0,9999)». Es spielt keine Rolle, ob das Programm eine Fehlerinformation in einem Pop-up-Fenster oder in einer Protokolldatei zeigt. Auf jeden Fall sollte es für den Benutzer oder den Programmierer (vor allem in der Log-Datei-Analyse) ausreichend sein, um die Ursache des Fehlers zu verstehen und wenn es möglich ist, den Fehler zu korrigieren. Allerdings sollte der Benutzer mit Informationen nicht belastet werden: Es ist nicht notwendig, einen Code des Fehlers in einem Popup-Fenster zu zeigen, da der Benutzer nicht in der Lage ist, mit ihm viel zu tun.


Die Protokollierung mit MQL5

Die Log-Dateien werden in der Regel durch das Programm speziell für Programmierer erstellt, um die Suche nach Ausfall / Fehler-Gründen zu erleichtern und für die Einschätzung des Systemzustandes zu einem bestimmten Zeitpunkt usw. Außerdem kann die Protokollierung für die Software-Profilierung verwendet werden.


Die Ebene der Protokollierung

Die Nachrichten, die in den Log-Dateien kommen, tragen oft verschiedene Kritikalität und erfordern verschiedene Aufmerksamkeit. Um die Nachrichten mit verschiedenen Kritikalität voneinander zu unterscheiden, und auch den Kritikalitätsgrad der angezeigten Nachrichten einzustellen, werden die Ebene der Protokollierung verwendet. In der Regel werden mehrere Ebenen der Protokollierung realisiert:


Die Einführung der Log-Datei

Der einfachste Weg, die Log-Dateien mit MQL5 einzuführen, ist die Verwendung der Standardfunktion Print oder PrintFormat. Als Ergebnis werden alle Nachrichten ins allgemeinen Experten-Journal, der Indikatoren und Skripte des Terminals gesendet.

Das Beispiel für die Anzeige der Nachrichten im allgemeinen Experten-Journal mit der Funktion Print()

double VolumeCalculation()
  {
   double result=...;
   if(result<SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN))
     {
      Print("Das Volumen der Transaktionen (",DoubleToString(result,2),") war weniger als akzeptabel und wurde korrigiert "+DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN),2));
      result=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
     }
   return result;
  }

Dieser Ansatz hat mehrere Nachteile:

  1. Nachrichten von mehreren Programmen können in einem gemeinsamen "Haufen" abgeschoben werden, was ihre Analyse erschwert.
  2. Die Log-Datei im Hinblick auf leichter Verfügbarkeit kann versehentlich oder absichtlich von dem Benutzer gelöscht werden.
  3. Es ist ziemlich schwierig die Ebene der Protokollierung zu realisieren und zu konfigurieren.
  4. Sie können die Anzeige der Log-Meldungen nicht an eine andere Quelle umzuleiten (externe Datei, Datenbank, E-Mail, etc.).
  5. Es ist nicht möglich, eine obligatorische Rotation der Log-Dateien (Die Datei nach Datum und Uhrzeit oder nach einer bestimmten Größe zu ersetzten) zu realisieren.

Die Vorteile dieses Ansatzes:

  1. Sie brauchen nichts zu erfinden,es ist genug, die gleiche Funktion zu verwenden.
  2. In vielen Fällen können Sie die Log-Datei direkt im Terminal zu sehen, es ist nicht notwendig, sie getrennt zu suchen.

Die Realisierung des eigenen Logging-Mechanismus kann alle Nachteile von der Verwundung Print () und Printformat () beseitigen, aber wenn die Wiederverwendung des Code nötig wird, erfordert den Logging-Mechanismus an einem neuen Projekt zu übertragen (oder die Ablehnung der Verwundung im Code).

Als Beispiel für die Realisierung des Logging-Mechanismus in MQL5 kann man das folgende Szenario betrachten.

Ein Beispiel für die Realisierung des Logging-Mechanismus in MQL5

//+------------------------------------------------------------------+
//|                                                       logger.mqh |
//|                                   Copyright 2015, Sergey Eryomin |
//|                                             http://www.ensed.org |
//+------------------------------------------------------------------+
#property copyright "Sergey Eryomin"
#property link      "http://www.ensed.org"

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")
//--- Die maximale Anzahl der Dateien für den Modus " eine neue Log-Datei für jede neue 1MB"
#define MAX_LOG_FILE_COUNTER (100000) 
//--- Die Anzahl der Bytes in Megabyte
#define BYTES_IN_MEGABYTE (1048576)
//--- Die maximale Länge des Log-Datei-Namens
#define MAX_LOG_FILE_NAME_LENGTH (255)
//--- Die Ebene der Protokollierung
enum ENUM_LOG_LEVEL
  {
   LOG_LEVEL_DEBUG,
   LOG_LEVEL_INFO,
   LOG_LEVEL_WARNING,
   LOG_LEVEL_ERROR,
   LOG_LEVEL_FATAL
  };
//--- Die Methoden der Protokollierung
enum ENUM_LOGGING_METHOD
  {
   LOGGING_OUTPUT_METHOD_EXTERN_FILE,// die externe Datei
   LOGGING_OUTPUT_METHOD_PRINT // die Funktion Print
  };
//--- Die Benachrichtigungsmethoden
enum ENUM_NOTIFICATION_METHOD
  {
   NOTIFICATION_METHOD_NONE,// ausgemacht
   NOTIFICATION_METHOD_ALERT,// die Funktion Alert
   NOTIFICATION_METHOD_MAIL, // die Funktion SendMail
   NOTIFICATION_METHOD_PUSH // die Funktion SendNotification
  };
//--- Die Arten von Beschränkungen der Log-Dateien
enum ENUM_LOG_FILE_LIMIT_TYPE
  {
   LOG_FILE_LIMIT_TYPE_ONE_DAY,// Eine neue Log-Datei für jeden neuen Tag
   LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE // eine neue Log-Datei für jede neue 1MB
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CLogger
  {
public:
   //--- Hinzufügen Sie die Nachricht zum Protokoll
   //--- Hinweis:
   //--- Wenn der Anzeigemodus in eine externe Datei aktiviert ist, aber die Anzeige kann nicht
   //--- geliefert werden, wird die Meldung durch Print() angezeigt
   static void Add(const ENUM_LOG_LEVEL level,const string message)
     {
      if(level>=m_logLevel)
        {
         Write(level,message);
        }

      if(level>=m_notifyLevel)
        {
         Notify(level,message);
        }
     }
   //--- Die Eingabe der Protokollierungsebene
   static void SetLevels(const ENUM_LOG_LEVEL logLevel,const ENUM_LOG_LEVEL notifyLevel)
     {
      m_logLevel=logLevel;
      //--- Die Ausgabeebene der Nachrichten durch die Meldung sollte nicht unter der Ebene der Nachrichtenaufzeichnung in der Log-Datei sein
      m_notifyLevel=fmax(notifyLevel,m_logLevel);
     }
   //--- Die Eingabe der Protokollierungsmethode
   static void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod)
     {
      m_loggingMethod=loggingMethod;
     }
   //--- Die Eingabe der Benachrichtigungsmethode
   static void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod)
     {
      m_notificationMethod=notificationMethod;
     }
   //--- Die Eingabe des Log-Datei-Namens
   static void SetLogFileName(const string logFileName)
     {
      m_logFileName=logFileName;
     }
   //--- Die Eingabe der Art von Beschränkungen auf Log-Datei
   static void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType)
     {
      m_logFileLimitType=logFileLimitType;
     }

private:
   //--- Die Ebene der Protokollierung, es werden keine Meldungen in der Log-Datei / Journal gespeichert, die unter dieser Ebene sind 
   static ENUM_LOG_LEVEL m_logLevel;
   //--- Die Ebene der Protokollierung, es werden keine Meldungen als Benachrichtigungen angezeigt, die unter dieser Ebene sind 
   static ENUM_LOG_LEVEL m_notifyLevel;
   //--- Die Protokollierungsmethode
   static ENUM_LOGGING_METHOD m_loggingMethod;
   //--- Die Benachrichtigungsmethode
   static ENUM_NOTIFICATION_METHOD m_notificationMethod;
   //--- Der Log-Datei-Name
   static string     m_logFileName;
   //--- Die Arten von Beschränkungen der Log-Dateien
   static ENUM_LOG_FILE_LIMIT_TYPE m_logFileLimitType;
   //--- Das Erhaltungsergebnis des Dateinamens für das Protokoll           
   struct GettingFileLogNameResult
     {
                        GettingFileLogNameResult(void)
        {
         succes=false;
         ArrayInitialize(value,0);
        }
      bool              succes;
      char              value[MAX_LOG_FILE_NAME_LENGTH];
     };
   //--- Das Ergebnis der Größe-Überprüfung der vorhandenen Log-Datei
   enum ENUM_LOG_FILE_SIZE_CHECKING_RESULT
     {
      IS_LOG_FILE_LESS_ONE_MEGABYTE,
      IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE,
      LOG_FILE_SIZE_CHECKING_ERROR
     };
   //---Schreiben Sie den Eintrag in der Log-Datei
   static void Write(const ENUM_LOG_LEVEL level,const string message)
     {
      switch(m_loggingMethod)
        {
         case LOGGING_OUTPUT_METHOD_EXTERN_FILE:
           {
            GettingFileLogNameResult getLogFileNameResult=GetLogFileName();
            //---
            if(getLogFileNameResult.succes)
              {
               string fileName=CharArrayToString(getLogFileNameResult.value);
               //---
               if(WriteToFile(fileName,GetDebugLevelStr(level)+": "+message))
                 {
                  break;
                 }
              }
           }
         case LOGGING_OUTPUT_METHOD_PRINT:
            default:
              {
               Print(GetDebugLevelStr(level)+": "+message);
               break;
              }
        }
     }
   //--- Die Benachrichtigung durchführen
   static void Notify(const ENUM_LOG_LEVEL level,const string message)
     {
      if(m_notificationMethod==NOTIFICATION_METHOD_NONE)
        {
         return;
        }
      string fullMessage=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+message;
      //---
      switch(m_notificationMethod)
        {
         case NOTIFICATION_METHOD_MAIL:
           {
            if(TerminalInfoInteger(TERMINAL_EMAIL_ENABLED))
              {
               if(SendMail("Logger",fullMessage))
                 {
                  return;
                 }
              }
           }
         case NOTIFICATION_METHOD_PUSH:
           {
            if(TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
              {
               if(SendNotification(fullMessage))
                 {
                  return;
                 }
              }
           }
        }
      //---
      Alert(GetDebugLevelStr(level)+": "+message);
     }
   //--- Die Erhaltung des Log-Datei-Namens für den Eintrag
   static GettingFileLogNameResult GetLogFileName()
     {
      if(m_logFileName=="")
        {
         InitializeDefaultLogFileName();
        }
      //---
      switch(m_logFileLimitType)
        {
         case LOG_FILE_LIMIT_TYPE_ONE_DAY:
           {
            return GetLogFileNameOnOneDayLimit();
           }
         case LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE:
           {
            return GetLogFileNameOnOneMegabyteLimit();
           }
         default:
           {
            GettingFileLogNameResult failResult;
            failResult.succes=false;
            return failResult;
           }
        }
     }
   //--- Die Erhaltung des Log-Datei-Namens für den Beschränkungsfall "eine neue Log-Datei für jeden neuen Tag"
   static GettingFileLogNameResult GetLogFileNameOnOneDayLimit()
     {
      GettingFileLogNameResult result;
      string fileName=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+TimeToString(TimeLocal(),TIME_DATE);
      StringReplace(fileName,".","_");
      fileName=fileName+".log";
      result.succes=(StringToCharArray(fileName,result.value)==StringLen(fileName)+1);
      return result;
     }
   //--- Die Erhaltung des Log-Datei-Namens für den Fall "eine neue Log-Datei für jede neue 1MB" 
   static GettingFileLogNameResult GetLogFileNameOnOneMegabyteLimit()
     {
      GettingFileLogNameResult result;
      //---
      for(int i=0; i<MAX_LOG_FILE_COUNTER; i++)
        {
         ResetLastError();
         string fileNameToCheck=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+(string)i;
         StringReplace(fileNameToCheck,".","_");
         fileNameToCheck=fileNameToCheck+".log";
         ResetLastError();
         bool isExists=FileIsExist(fileNameToCheck);
         //---
         if(!isExists)
           {
            if(GetLastError()==5018)
              {
               continue;
              }
           }
         //---
         if(!isExists)
           {
            result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

            break;
           }
         else
           {
            ENUM_LOG_FILE_SIZE_CHECKING_RESULT checkLogFileSize=CheckLogFileSize(fileNameToCheck);

            if(checkLogFileSize==IS_LOG_FILE_LESS_ONE_MEGABYTE)
              {
               result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

               break;
              }
            else if(checkLogFileSize!=IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE)
              {
               break;
              }
           }
        }
      //---
      return result;
     }
   //---
   static ENUM_LOG_FILE_SIZE_CHECKING_RESULT CheckLogFileSize(const string fileNameToCheck)
     {
      int fileHandle=FileOpen(fileNameToCheck,FILE_TXT|FILE_READ);
      //---
      if(fileHandle==INVALID_HANDLE)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      ResetLastError();
      ulong fileSize=FileSize(fileHandle);
      FileClose(fileHandle);
      //---
      if(GetLastError()!=0)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      if(fileSize<BYTES_IN_MEGABYTE)
        {
         return IS_LOG_FILE_LESS_ONE_MEGABYTE;
        }
      else
        {
         return IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE;
        }
     }
   //--- Initialisieren Sie den Log-Datei-Namen standardmäßig
   static void InitializeDefaultLogFileName()
     {
      m_logFileName=MQLInfoString(MQL_PROGRAM_NAME);
      //---
#ifdef __MQL4__
      StringReplace(m_logFileName,".ex4","");
#endif

#ifdef __MQL5__
      StringReplace(m_logFileName,".ex5","");
#endif
     }
   //--- Schreiben Sie die Nachricht in der Datei
   static bool WriteToFile(const string fileName,
                           const string text)
     {
      ResetLastError();
      string fullText=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+text;
      int fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);
      bool result=true;
      //---
      if(fileHandle!=INVALID_HANDLE)
        {
         //--- Versuchen Sie, den Dateizeiger am Ende der Datei zu platzieren            
         if(!FileSeek(fileHandle,0,SEEK_END))
           {
            Print("Logger: FileSeek() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
            result=false;
           }
         //---  Versuchen Sie, den Text in der Datei zu schreiben
         if(result)
           {
            if(FileWrite(fileHandle,fullText)==0)
              {
               Print("Logger: FileWrite() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
               result=false;
              }
           }
         //---
         FileClose(fileHandle);
        }
      else
        {
         Print("Logger: FileOpen() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
         result=false;
        }
      //---
      return result;
     }
   //--- Die Erhaltung der aktuellen Zeit als Zeile
   static string GetPeriodStr()
     {
      ResetLastError();
      string periodStr=EnumToString(Period());
      if(GetLastError()!=0)
        {
         periodStr=(string)Period();
        }
      StringReplace(periodStr,"PERIOD_","");
      //---
      return periodStr;
     }
   //---
   static string GetDebugLevelStr(const ENUM_LOG_LEVEL level)
     {
      ResetLastError();
      string levelStr=EnumToString(level);
      //---
      if(GetLastError()!=0)
        {
         levelStr=(string)level;
        }
      StringReplace(levelStr,"LOG_LEVEL_","");
      //---
      return levelStr;
     }
  };
ENUM_LOG_LEVEL CLogger::m_logLevel=LOG_LEVEL_INFO;
ENUM_LOG_LEVEL CLogger::m_notifyLevel=LOG_LEVEL_FATAL;
ENUM_LOGGING_METHOD CLogger::m_loggingMethod=LOGGING_OUTPUT_METHOD_EXTERN_FILE;
ENUM_NOTIFICATION_METHOD CLogger::m_notificationMethod=NOTIFICATION_METHOD_ALERT;
string CLogger::m_logFileName="";
ENUM_LOG_FILE_LIMIT_TYPE CLogger::m_logFileLimitType=LOG_FILE_LIMIT_TYPE_ONE_DAY;
//+------------------------------------------------------------------+

Dieser Code kann in einer separaten hinzufügbaren Datei platziert werden, zum Beispiel Logger.mqh, und sie in <Das Datenverzeichnis des Terminals> / MQL5 / Include (Die Datei ist zu diesem Artikel hinzugefügt) speichern. Nachdem wird die Arbeit mit der Klasse CLogger ungefähr so aussehen:

Ein Beispiel für die Verwendung des Logging-Mechanismus

#include <Logger.mqh>

//--- Initialisieren Sie den Logger
void InitLogger()
  {
//--- Die Eingabe der Protokollierungsebene: 
//--- DEBUG - Die Ebene für die Meldungsaufnahme in der Log-Datei
//--- ERROR-Die Ebene für die Benachrichtigung
   CLogger::SetLevels(LOG_LEVEL_DEBUG,LOG_LEVEL_ERROR);
//--- Geben Sie die Art von der Benachrichtigung als PUSH-Benachrichtigung ein
   CLogger::SetNotificationMethod(NOTIFICATION_METHOD_PUSH);
//--- Die Eingabe der Protokollierungsmethode als Eintrag in der externen Datei
   CLogger::SetLoggingMethod(LOGGING_OUTPUT_METHOD_EXTERN_FILE);
//--- Die Eingabe des Log-Dateien-Namens
   CLogger::SetLogFileName("my_log");
//--- Die Eingabe der Art von Beschränkungen auf Log-Datei als "eine neue Log-Datei für jeden neuen Tag"
   CLogger::SetLogFileLimitType(LOG_FILE_LIMIT_TYPE_ONE_DAY);
  }

int OnInit()
  {
//---
   InitLogger();
//---
   CLogger::Add(LOG_LEVEL_INFO,"");
   CLogger::Add(LOG_LEVEL_INFO,"---------- OnInit() -----------");
   LOG(LOG_LEVEL_DEBUG,"Example of debug message");
   LOG(LOG_LEVEL_INFO,"Example of info message");
   LOG(LOG_LEVEL_WARNING,"Example of warning message");
   LOG(LOG_LEVEL_ERROR,"Example of error message");
   LOG(LOG_LEVEL_FATAL,"Example of fatal message");
//---
   return(INIT_SUCCEEDED);
  }

Zunächst wird in der Funktion InitLogger() alle möglichen Parameter des Loggers initialisiert, und dann werden Nachrichten in einer Log-Datei protokolliert. Das Ergebnis dieses Codes wird der Eintrag in der Log-Datei unter einem Namen wie «my_log_USDCAD_D1_2015_09_23.log» innerhalb von <Verzeichnis_Daten_Terminal>/MQL5/Files des folgenden Textes:

2015.09.23 09:02:10, USDCAD (D1), INFO: 
2015.09.23 09:02:10, USDCAD (D1), INFO: ---------- OnInit() -----------
2015.09.23 09:02:10, USDCAD (D1), DEBUG: Example of debug message (LoggerTest.mq5; int OnInit(); Line: 36)
2015.09.23 09:02:10, USDCAD (D1), INFO: Example of info message (LoggerTest.mq5; int OnInit(); Line: 38)
2015.09.23 09:02:10, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:02:10, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:02:10, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

Darüber hinaus werden durch PUSH-Benachrichtigungen die Nachrichten der Ebenen ERROR und FATAL gesendet.

Wenn Sie die Ebene der Nachrichten für das Schreiben in der Log-Datei als Warning (CLogger::SetLevels(LOG_LEVEL_WARNING,LOG_LEVEL_ERROR)) eingeben, dann wird die Ausgabe folgendermaßen aussehen:

2015.09.23 09:34:00, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:34:00, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:34:00, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

Das heißt, die Speicherung der Nachrichten unter der Ebene WARNING wird nicht durchgeführt.


Die öffentlichen Klassenmethoden CLogger und Makros LOG

Betrachten wir mehr die öffentlichen Klassenmethoden CLogger und Makros LOG.


Die Methode void SetLevels(const ENUM_LOG_LEVEL logLevel, const ENUM_LOG_LEVEL notifyLevel). Gibt die Ebene der Protokollierung ein.

const ENUM_LOG_LEVEL logLevel — Die Ebene der Protokollierung, es werden keine Meldungen in der Log-Datei / Journal gespeichert, die unter dieser Ebene sind. Standardmäßig = LOG_LEVEL_INFO.

const ENUM_LOG_LEVEL notifyLevel — Die Ebene der Protokollierung, es werden keine Meldungen als Benachrichtigungen angezeigt, die unter dieser Ebene sind. Standardmäßig = LOG_LEVEL_FATAL.

Mögliche Werte bei beiden:


Die Methode void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod). Gibt die Protokollierungsmethode ein.

const ENUM_LOGGING_METHOD loggingMethod — die Protokollierungsmethode. Standardmäßig = LOGGING_OUTPUT_METHOD_EXTERN_FILE.

Mögliche Werte:


Die Methode void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod). Gibt die Benachrichtigungsmethode ein.

const ENUM_NOTIFICATION_METHOD notificationMethod — die Benachrichtigungsmethode. Standardmäßig = NOTIFICATION_METHOD_ALERT.

Mögliche Werte:


Die Methode void SetLogFileName(const string logFileName). Die Eingabe des Log-Datei-Namens.

const string logFileName — Der Log-Datei-Name. Der Standardwert wird der Name des Programms sein, in dem der Logger verwendet wird (siehe die private Methode InitializeDefaultLogFileName()).


Die Methode void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType). Die Eingabe der Art von Beschränkungen auf Log-Datei.

const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType - Die Art von Beschränkungen auf Log-Date. Der standardmäßige Wert: LOG_FILE_LIMIT_TYPE_ONE_DAY.

Mögliche Werte:


Метод void Add(const ENUM_LOG_LEVEL level,const string message). Fügen wir die Nachricht in der Log-Datei.

const ENUM_LOG_LEVEL level — Die Ebene der Nachricht. Mögliche Werte:

const string message — Der Text der Meldung.


Neben der Methode Add wurde auch die LOG-Makros realisiert, das zum Text der Nachricht den Datei-Name hinzufügt, auch die Unterschrift der Funktion und die Zeilennummer, wo der Eintrag in der Log-Datei hinzufügt wird:

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")

Dieses Makro kann besonders nützlich bei Debuggen sein.

Somit wird im Beispiel der Protokollierungsmechanismus gezeigt, der ermöglicht:

  1. Konfigurierung der Ebene der Protokollierung (DEBUG..FATAL).
  2. Nach welcher Ebene der Meldungen der Benutzer benachrichtigt werden muss.
  3. Einstellen, wo das Log geschrieben werden muss — ins Experten-Journal durch Print() oder in einer externen Datei.
  4. Die Ausgabe in eine externe Datei - einen Datei-Namen eingeben und Einschränkungen für Log-Dateien eingeben: nach der Datei in jedem einzelnen Datum, oder nach der Datei für jede Megabyte der Log-Datei.
  5. Die Art der Benachrichtigung (Alert(), SendMail(), SendNotify()) eingeben.

Selbstverständlich ist die vorgeschlagene Variante nur ein Beispiel, und für bestimmte Aufgaben wird es die Verarbeitung (einschließlich auch die Beseitigung einer ungebrauchten Funktionalität) erfordern. Zum Beispiel, zusätzlich zu einem Eintrag in eine externe Datei und ins allgemeine Journal, kann man, eine Protokollierungsmethode, wie die Ausgabe in Datenbank hinzuzufügen.


Fazit

In diesem Artikel werden die Fragen bezüglich der Fehlerverarbeitung und Protokollierung mit MQL5 betrachtet. Die richtige Fehlerverarbeitung und relevante Protokollierung kann erheblich die Qualität der entwickelten Software verbessern und stark zukünftige Unterstützung vereinfachen.