English Русский 中文 Español 日本語 Português 한국어 Français Italiano Türkçe
Zur Fehlerbehebung von MQL5-Programmen (Debugging)

Zur Fehlerbehebung von MQL5-Programmen (Debugging)

MetaTrader 5Beispiele | 27 Juni 2016, 15:11
2 297 0
Mykola Demko
Mykola Demko

Einleitung

Dieser Artikel richtet sich primär an Programmierer, die die Sprache zwar bereits gelernt haben, die allerdings noch keine Meister ihres Fachs sind. Dabei geht der Artikel auf die wichtigste Elemente ein, mit denen sich Entwickler auseinandersetzen müssen, wenn sie ein Programm debuggen wollen. Was also verstehen wir unter Fehlerbehebung beziehungsweise debuggen?

Unter Debugging versteht man jene Stufe bei der Entwicklung eines Programms, bei der es darum geht, dessen Fehler ausfindig zu machen und zu beseitigen. Während des Debuggings analysiert ein Entwickler eine Applikation, indem er versucht, potentielle Fehler und Fehlerquellen zu ermitteln. Die zu analysierenden Daten erhält man durch eine Beobachtung der Variablen und der Programmoperationen (welche Funktionen wann aufgerufen werden).

Es existieren zwei komplementäre Debugging-Technologien:

  • Das Debugger-Dienstprogramm, das jeden einzelnen Schritt des zu entwickelten Programms anzeigt.
  • Eine interaktive Anzeige für den Status von Variablen und Funktionsaufrufen auf einem Bildschirm, im Journal oder in einer Datei.

Lassen Sie uns annehmen, Sie kennten bereits MQL5, einschließlich etwaiger Variablen, Strukturen usw., hätten allerdings noch nie ein eigenes Programm entwickelt. Die erste Operation, die Sie nun durchführen, ist das Kompilieren. Tatsächlich handelt es sich an dieser Stelle um die erste Phase des Debuggings.


1. Kompilieren

Unter dem Kompilieren versteht man das Übersetzen eines Quellcodes einer höheren Programmiersprache in den einer weniger komplexen.

Der MetaEditor Compiler übersetzt Programme in einen Bytecode, nicht in einen Native Code (folgen Sie dem Link für mehr Details). Dies erlaubt die Entwicklung von Verschlüsselungsprogrammen. Ferner kann ein Bytecode auf 32- wie auch auf 64-Bit-Systemen ausgeführt werden.

Aber lassen Sie uns zum Kompilieren, der ersten Stufe des Debuggings, zurückkommen. Nach dem Drücken von F7 (oder dem Kompilieren-Button) wird MetaEditor 5 Sie mit einem Fehlerreport hinsichtlich aller Fehler versorgen, die Sie während des Codeschreibens gemacht haben. Der „Fehler“-Tab des „Toolbox“-Fensters zeigt Ihnen detailliertere Beschreibungen zu den gefundenen Fehlern an. Markieren Sie eine Beschreibung mit dem Cursor und drücken Sie die Eingabetaste, um direkt zum Fehler zu springen.

Der Compiler zeigt lediglich zwei Fehlertypen an:

  • Syntaxfehler (in rot angezeigt) - Ein Quellcode kann erst dann kompiliert werden, wenn alle Syntaxfehler behoben wurden.
  • Warnungen (in gelb angezeigt) - Obwohl ein Quellcode kompiliert werden könnte, wird empfohlen, diese Fehler zu beseitigen.

Syntaxfehler entstehen oft durch bloße Unachtsamkeit. So werden beispielsweise bei der Bezeichnung von Variablen „,“ und „;“ miteinander verwechselt:

int a; b; // incorrect declaration

In solch einem Fall wird der Compiler eine Fehlermeldung ausgeben. Eine korrekte Bezeichnung sieht entweder folgendermaßen aus:

int a, b; // correct declaration

Oder auch so:

int a; int b; // correct declaration

Warnungen sollten grundsätzlich nicht ignoriert werden (viele Entwickler gehen einfach zu leichtsinnig mit ihnen um). Falls MetaEditor 5 während einer Kompilierung Warnungen ausgibt, so wird zwar ein Programm erstellt werden, allerdings kann nicht ausgeschlossen werden, dass es nicht wie geplant funktioniert.

Warnungen stellen nur die Spitze des Eisbergs dar; die Entwickler von MQL5 haben viele Anstrengungen unternommen, um die typischsten Tippfehler von Programmierern zu systematisieren.

Stellen wir uns vor, dass Sie zwei Variablen vergleichen wollen.

if(a==b) { } // if a is equal to b, then ...

Aufgrund eines Tippfehlers oder aus Gründen der Vergesslichkeit schreiben Sie allerdings „=“ anstelle von „==“. In diesem Fall fasst der Compiler den Code wie folgt auf:

if(a=b) { } // assign b to a, if a is true, then ... (unlike MQL4, it is applicable in MQL5)

Wie wir sehen können, hat dieser kleine Fehler einen gewaltigen Einfluss auf die Programmoperationen. Folglich gibt der Compiler für diese Codezeile eine entsprechende Warnung aus.

Fassen wir zusammen: Kompilieren ist die erste Stufe des Debuggings. Die Warnungen eines Compilers sollten nicht ignoriert werden.

Abb. 1. Debugging von Daten während eines Kompilierungsvorgangs

Abb. 1. Debugging von Daten während eines Kompilierungsvorgangs


2. Debugger

Die zweite Debugging-Phase besteht darin, einen sogenannter Debugger (Hotkey: F5) zu verwenden. Der Debugger führt Ihr Programm in einer Art Emulationsmodus aus - Schritt für Schritt. Der MetaEditor 5 Debugger ist ein neues Feature, das in MetaEditor 4 so noch nicht zur Verfügung stand. Daher haben selbst Programmierer, die bereits mit MQL 4 gearbeitet haben, noch keine Erfahrungen mit diesem mächtigen Werkzeug sammeln können.

Das Debugger-Inferface verfügt über drei Haupt- und drei Hilfsbuttons.

  • Start [F5] - Beginnt die Fehlerbehebung.
  • Pause [Pause/Untbr] - Pausiert die Fehlerbehebung.
  • Stopp [Shift+F5] - Stoppt die Fehlerbehebung.
  • Springen [F11] - Der Benutzer springt zur Funktion, die in dieser Linie aufgerufen wird.
  • Ignorieren [F10] - Der Debugger ignoriert den Körper der aufgerufenen Funktion dieses Strings und springt zur nächsten Zeile.
  • Verlassen [Shift+F11] - Der Benutzer verlässt den Körper der Funktion, in dem er sich gerade befindet.

Dies ist das Debugger-Interface. Aber wie verwenden wir es? Das Debugging eines Programms beginnt ab der Zeile, an der der Programmierer die spezielle Debugging-Funktion DebugBreak() platziert hat, oder von einem Haltepunkte (Breakpoint) an, der via F9-Button bzw. mittels des Klicks eines speziellen Symbolleistenbuttons festgelegt werden kann.

Abb. 2 Setzen zusätzlicher Haltepunkte (Breakpoints)

Abb. 2 Setzen von Haltepunkten (Breakpoints)

Ohne Haltepunkte wird der Debugger ganz einfach das Programm ausführen und berichten, dass das Debugging erfolgreich gewesen sei, ohne dass Sie jedoch etwas sehen werden. Durch DebugBreak können Sie Teile des Codes, die Sie nicht interessieren, überspringen und somit eine Überprüfung auf diejenigen Zeilen fokussieren, die Sie für verdächtig halten.

Wir haben also den Debugger ausgeführt, DebugBreak an den richtigen Platz verfrachtet und sind jetzt dabei, die Programmausführung zu untersuchen. Was kommt als Nächstes? Wie kann es uns dabei helfen, zu verstehen, was mit dem Programm passiert?

Schauen Sie bitte zunächst einmal an den linken Rand des Debugger-Fensters. Dort sehen Sie den Namen der Funktion sowie die Nummer der Zeile, in der Sie sich soeben befinden. Sehen Sie nun auf die rechte Seite des Fensters. Obgleich es leer ist, können Sie in das Ausdrucksfeld eine jede Variable eintragen, die Ihnen beliebt. Geben Sie den Namen der Variable ein, um deren aktuellen Wert im Wertfeld einzusehen.

Wie Sie unten sehen, kann die Variable außerdem durch Drücken des Tastaturkürzels [Shift+F9] (oder via Kontextmenü) ausgewählt bzw. hinzugefügt werden:

Abb. 3 Variablenansicht während des Debuggings hinzufügen

Abb. 3 Variablenansicht während des Debuggings hinzufügen

Sie können also zu der Codezeile springen, auf der Sie sich gerade befinden, und sich die Werte der wichtigen Variablen anzeigen lassen. Indem Sie all dies analysieren, werden Sie schon mitbekommen, ob das Programm korrekt funktioniert.

Es gibt keinen Grund für die Sorge, dass die Variable, für die Sie sich interessieren, eventuell bereits lokal deklariert wurde, während Sie noch nicht die Funktion erreicht haben, in der sie deklariert wird. Wenn Sie sich außerhalb des Variablenbereichs aufhalten sollten, wir diese stets den Wert „Unbekannter Identifikator“ besitzen. Das bedeutet, dass die Variable noch nicht deklariert wurde. Dies führt nicht zum Auftreten eines Debugger-Fehlers. Nachdem Sie den Variablenbereich erreicht haben, werden Sie den entsprechenden Wert und Typ sehen.

Abb. 4 Debugging-Vorgang: Die Werte von Variablen ansehen

Abb. 4 Debugging-Vorgang Die Werte von Variablen ansehen

Dies sind die primären Debugger-Features. Der Testerabschnitt zeigt Ihnen an, was mit dem Debugger nicht getan werden kann.


3. Profiler

Der Code-Profiler ist eine nützliche Ergänzung zum Debugger. Tatsächlich handelt es sich um die letzte Phase des Debuggings des Programms - der Optimierung.

Der Profiler wird im MetaEditor 5-Menü mithilfe des „Profiler Starten“-Buttons aufgerufen. Im Gegensatz zur Schritt-für-Schritt-Analyse des Debuggers, führt der Profiler das Programm aus. Fall es sich bei einem Programm um einen Indikator oder einen Expert Advisor handelt, wird der Profiler seine Arbeit verrichten, bis das Programm aus dem Speicher entfernt wird (unload). Dieser Vorgang kann zum Beispiel durch die Entfernung eines Indikators oder eines Expert Advisors von einem Chart durchgeführt werden. Alternativ genügt ein Klick auf „Profiler Stoppen“.

Ein Profiler versorgt uns mit vielen nützlichen Statistiken: Wie oft wurde eine gewisse Funktion aufgerufen? Wie viel Zeit hat ihre Ausführung in Anspruch genommen. Sie werden möglicherweise etwas verwirrt über die Statistiken in Prozentangabe sein. Es ist hierbei wichtig, zu verstehen, dass Statistiken keine verschachtelten Funktionen miteinbeziehen. Daher wird die Summe aller prozentualen Werte 100% bei weitem übersteigen.

Dennoch ist und bleibt der Profiler ein mächtiges Werkzeug, um Programme zu optimieren und um Benutzern anzuzeigen, welche Funktion aus Geschwindigkeitsgründen optimiert werden sollte und wie man Speicher spart.

Abb. 5 Resultate der Operationen des Profilers

Abb. 5 Resultate der Operationen des Profilers


4. Interaktivität

Wie dem auch sei, ich glaube, dass Nachrichtenanzeigefunktionen wie Print und Comment die hauptsächlichen Debugging-Werkzeuge darstellen. Zunächst einmal sind sie sehr leicht zu bedienen. Zum Zweiten kennen Programmierer, die von der früheren Version von MQL5 zu eben dieser Version gewechselt haben, sie bereits.

Die „Print“-Funktion sendet den übergebenen Parameter in Form eines Textstrings in Richtung der Logdatei und des „Experts“-Tool-Tab. Die Zeit der Übersendung wie auch der Name des Programms, das die Funktion aufgerufen hat, werden auf der linken Seiten des Texts angezeigt. Während des Debuggings, wird dies Funktion dafür genutzt, zu bestimmen, welche Werte in den Variablen enthalten sind.

Neben den Werten der Variablen ist es manchmal ebenso notwendig, die Sequenz der Aufrufe dieser Funktionen zu kennen. Hierfür eigenen sich zum Beispiel die Makros „__FUNCTION__" und „__FUNCSIG__“. Das erste Makro gibt einen String mit dem Namen der Funktion aus, die den Aufruf getätigt hat, wohingegen das zweite zusätzlich eine Liste der Parameter der aufgerufenen Funktion anzeigt.

Unten sehen Sie die beiden Makros in Aktion:

//+------------------------------------------------------------------+
//| Example of displaying data for debugging                             |
//+------------------------------------------------------------------+
void myfunc(int a)
  {
   Print(__FUNCSIG__); // display data for debugging 
//--- here is some code of the function itself
  }

Ich ziehe im Übrigen die Verwendung des Makros „__FUNCSIG__" vor, da es den Unterschied zwischen überladenen Funktionen anzeigt (identischer Name bei unterschiedlichen Parametern).

Es ist nicht selten notwendig, einige Aufrufe zu überspringen beziehungsweise sich auf einige spezifische Aufrufe zu fokussieren. Zu diesem Zwecke kann die Print-Funktion durch eine Bedingung geschützt werden. Zum Beispiel: Print kann nur nach der 1013. Iteration aufgerufen werden.

//+------------------------------------------------------------------+
//| Example of data output for debugging                             |
//+------------------------------------------------------------------+
void myfunc(int a)
  {
//--- declare the static counter
   static int cnt=0;
//--- condition for the function call
   if(cnt==1013)
      Print(__FUNCSIG__," a=",a); // data output for debugging
//--- increment the counter
   cnt++;
//--- here is some code of the function itself
  }

Das selbe kann mit der Comment-Funktion durchgeführt werden, die Kommentare in der oberen, linken Ecke des Charts anzeigt. Dies ist unglaublich praktisch, da Sie so während des Debuggens nicht die Ansicht wechseln müssen. Allerdings, falls Sie diese Funktion verwenden, löscht jeder neue Kommentar den jeweils älteren. Obgleich dieser Umstand manchmal durchaus positiv sein kann, würde ich ihn meist für nachteilhaft befinden.

Um diesen Nachteil zu beseitigen, kann ein zusätzlicher String auf die Variable geschrieben werden. Zuerst wird eine Variable des Types String deklariert (meistens: global) und mit einem leeren Wert initialisiert. Dann wird jeder neue String an den Anfang gestellt und mit einem Zeilenvorschubzeichen versehen, während der vorherige Wert der Variable zum Ende hinzuaddiert wird.

string com=""; // declare the global variable for storing debugging data
//+------------------------------------------------------------------+
//| Example of data output for debugging                             |
//+------------------------------------------------------------------+
void myfunc(int a)
  {
//--- declare the static counter
   static int cnt=0;
//--- storing debugging data in the global variable
   com=(__FUNCSIG__+" cnt="+(string)cnt+"\n")+com;
   Comment(com); // вывод информации для отладки
//--- increase the counter
   cnt++;
//--- here is some code of the function itself
  }

An dieser Stelle erhalten wir nun eine neue Möglichkeit, uns die Details des Programms ein wenig genauer anzusehen - das Print-To-File-Prinzip. Print- und Comment-Funktionen sind jedoch nicht immer für große Datenvolumen oder Hochgeschwindigkeits-Printing-Prozesse geeignet. Erstgenannte hat nicht immer ausreichend Zeit, um die Änderungen anzuzeigen (da Aufrufe vor dem Anzeigen bereits durchgeführt werden können, was zur Konfusion führen mag). Letztgenannte operiert einfach zu langsam. Ferner können Comment-Funktionen nicht erneut gelesen und auch nicht untersucht werden.

Printing-To-File ist dabei die komfortabelste Methode der Datenausgabe, falls Sie die Sequenz von Aufrufen oder Logs - die eine große Menge an Daten umfassen - prüfen müssen Gleichwohl sollte stets bedacht werden, dass Print nicht bei jeder Iteration, sondern nur am Ende einer Datei verwendet wird, wohingegen die Datenspeicherung in String-Variablen während jeder Iteration stattfindet - entsprechend dem oben erwähnten Prinzip (der Unterschied besteht darin, dass neue Daten zusätzlich am Ende der Variable geschrieben werden).

string com=""; // declare the global variable for storing debugging data
//+------------------------------------------------------------------+
//| Program shutdown                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- saving data to the file when closing the program
   WriteFile();
  }
//+------------------------------------------------------------------+
//| Example of data output for debugging                             |
//+------------------------------------------------------------------+
void myfunc(int a)
  {
//--- declare the static counter
   static int cnt=0;
//--- storing debugging data in the global variable
   com+=__FUNCSIG__+" cnt="+(string)cnt+"\n";
//--- increment the counter
   cnt++;
//--- here is some code of the function itself
  }
//+------------------------------------------------------------------+
//| Save data to file                                                |
//+------------------------------------------------------------------+
void WriteFile(string name="Отладка")
  {
//--- open the file
   ResetLastError();
   int han=FileOpen(name+".txt",FILE_WRITE|FILE_TXT|FILE_ANSI," ");
//--- check if the file has been opened
   if(han!=INVALID_HANDLE)
     {
      FileWrite(han,com); // печать данных
      FileClose(han);     // закрытие файла
     }
   else
      Print("File open failed "+name+".txt, error",GetLastError());
  }

Die Funktion WriteFile wird in OnDeinit aufgerufen. Folglich werden alle Änderungen des Programms in die Datei geschrieben.

Hinweise: Falls Ihr Log zu lang sein sollte, wäre es definitiv ratsam, es in mehreren Variablen zu speichern. Der beste Weg dies zu tun, besteht darin, die Inhalte der Textvariable als Stringtyp im Cell Array und in der Zero-Out-Com-Variable zu platzieren (als Vobereitung für die nächste Arbeitsphase).

Dies sollte in etwa nach 1-2 Millionen Strings erfolgen (keine wiederkehrende Einträge). Erstens wird hierdurch Datenverlust durch Variablenüberlauf vermieden (ich war im Übrigen trotz meiner Anstrengungen nicht in der Lage, dies zu tun). Zweitens, und möglicherweise noch wichtiger, erhalten Sie so die Möglichkeit, sich Daten in verschiedenen Dateien anzeigen zu lassen, anstatt ein jedes Mal eine riesige Datei im Editor öffnen zu müssen.

Um nicht andauernd die Menge an gespeicherten Strings verfolgen zu müssen, können Sie sich der Funktionstrennung bedienen, um mit Dateien in drei verschiedenen Schritten zu interagieren. Der erste Schritt besteht darin, die Datei zu öffnen. Zweitens wird die Datei entsprechend der Iteration beschrieben. Drittens wird die Datei geschlossen.

//--- open the file
int han=FileOpen("Debugging.txt",FILE_WRITE|FILE_TXT|FILE_ANSI," ");
//--- print data
if(han!=INVALID_HANDLE) FileWrite(han,com);
if(han!=INVALID_HANDLE) FileWrite(han,com);
if(han!=INVALID_HANDLE) FileWrite(han,com);
if(han!=INVALID_HANDLE) FileWrite(han,com);
//--- close the file
if(han!=INVALID_HANDLE) FileClose(han);

Allerdings sollten Sie diese Methode mit Vorsicht benutzen. Falls die Ausführung Ihres Programms nicht korrekt funktioniert (Beispiel: es wird durch 0 geteilt), erhalten Sie womöglich eine nicht mehr zu öffnende Datei, die Ihr Betriebssystem beeinträchtigen kann.

Ferner empfehle ich nicht, die Open-Write-Close-Loop bei jeder einzelnen Iteration zu verwenden. Meine persönliche Erfahrung zeigt, dass Ihre Festplatte in diesem Fall nach spätestens 2-3 Monaten den Geist aufgeben wird.


5. Tester

Während des Debuggings eines Expert Advisors müssen Sie normalerweise die Aktivierung ganz bestimmter Bedingungen im Auge behalten. Allerdings führt der oben erwähnte Debugger einen EA nur in Echtzeit aus - und Sie müssen relativ lange darauf warten, bis diese Bedingungen endlich aktiviert werden.

Tatsächlich können ganz spezifische Bedingungen eine ganze Zeit lang brauchen, bis Sie auftreten. Obwohl wir wissen, dass sie irgendwann eintreten werden, kann die Wartezeit mehrere Monate betragen - es wäre unsinnig darauf zu warten. Was also tun wir?

In diesem Fall kann uns der Strategietester weiterhelfen. Dieselben Print- und Comment-Funktionen werden auch fürs Debugging verwendet. Die Comment-Funktion ist besser beim Beurteilen einer Situation. Auf der anderen Seite verspricht die Print-Funktion eine wesentlich detailliertere Analyse. Tester speichern angezeigte Daten in Testerlogs (separate Pfade für jeden Tester).

Um einen Expert Advisor im richtigen Intervall zu starten, muss ich die Zeit, in der meiner Meinung nach Fehler auftreten, ausfindig machen, das entsprechende Datum im Tester einstellen und ihn im Visualisierungsmodus (alle Ticks) starten.

Ich habe mir diese Debugging-Methode von MetaTrader 4 abgeschaut - damals noch die einzige Methode, mit der man ein Programm während dessen Ausführung debuggen konnte.

Abb. 6 Debugging via Strategietester

Abb. 6 Debugging via Strategietester


6. Objektorientiertes Programmieren & Debugging

Objektorientiertes Programmieren, das erstmals in MQL5 angewendet wird, hat seine Spuren hinsichtlich des Debugging-Prozesses hinterlassen. Wenn Sie Prozeduren debuggen, können Sie im Programm mithilfe der bloßen Verwendung von Funktionsnamen navigieren. Wenn wir allerdings von OOP sprechen, ist es oft notwendig, die verschiedenen Methoden zu kennen, mit denen ein Objekt aufgerufen werden kann. Dies gilt vor allem für Objekte, die vertikal designet sind (unter Verwendung von Vererbung [Inheritance]). In solch einem Fall helfen uns die in MQL5 neu eingeführten Templates.

Die Template-Funktion erlaubt es uns, den Pointer-Typ als Wert eines String-Typs zu erhalten.

template<typename T> string GetTypeName(const T &t) { return(typename(T)); }

Ich mache mir diese Eigenschaft beim Debugging folgendermaßen zunutze:

//+------------------------------------------------------------------+
//| Base class contains the variable for storing the type             |
//+------------------------------------------------------------------+
class CFirst
  {
public:
   string            m_typename; // variable for storing the type
   //--- filling the variable by the custom type in the constructor
                     CFirst(void) { m_typename=GetTypeName(this); }
                    ~CFirst(void) { }
  };
//+------------------------------------------------------------------+
//| Derived class changes the value of the base class variable  |
//+------------------------------------------------------------------+
class CSecond : public CFirst
  {
public:
   //--- filling the variable by the custom type in the constructor
                     CSecond(void) { m_typename=GetTypeName(this); }
                    ~CSecond(void) {  }
  };

Die Basisklasse enthält die Variable, um ihren Typ zu speichern (die Variable wird im Konstruktor eines jeden Objekts initialisiert). Eine abgeleitete Klasse benutzt ebenfalls den Wert dieser Variable, um ihren Typ zu speichern. Wenn nun also das Makro aufgerufen wird, füge ich einfach m_typename hinzu, wodurch die Variable nicht nur den Namen der aufgerufenen Funktion, sondern ebenso den Typ des Objekts erhält, der die Funktion aufgerufen hat.

Der Pointer selbst kann abgeleitet werden, um Objekte besser zu erkennen, wodurch Benutzer besser zwischen Objekten differenzieren können. Im Innern des Objekt funktioniert dies wie folgt:

Print((string)this); // print pointer number inside the class

Außerhalb sieht die Sache so aus:

Print((string)GetPointer(pointer)); // print pointer number outside the class

Außerdem kann die Variable zum Speichern von Objektnamen innerhalb einer jeden Klasse verwendet werden. In solch einem Fall ist es möglich, den Objektnamen als einen Konstruktorparameter weiterzugeben, wenn man ein Objekt kreiert. Dies erlaubt es Ihnen nicht nur, Objekte entsprechend zu trennen, sondern ebenso, zu verstehen, wofür ein jedes Objekt steht (da Sie sie benennen), Diese Methode kann ähnlich dem Füllen von m_typename-Variablen realisiert werden.


7. Ablaufverfolgung

Alle bisher erwähnten Methoden sind fürs Debugging sehr wichtig und ergänzen sich untereinander. Allerdings gibt es noch eine weitere, wenngleich weniger populäre Methode - die Ablaufverfolgung.

Diese Methode wird vor allem aufgrund ihrer Komplexität nur sehr selten benutzt. Falls Sie nicht weiterkommen und keine Ahnung haben, was Sie noch tun könnten, so ist eine Ablaufverfolgung möglicherweise genau das Richtige für Sie.

Diese Methode erlaubt es Ihnen, die Struktur der App ein wenig besser zu verstehen - Sequenzen und die Objekte von Aufrufen. Mithilfe der Ablaufverfolgung, auch Tracing genannt, werden Sie verstehen, was mit dem Programm nicht stimmt. Außerdem gewährt uns diese Methode einen sehr guten Überblick über das Projekt.

Tracing funktioniert folgendermaßen: Legen Sie zwei Makros an:

//--- opening substitution  
#define zx Print(__FUNCSIG__+"{");
//--- closing substitution
#define xz Print("};");

Hierbei handelt es sich um Opening- (zx) als auch Closing-Makros (xz). Lassen Sie sie uns in den Funktionskörpern platzieren, die wir zu verfolgen gedenken.

//+------------------------------------------------------------------+
//| Example of function tracing                                       |
//+------------------------------------------------------------------+
void myfunc(int a,int b)
  {
   zx
//--- here is some code of the function itself
   if(a!=b) { xz return; } // exit in the middle of the function
//--- here is some code of the function itself
   xz return;
  }

Falls die Funktion entsprechend den Bedingungen ein Exit aufweist, so sollte in den geschützten Abschnitten xz (Closing) eingestellt werden. Dies gewährt unserer Tracing-Struktur einen gewissen Schutz gegen Störungen.

Nehmen Sie bitte zur Kenntnis, dass das oben erwähnte Makro dazu benutzt worden ist, dieses Beispiel ein wenig zu simplifizieren. Normalerweise wäre es besser, die „Print to file“-Einstellung zur Ablaufverfolgung zu benutzen. Nebenbei, ich persönlich verwende einen kleinen Trick, wenn ich mich dieser Option bediene. Um die komplette Tracing-Struktur zu sehen, verpacke ich die Namen von Funktionen mithilfe folgender syntaktischen Konstruktion:

if() {...}

Die letztendliche Datei besitzt die Erweiterung „.mqh“, die es uns erlaubt, sie im MetaEditor mittels styler [Strg+,] zu öffnen, wodurch die Tracing-Struktur sichtbar wird.

Der vollständige Tracing-Code kann unten eingesehen werden:

string com=""; // declare global variable for storing debugging data
//--- opening substitution
#define zx com+="if("+__FUNCSIG__+"){\n";
//--- closing substitution
#define xz com+="};\n"; 
//+------------------------------------------------------------------+
//| Program shutdown                                      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- //--- saving data to the file when closing the program
   WriteFile();
  }
//+------------------------------------------------------------------+
//| Example of the function tracing                                       |
//+------------------------------------------------------------------+
void myfunc(int a,int b)
  {
   zx
//--- here is some code of the function itself
   if(a!=b) { xz return; } // exit in the middle of the function
//--- here is some code of the function itself
   xz return;
  }
//+------------------------------------------------------------------+
//| Save data to file                                              |
//+------------------------------------------------------------------+
void WriteFile(string name="Tracing")
  {
//--- open the file
   ResetLastError();
   int han=FileOpen(name+".mqh",FILE_WRITE|FILE_TXT|FILE_ANSI," ");
//--- check if the file has opened
   if(han!=INVALID_HANDLE)
     {
      FileWrite(han,com); // print data
      FileClose(han);     // close the file
     }
   else
      Print("File open failed "+name+".mqh, error",GetLastError());
  }

Um den Tracing-Vorgang von einem spezifischen Ort aus zu starten, sollten Makros durch Bedingungen unterstützt werden:

bool trace=0; // variable for protecting tracing by condition
//--- opening substitution
#define zx if(trace) com+="if("+__FUNCSIG__+"){\n";
//--- closing substitution
#define xz if(trace) com+="};\n";

In diesem Fall ist es Ihnen möglich, die Tracing zu aktivieren bzw. zu deaktivieren, nachdem Sie die Einstellung der „Trace“-Variable nach einem bestimmten Ereignis auf „wahr“ oder „falsch“ gesetzt haben.

Falls Tracing nicht notwendig sein sollte (obwohl es später allerdings notwendig werden könnte), oder wenn nicht genug Zeit zur Säuberung der Quelle zur Verfügung steht, kann es via Änderung der Makrowerte zu leeren Werten deaktiviert werden:

//--- substitute empty values
#define zx
#define xz

Unten finden Sie die Datei des Standard-EAs samt entsprechender Tracing-Änderungen: Die Tracing-Resultate finden Sie im Ordner-Verzeichnis, direkt nachdem Sie den EA auf den Chart angewendet bzw. gestartet haben - hierbei wird tracing.mqh angelegt. Und hier haben Sie die entsprechende Passage des Textes der resultierenden Datei:

if(int OnInit()){
};
if(void OnTick()){
if(void CheckForOpen()){
};
};
if(void OnTick()){
if(void CheckForOpen()){
};
};
if(void OnTick()){
if(void CheckForOpen()){
};
};
//--- ...

Beachten Sie, dass die Struktur der verschachtelten Aufrufe in der neu kreierten Datei zu Anfang nicht klar definiert werden kann. Allerdings können Sie die gesamte Struktur sehen, nachdem Sie den Code-Styler verwendet haben. Unten finden Sie die resultierende Datei nach der Verwendung des Stylers:

if(int OnInit())
  {
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
if(void OnTick())
  {
   if(void CheckForOpen())
     {
     };
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
if(void OnTick())
  {
   if(void CheckForOpen())
     {
     };
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
if(void OnTick())
  {
   if(void CheckForOpen())
     {
     };
  };
//--- ...

Dies ist lediglich ein kleiner Trick meinerseits und keine Beschreibung dessen, wie Tracing normalerweise durchgeführt werden sollte. Jedem steht es frei, seine ganz eigenen Tracing-Strategie zu entwickeln. Wichtig ist hierbei nur, dass Tracing die Struktur von Funktionsaufrufen aufdeckt.


Wichtige Hinweise zum Debugging

Wenn Sie an Ihrem Code während des Debuggings irgendwelche Änderungen vornehmen, sollten die Aufrufe Ihrer MQL5-Funktionen grundsätzlich via Wrapping geschehen. Unten sehen Sie, wie das funktioniert:

//+------------------------------------------------------------------+
//| Example of wrapping a standard function in a shell function      |
//+------------------------------------------------------------------+
void DebugPrint(string text) { Print(text); }

Dies wird es Ihnen erlauben, den Code leichter zu entfernen, sobald das Debugging abgeschlossen ist:

  • Entfernen Sie den Funktionsaufruf „DebugPrint“,
  • führen Sie dann einen Kompilierungsprozess aus
  • und löschen Sie die Aufrufe dieser Funktion in denjenigen Zeilen, für die MetaEditor einen Kompilationsfehler ausgibt.

Das Gleiche gilt ebenso für Variablen, die während des Debuggings benutzt werden. Sie sollten daher auf global deklarierte Variablen und Funktionen setzen. Das wird es Ihnen ersparen, in den unendlichen Weiten Ihrer Applikation nach verschollenen Konstrukten zu suchen.


Fazit

Debugging ist ein wichtiger Bestandteil des Programmierens. Eine Person, die nicht in der Lage ist, ein Programm adäquat zu debuggen, kann sich nur schwerlich als Programmierer bezeichnen. Der primäre Debugging-Prozess findet jedoch stets in Ihrem Kopf statt. Dieser Artikel hat Sie dabei nur mit einer Auswahl von Debugging-Methoden vertraut gemacht. Aber selbst diese Methoden sind, wenn Sie die Operationsprinzipien einer Applikation nicht verstehen, nichts wert.

Ich wünsche Ihnen in jedem Fall noch ein fröhliches Debuggen!

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/654

Beigefügte Dateien |
Der ZigZag-Indikator: Frischer Ansatz und Neue Lösungen Der ZigZag-Indikator: Frischer Ansatz und Neue Lösungen
Dieser Beitrag beschäftigt sich mit der Möglichkeit, einen fortgeschrittenen ZigZag-Indikator zu erzeugen. Das Konzept der Identifikation von Knoten beruht auf der Verwendung des Envelopes-Indikators. Wir gehen davon aus, dass wir eine bestimmte Kombination von Eingabe-Parametern für eine Reihe von Envelopes finden können, bei denen alle ZigZag-Knoten innerhalb der Grenzen der Envelopes-Bänder liegen. Als Konsequenz können wir daher versuchen, die Koordinaten des neuen Knoten vorherzusagen.
Indikator für das Zeichnen von Point-and-Figure-Charts Indikator für das Zeichnen von Point-and-Figure-Charts
Es gibt viele verschiedene Charttypen, die Informationen zur aktuellen Marktsituation anzeigen. Viele von Ihnen - wie beispielsweise Point-&-Figure-Charts - sind die Hinterlassenschaften einer weit zurückliegenden Vergangenheit. Dieser Artikel beschäftigt sich mit dem Zeichnen von Point-&-Figure-Charts mithilfe von Echtzeitindikatoren.
MQL5 Cookbook: Handelsbedingungen mit Hilfe von Indikatoren in Experts Advisors einrichten MQL5 Cookbook: Handelsbedingungen mit Hilfe von Indikatoren in Experts Advisors einrichten
Auch in diesem Beitrag werden wir den Expert Advisor, den wir in allen vorangegangenen Beiträgen der MQL5 Cookbook Reihe bearbeitet haben, weiter verändern. Diesmal soll er durch Indikatoren verbessert werden mit Hilfe deren Werte nach Bedingungen zur Eröffnung von Positions gesucht werden kann. Um dem noch eins draufzusetzen, legen wir eine Dropdown-Liste in den externen Parametern an, um einen der drei Handels-Indikatoren auswählen zu können.
MQL5 Cloud Network Kalkulieren Sie noch? MQL5 Cloud Network Kalkulieren Sie noch?
Die Veröffentlichung von MQL5 Cloud Network ist nun schon beinahe anderthalb Jahre her. Dieser Zeitpunkt läutete gewissermaßen den Beginn einer neuen Ära des algorithmischen Tradings ein - mit nur einigen wenigen Klicks stehen Tradern nun mehrere hundert bis tausend Computerkerne zur Verfügung, um Ihre Handelsstrategien zu optimieren.