Multi-Symbol-Chart der Bilanz in MetaTrader 5

9 April 2018, 09:36
Anatoli Kazharski
0
288

Inhaltsverzeichnis

Einleitung

In einem der vorherigen Artikel haben wir die Visualisierung von Multi-Symbol-Charts der Bilanz betrachtet. Seitdem erschien aber eine Vielzahl von MQL-Bibliotheken, mit welchen man das Ganze in MetaTrader 5 implementieren kann, ohne Drittprogramme anzuwenden.

In diesem Artikel zeige ich ein Beispiel für eine Anwendung mit dem grafischen Interface, in welchem die Kurven der Bilanz und des Rückgangs für mehrere Symbole nach den Ergebnissen des letzten Tests angezeigt werden. Am Ende des Testens des Expert Advisors wird die Historie von Abschlüssen in eine Datei geschrieben. Danach können die Daten gelesen und in Charts angezeigt werden.

Darüber hinaus ist im Artikel eine Version des Expert Advisors angeführt, in welcher ein Multi-Symbol-Chart der Bilanz direkt während des Handels sowie während visuellen Testens im grafischen Interface angezeigt und aktualisiert wird.


Erstellen eines grafischen Interfaces

Im Artikel Visualisieren der Optimierung einer Handelsstrategie in MetaTrader 5 wurde gezeigt, wie die Bibliothek EasyAndFast eingebunden und verwendet werden kann sowie wie man mithilfe der Bibliothek ein grafisches Interface für eine MQL-Anwendung erstellen kann. Deswegen beginnen wir gleich mit dem grafischen Interface. 

Listen wir die Elemente auf, die im grafischen Interface verwendet werden:

  • Form für Steuerelemente.
  • Button für die Aktualisierung von Charts mit den Ergebnissen des letzten Tests.
  • Chart für die Anzeige der Bilanzkurve für mehrere Symbole.
  • Chart für die Anzeige des Rückgangs.
  • Statusleiste für die Anzeige der zusätzlichen Informationen.

Im Listing des Codes unten ist die Deklaration der Methoden für die Erstellung dieser Elemente angeführt. Die Implementierung der Methoden befindet sich in einer separaten Include-Datei.

//+------------------------------------------------------------------+
//| Klasse für die Erstellung der Anwendung                          |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  { 
private:
   //--- Fenster
   CWindow           m_window1;
   //--- Statusleiste
   CStatusBar        m_status_bar;
   //--- Charts
   CGraph            m_graph1;
   CGraph            m_graph2;
   //--- Buttons
   CButton           m_update_graph;
   +//---+
public:
   //--- Erstellt ein grafisches Interface
   bool              CreateGUI(void);
   +//---+
private:
   //--- Form
   bool              CreateWindow(const string text);
   //--- Statusleiste
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- Charts
   bool              CreateGraph1(const int x_gap,const int y_gap);
   bool              CreateGraph2(const int x_gap,const int y_gap);
   //--- Buttons
   bool              CreateUpdateGraph(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| Methoden für die Erstellung der Steuerelemente                   |
//+------------------------------------------------------------------+
#include "CreateGUI.mqh"
//+------------------------------------------------------------------+

Die Hauptmethode für die Erstellung des grafischen Interfaces sieht wie folgt aus:

//+------------------------------------------------------------------+
//| Erstellt das grafische Interface                                 |
//+------------------------------------------------------------------+
bool CProgram::CreateGUI(void)
  { 
//--- Erstellen der Form der Steuerelemente
   if(!CreateWindow("Expert panel"))
      return(false);
//--- Erstellen der Steuerelemente
   if(!CreateStatusBar(1,23))
      return(false);
   if(!CreateGraph1(1,50))
      return(false);
   if(!CreateGraph2(1,159))     
      return(false);
   if(!CreateUpdateGraph(7,25,"Update data"))
      return(false);
//--- Die Erstellung des GUI beenden
   CWndEvents::CompletedGUI();
   return(true);
  }

Wenn man den Expert Advisor jetzt kompiliert und ihn auf einen Chart im Terminal lädt, wird das Ergebnis wie folgt aussehen:

 Abb. 1 – Grafisches Interface des Expert Advisors.

Abb. 1. Grafisches Interface des Expert Advisors.

Weiter betrachten wir das Schreiben der Daten in eine Datei nach dem Testen.


Multi-Symbol Expert Advisor für Tests

Für Tests nehmen wir den Expert Advisor MACD Sample aus dem Standardpaket, aber wir machen daraus einen Multi-Symbol Expert Advisor. Das in dieser Version verwendete Multi-Symbol-Schema ist ungenau. Die Ergebnisse der Tests mit den gleichen Parametern werden unterschiedlich sein, je nachdem auf welchem Symbol getestet wird (das Symbol wird in den Einstellungen des Strategietesters ausgewählt). Aus diesem Grund passt der Expert Advisor nur für Tests und für die Demonstration der erhaltenen Ergebnisse.

In den kommenden Updates für das MetaTrader 5 Terminal werden neue Möglichkeiten für die Erstellung von Multi-Symbol Expert Advisors präsentiert. Dann kann man sich Gedanken über die Erstellung einer universellen Version für Expert Advisors von diesem Typ machen. Wenn Sie dringend eine schnelle und genaue Multi-Symbol-Lösung brauchen, könne Sie die Variante aus dem Forum probieren.

Den externen Parametern fügen wir noch einen String-Parameter für die Angabe der Symbole hinzu, auf welchen es getestet wird:

//--- Externe Parameter
sinput string Symbols           ="EURUSD,USDJPY,GBPUSD,EURCHF"; // Symbole
input  double InpLots           =0.1;                           // Lots
input  int    InpTakeProfit     =167;                           // Take Profit (in pips)
input  int    InpTrailingStop   =97;                            // Trailing Stop Level (in pips)
input  int    InpMACDOpenLevel  =16;                            // MACD open level (in pips)
input  int    InpMACDCloseLevel =19;                            // MACD close level (in pips)
input  int    InpMATrendPeriod  =14;                            // MA trend period

Die Symbole sind mit einem Komma voneinander zu trennen. In der Klasse des Programms (CProgram) sind Methoden für das Lesen dieses Parameters sowie für das Überprüfen und Hinzufügen der Symbole zur Marktübersicht implementiert, die in der Liste auf dem Server vorhanden sind. Die Symbole für den Handel kann man auch über eine vorher vorbereitete Liste in der Datei angeben, wie es im Artikel Das MQL5-Kochbuch: Entwicklung eines mehrwährungsfähigen Expert Advisors mit unbegrenzter Anzahl von Parametern gezeigt wurde. Darüber hinaus kann man mehrere Listen zur Auswahl in der Datei erstellen, ein Beispiel kann man im Artikel Das MQL5-Kochbuch: Abschwächen der Auswirkungen von Überanpassungen und Umgang mit mangelnden Kursen finden. Man kann sich eine Vielzahl unterschiedlicher Weisen für die Auswahl von Symbolen und deren Listen mithilfe des grafischen Interfaces einfallen lassen. In einem der nächsten Artikeln zeige ich eine solche Variante. 

Vor dem Überprüfen der Symbole in der Liste, muss man diese in einem Array speichern. Danach übergeben wir der Methode CProgram::CheckTradeSymbols() dieses Array (source_array[]). In der ersten Schleife iterieren wir über die im externen Parameter angegebenen Symbole und in der zweite Schleife überprüfen wir, ob das Symbol in der Liste auf dem Server des Brokers vorhanden ist. Wenn ja, fügen wir das Symbol der Marktübersicht und dem Array der geprüften Symbole hinzu. 

Am Ende der Methode, wenn keine Symbole gefunden wurden, wird das aktuelle Symbol verwendet, auf welchem der Expert Advisor läuft.

class CProgram : public CWndEvents
  { 
private:
   //--- Überprüft die Handelssymbole im übergebenen Array und gibt das Array der verfügbaren Symbole zurück
   void              CheckTradeSymbols(string &source_array[],string &checked_array[]);
  };
//+------------------------------------------------------------------+
//| Überprüft Symbole im übergebenen Array und                       |
//| gibt das Array der verfügbaren Symbole zurück                    |
//+------------------------------------------------------------------+
void CProgram::CheckTradeSymbols(string &source_array[],string &checked_array[])
  { 
   int symbols_total     =::SymbolsTotal(false);
   int size_source_array =::ArraySize(source_array);
//--- Suchen nach den angegebenen Symbolen in der gemeinsamen Liste
   for(int i=0; i<size_source_array; i++)
     { 
      for(int s=0; s<symbols_total; s++)
        { 
         //--- Erhalten des Namens des aktuellen Symbols in der Liste
         string symbol_name=::SymbolName(s,false);
         //--- Wenn es eine Übereinstimmung gibt
         if(symbol_name==source_array[i])
           { 
            //--- Fügen wir der Marktübersicht das Symbol hinzu
            ::SymbolSelect(symbol_name,true);
            //--- Fügen wir dem Array der geprüften Symbole das Symbol hinzu
            int size_array=::ArraySize(checked_array);
            ::ArrayResize(checked_array,size_array+1);
            checked_array[size_array]=symbol_name;
            break;
           }
        }
     }
//--- Wenn keine Symbole gefunden wurden, verwenden wir nur das aktuelle Symbol
   if(::ArraySize(checked_array)<1)
     { 
      ::ArrayResize(checked_array,1);
      checked_array[0]=_Symbol;
     }
  }

Für das Lesen des String-Parameters, in welchem die Symbole angegeben werden, wird die Methode CProgram::CheckSymbols() verwendet. Hier wird der String nach dem Trennzeichnen ',' ins Array geteilt. In den erhaltenen Strings werden die Leerzeichen von beiden Seiten entfernt. Danach wird dieses Array der Methode CProgram::CheckTradeSymbols() zur Überprüfung übergeben.

class CProgram : public CWndEvents
  { 
private:
   //| Überprüft und fügt dem Array die Symbole für den Handel aus dem String hinzu   |
   int               CheckSymbols(const string symbols_enum);
  };
//+------------------------------------------------------------------+
//| Überprüft und fügt dem Array Symbole aus dem String hinzu        |
//+------------------------------------------------------------------+
int CProgram::CheckSymbols(const string symbols_enum)
  { 
   if(symbols_enum!="")
      ::Print(__FUNCTION__," > input trade symbols: ",symbols_enum);
//--- Abfragen der Symbole aus dem String
   string symbols[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(symbols_enum,u_sep,symbols);
//--- Entfernen der Leerzeichen von beiden Seiten
   int elements_total=::ArraySize(symbols);
   for(int e=0; e<elements_total; e++)
     { 
      ::StringTrimLeft(symbols[e]);
      ::StringTrimRight(symbols[e]);
     }
//--- Überprüfen der Symbole
   ::ArrayFree(m_symbols);
   CheckTradeSymbols(symbols,m_symbols);
//--- Die Anzahl der Symbole für den Handel zurückgeben
   return(::ArraySize(m_symbols));
  }

Die Datei mit der Klasse der Handelsstrategie wird in die Datei mit der Klasse der Anwendung eingebunden und es wird ein dynamisches Array vom Typ CStrategy erstellt. 

#include "Strategy.mqh"
//+------------------------------------------------------------------+
//| Klasse für die Erstellung der Anwendung                          |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  { 
private:
   //--- Array der Strategien
   CStrategy         m_strategy[];
  };

Während der Initialisierung des Programms erhalten wir das Array von Symbolen und deren Anzahl aus dem externen Parameter direkt hier. Danach setzen wir die Größe für das Array der Strategien nach der Anzahl der Symbole und initialisieren wir alle Instanzen der Strategien, indem wir jeder Instanz einen Symbolnamen übergeben.

class CProgram : public CWndEvents
  { 
private:
   //--- Symbole insgesamt
   int               m_symbols_total;
  };
//+------------------------------------------------------------------+
//| Initialisierung                                                  |
//+------------------------------------------------------------------+
bool CProgram::OnInitEvent(void)
  { 
//--- Abfragen der Symbole für den Handel
   m_symbols_total=CheckSymbols(Symbols);
//--- Größe des Arrays der Symbole
   ::ArrayResize(m_strategy,m_symbols_total);
//--- Initialisierung
   for(int i=0; i<m_symbols_total; i++)
     { 
      if(!m_strategy[i].OnInitEvent(m_symbols[i]))
         return(false);
     }
//--- Initialisierung erfolgreich
   return(true);
  }

Weiter betrachten wir das Schreiben der Daten des letzten Tests in eine Datei.


Schreiben der Daten in eine Datei

Die Daten des letzten Tests werden im gemeinsamen Ordner des Terminals gespeichert. Auf diese Weise kann man auf die Datei von jedem MetaTrader 5 Terminal aus zugreifen. Im Konstruktor legen wir den Ordnernamen und den Dateinamen fest:

class CProgram : public CWndEvents
  { 
private:
   //--- Pfad zur Datei mit den Ergebnissen des letzten Tests
   string            m_last_test_report_path;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_symbols_total(0)
  { 
//--- Pfad zur Datei mit den Ergebnissen des letzten Tests
   m_last_test_report_path=::MQLInfoString(MQL_PROGRAM_NAME)+"\\LastTest.csv";
  }

Betrachten wir die Methode CProgram::CreateSymbolBalanceReport(), mit welcher die Daten in die Datei geschrieben werden. Für die Arbeit in dieser Methode (und auch in einer anderen, die wir später betrachten) brauchen wir die Arrays der Bilanz der Symbole.

//--- Arrays für die Bilanz aller Symbole
struct CReportBalance { double m_data[]; };
//+------------------------------------------------------------------+
//| Klasse für die Erstellung der Anwendung                          |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  { 
private:
   //--- Array der Bilanz aller Symbole
   CReportBalance    m_symbol_balance[];
   +//---+
private:
   //--- Erstellt einen Testbericht im CSV-Format
   void              CreateSymbolBalanceReport(void);
  };
//+------------------------------------------------------------------+
//| Erstellt einen Testbericht im CSV-Format                         |
//+------------------------------------------------------------------+
void CProgram::CreateSymbolBalanceReport(void)
  { 
   ...
  }

Am Anfang der Methode öffnen wir eine Datei im gemeinsamen Ordner der Terminals (FILE_COMMON):

...
//--- Erstellen einer Datei für das Schreiben von Daten im gemeinsamen Ordner des Terminals
   int file_handle=::FileOpen(m_last_test_report_path,FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON);
//--- Wenn das Handle gültig ist (die Datei wurde erstellt/geöffnet)
   if(file_handle==INVALID_HANDLE)
     { 
      ::Print(__FUNCTION__," > Error creating file: ",::GetLastError());
      return;
     }
...

Es werden mehrere Hilfsvariablen für das Bilden einiger Kennzahlen des Berichts benötigt. In die Datei wird die ganze Historie mit den Daten geschrieben, die in der Liste unten angeführt sind:

  • Zeit des Abschlusses
  • Symbol
  • Typ
  • Richtung
  • Volumen
  • Preis
  • SWAP
  • Ergebnis (Gewinn/Verlust)
  • Rückgang
  • Bilanz. In dieser Spalte wird die Gesamtbilanz, und in den nächsten - die Bilanz der Symbole, die beim Testen verwendet wurden, angezeigt.

Hier wird der erste String mit den Headern dieser Daten gebildet:

...
   double max_drawdown    =0.0; // Max. Rückgang
   double balance         =0.0; // Bilanz
   string delimeter       =","; // Trennzeichen
   string string_to_write ="";  // Für das Bilden des Strings für das Schreiben
//--- Bilden des Header-Strings 
   string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,PRICE,SWAP($),PROFIT($),DRAWDOWN(%),BALANCE";
...

Wenn mehr als ein Symbol getestet wird, muss der Header-String durch die Namen der getesteten Symbolen ergänzt werden. Danach können die Headers (der erste String) in die Datei geschrieben werden

...
//--- Wenn mehr als ein Symbol getestet wird, ergänzen wir den Header-String
   int symbols_total=::ArraySize(m_symbols);
   if(symbols_total>1)
     { 
      for(int s=0; s<symbols_total; s++)
         ::StringAdd(headers,delimeter+m_symbols[s]);
     }
//--- Schreiben der Headers des Berichts
   ::FileWrite(file_handle,headers);
...

Danach erhalten wir die vollständige Historie der Abschlüsse und deren Zahl und setzen die Array-Größe:

...
//--- Abfragen der ganzen Historie
   ::HistorySelect(0,LONG_MAX);
//--- Ermitteln der Zahl der Trades
   int deals_total=::HistoryDealsTotal();
//--- Setzen der Größe des Balance-Arrays nach der Anzahl der Symbole
   ::ArrayResize(m_symbol_balance,symbols_total);
//--- Setzen der Größe des Arrays der Abschlüsse für jedes Symbol
   for(int s=0; s<symbols_total; s++)
      ::ArrayResize(m_symbol_balance[s].m_data,deals_total);
...

Iterieren wir über die ganze Historie in der Hauptschleife und bilden wir Strings für das Schreiben in die Datei. Bei der Berechnung des Profits addieren wir auch Swap und Kommission. Wenn es sich herausstellt, dass es mehr als ein Symbol gibt, iterieren wir über Symbole in der zweiten Schleife und bilden die Bilanz für jedes Symbol.

Schreiben wir die Daten zeilenweise in die Datei. Am Ende der Methode wird die Datei geschlossen.
...
//--- Iterieren in der Schleifen und Schreiben der Daten 
   for(int i=0; i<deals_total; i++)
     { 
      //--- Abfragen des Tickets des Abschlusses
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Ermitteln der Anzahl der Stellen im Preis
      int digits=(int)::SymbolInfoInteger(m_deal_info.Symbol(),SYMBOL_DIGITS);
      //--- Gesamtbilanz berechnen
      balance+=m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
      //--- Bilden des Strings für das Schreiben durch Konkatenation
      ::StringConcatenate(string_to_write,
                          ::TimeToString(m_deal_info.Time(),TIME_DATE|TIME_MINUTES),delimeter,
                          m_deal_info.Symbol(),delimeter,
                          m_deal_info.TypeDescription(),delimeter,
                          m_deal_info.EntryDescription(),delimeter,
                          ::DoubleToString(m_deal_info.Volume(),2),delimeter,
                          ::DoubleToString(m_deal_info.Price(),digits),delimeter,
                          ::DoubleToString(m_deal_info.Swap(),2),delimeter,
                          ::DoubleToString(m_deal_info.Profit(),2),delimeter,
                          MaxDrawdownToString(i,balance,max_drawdown),delimeter,
                          ::DoubleToString(balance,2));
      //--- Gibt es mehr als ein Symbol, schreiben wir die Bilanzwerte
      if(symbols_total>1)
        { 
         //--- Iterieren über alle Symbole
         for(int s=0; s<symbols_total; s++)
           { 
            //--- Wenn alle Symbole übereinstimmen und das Ergebnis des Abschlusses nicht gleich Null ist,
            if(m_deal_info.Symbol()==m_symbols[s] && m_deal_info.Profit()!=0)
               //--- Anzeige des Abschlusses in der Bilanz dieses Symbols. Swap und Kommission berücksichtigen
               m_symbol_balance[s].m_data[i]=m_symbol_balance[s].m_data[i-1]+m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
            //--- Andernfalls den vorherigen Wert schreiben
            else
              { 
               //--- Wenn es sich um den Abschluss-Typ "Balance" (erster Abschluss) handelt, ist die Bilanz für alle Symbole gleich
               if(m_deal_info.DealType()==DEAL_TYPE_BALANCE)
                  m_symbol_balance[s].m_data[i]=balance;
               //--- Andernfalls den vorherigen Wert schreiben
               else
                  m_symbol_balance[s].m_data[i]=m_symbol_balance[s].m_data[i-1];
              }
            //--- Hinzufügen der Bilanz des Symbols zum String 
            ::StringAdd(string_to_write,delimeter+::DoubleToString(m_symbol_balance[s].m_data[i],2));
           }
        }
      //--- Schreiben des gebildeten Strings
      ::FileWrite(file_handle,string_to_write);
      //--- die Variable für den nächsten String obligatorisch auf Null setzen
      string_to_write="";
     }
//--- Datei schließen
   ::FileClose(file_handle);
...

Für das Bilden der Strings (s. Code oben) für das Schreiben in die Datei für die Berechnung des gesamten Rückgangs wird die Methode CProgram::MaxDrawdownToString() verwendet. Beim ersten Aufruf ist der Rückgang gleich Null, als lokaler Hoch/Tief wird der aktuelle Bilanzwert gespeichert. In den nächsten Aufrufen der Methode, wenn der Bilanzwert größer als der gespeicherte ist, berechnen wir den Rückgang basierend auf den vorherigen Werten und aktualisieren das lokale Minimum. Andernfalls aktualisieren wir das lokale Minimum und geben den Nullwert (leere Zeile) zurück.

class CProgram : public CWndEvents
  { 
private:
   //--- Gibt den maximalen Rückgang vom lokalen Maximum zurück
   string            MaxDrawdownToString(const int deal_number,const double balance,double &max_drawdown);
  };
//+------------------------------------------------------------------+
//| Gibt den maximalen Rückgang vom lokalen Maximum zurück           |
//+------------------------------------------------------------------+
string CProgram::MaxDrawdownToString(const int deal_number,const double balance,double &max_drawdown)
  { 
//--- String für die Anzeige im Bericht
   string str="";
//--- Für die Berechnung des lokalen Hochs und des Rückgangs 
   static double max=0.0;
   static double min=0.0;
//--- Wenn der erste Abschluss
   if(deal_number==0)
     { 
      //--- Noch kein Rückgang
      max_drawdown=0.0;
      //--- Geben wir den Anfangspunkt als lokales Maximum an 
      max=balance;
      min=balance;
     }
   else
     { 
      //--- Wenn die aktuelle Bilanz größer als die gespeicherte ist
      if(balance>max)
        { 
         //--- Rückgang basierend auf den vorherigen Werten berechnen
         max_drawdown=100-((min/max)*100);
         //--- Lokales Hoch aktualisieren
         max=balance;
         min=balance;
        }
      else
        { 
         //--- Nullwert des Rückgangs zurückgeben und das Minimum aktualisieren
         max_drawdown=0.0;
         min=fmin(min,balance);
        }
     }
//--- String für den Bericht bestimmen
   str=(max_drawdown==0)? "" : ::DoubleToString(max_drawdown,2);
   return(str);
  }

Die Struktur der Datei erlaubt es, sie in Excel zu öffnen. Wie das aussieht, ist auf dem Screenshot unten angeführt:

 Abb. 2. Dateistruktur des Berichts.

Abb. 2. Dateistruktur des Berichts in Excel.

Also muss die Methode CProgram::CreateSymbolBalanceReport() für das Schreiben des Berichts nach dem Test am Ende des Tests aufgerufen werden:

//+------------------------------------------------------------------+
//| Ereignis des Beendens des Testens                                |
//+------------------------------------------------------------------+
double CProgram::OnTesterEvent(void)
  { 
//--- den Bericht erst nach dem Testen schreiben 
   if(::MQLInfoInteger(MQL_TESTER) && !::MQLInfoInteger(MQL_OPTIMIZATION) && 
      !::MQLInfoInteger(MQL_VISUAL_MODE) && !::MQLInfoInteger(MQL_FRAME_MODE))
     { 
      //--- Erzeugung des Berichts und Schreiben in die Dateien
      CreateSymbolBalanceReport();
     }
+//---+
   return(0.0);
  }

Weiter betrachten wir das Lesen der Daten des Berichts.


Exportieren der Daten aus der Datei

Nach dem oben Implementierten wird jede Überprüfung des Expert Advisors im Strategietester mit dem Schreiben des Berichts in eine Datei enden. Weiter betrachten wir die Methoden, mit welchen die Daten des Berichts gelesen werden. In erster Linie muss die Datei gelesen werden und ihr Inhalt muss in einem Array gespeichert werden. Dafür wird die Methode CProgram::ReadFileToArray() verwendet. Öffnen wir die Datei, in welche am Ende des Tests die Historie von Abschlüssen geschrieben wurde. In der Schleife lesen wir die Datei bis zum letzten String und füllen das Array mit den Ausgangsdaten. 

class CProgram : public CWndEvents
  { 
private:
   //--- Array für die Daten aus der Datei
   string            m_source_data[];
   +//---+ 
private:
   //--- Lesen der Datei in das übergebene Array
   bool              ReadFileToArray(const int file_handle);
  };
//+------------------------------------------------------------------+
//| Lesen der Datei in das übergebene Array                          |
//+------------------------------------------------------------------+
bool CProgram::ReadFileToArray(const int file_handle)
  { 
//--- Öffnen der Datei
   int file_handle=::FileOpen(m_last_test_report_path,FILE_READ|FILE_ANSI|FILE_COMMON);
//--- Beenden, wenn sich die Datei nicht geöffnet hat
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Leeren des Arrays
   ::ArrayFree(m_source_data);
//--- Lesen der Datei in das Array
   while(!::FileIsEnding(file_handle))
     { 
      int size=::ArraySize(m_source_data);
      ::ArrayResize(m_source_data,size+1,RESERVE);
      m_source_data[size]=::FileReadString(file_handle);
     }
//--- Schließen der Datei
   ::FileClose(file_handle);
   return(true);
  }

Es wird die Hilfsmethode CProgram::GetStartIndex() für die Bestimmung des Index der Spalte mit dem Namen BALANCE benötigt. Als Argumente müssen der Methode die Header-Zeile, in welcher die Suche nach dem Spaltennamen erfolgt, und das dynamische Array für die Elemente des nach dem Trennzeichen ',' gesplitteten Strings übergeben werden.  

class CProgram : public CWndEvents
  { 
private:
   //--- Anfangsindex der Bilanz im Bericht
   bool              GetBalanceIndex(const string headers);
  };
//+------------------------------------------------------------------+
//| Bestimmen des Index, mit welchem das Kopieren von Daten beginnt  |
//+------------------------------------------------------------------+
bool CProgram::GetBalanceIndex(const string headers)
  { 
//--- Abfragen der Elemente des Strings nach dem Trennzeichen
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(headers,u_sep,str_elements);
//--- Suchen nach der Spalte mit dem Namen 'BALANCE'
   int elements_total=::ArraySize(str_elements);
   for(int e=elements_total-1; e>=0; e--)
     { 
      string str=str_elements[e];
      ::StringToUpper(str);
      //--- Wenn die Spalte mit dem richtigen Header gefunden wurde
      if(str=="BALANCE")
        { 
         m_balance_index=e;
         break;
        }
     }
//--- Meldung ausgeben, wenn keine Spalte mit dem Namen 'BALANCE' gefunden wurde
   if(m_balance_index==WRONG_VALUE)
     { 
      ::Print(__FUNCTION__," > In the report file there is no heading \'BALANCE\' ! ");
      return(false);
     }
//--- Erfolgreich
   return(true);
  }

Auf der X-Achse werden die Nummern der Abschlüsse in beiden Charts angezeigt. Die Zeitspanne wird als zusätzliche Information in der unteren Fußzeile des Bilanz-Charts angezeigt. Um das Anfangsdatum und das Enddatum der Historie zu bestimmen, wurde die Methode CProgram::GetDateRange() implementiert. Der Methode werden zwei String-Variablen als Referenz für das Anfangs- und Enddatum der Historie übergeben.

class CProgram : public CWndEvents
  { 
private:
   //--- Zeitspanne
   void              GetDateRange(string &from_date,string &to_date);
  };
//+------------------------------------------------------------------+
//| Abfragen des Anfangs- und des Enddatums des Testzeitraums        |
//+------------------------------------------------------------------+
void CProgram::GetDateRange(string &from_date,string &to_date)
  { 
//--- Beenden, wenn es weniger als 3 Strings gibt
   int strings_total=::ArraySize(m_source_data);
   if(strings_total<3)
      return;
//--- Abfragen des Anfangs- und des Enddatums des Berichts
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
+//---+
   ::StringSplit(m_source_data[1],u_sep,str_elements);
   from_date=str_elements[0];
   ::StringSplit(m_source_data[strings_total-1],u_sep,str_elements);
   to_date=str_elements[0];
  }

Für das Abfragen der Daten der Bilanz und des Rückgangs werden die Methoden CProgram::GetReportDataToArray() und CProgram::AddDrawDown() verwendet. Die zweite Methode wird in der ersten Methode aufgerufen und ihr Code ist ganz kurz (s. das Listing unten). Hier wird der Index des Abschlusses und der Wert des Rückgangs übergeben werden, die in den entsprechenden Arrays gespeichert werden, deren Werte danach im Chart angezeigt werden. Im Array m_dd_y[] werden der Wert des Rückgangs und im Array m_dd_x[] — der Index gespeichert, auf welchem dieser Wert angezeigt werden soll. Auf den Indexen, wo es keine Werte gibt, wird nichts auf den Charts angezeigt (leere Werte).

class CProgram : public CWndEvents
  { 
private:
   //--- Rückgang der Gesamtbilanz
   double            m_dd_x[];
   double            m_dd_y[];
   +//---+ 
private:
   //--- Fügt den Arrays den Rückgang hinzu
   void              AddDrawDown(const int index,const double drawdown);
  };
//+------------------------------------------------------------------+
//| Fügt den Arrays den Rückgang hinzu                               |
//+------------------------------------------------------------------+
void CProgram::AddDrawDown(const int index,const double drawdown)
  { 
   int size=::ArraySize(m_dd_y);
   ::ArrayResize(m_dd_y,size+1,RESERVE);
   ::ArrayResize(m_dd_x,size+1,RESERVE);
   m_dd_y[size] =drawdown;
   m_dd_x[size] =(double)index;
  }

In der Methode CProgram::GetReportDataToArray() werden zuerst die Größe der Arrays und die Anzahl der Serien für den Bilanz-Chart definiert. Danach initialisieren wir das Array der Headers. Danach werden Elemente des Strings nach dem Trennzeichen zeilenweise exportiert und die Daten werden in die Arrays des Rückgangs und der Bilanz platziert.  

class CProgram : public CWndEvents
  { 
private:
   //--- Erhält die Daten der Symbole aus dem Bericht
   int               GetReportDataToArray(string &headers[]);
  };
//+------------------------------------------------------------------+
//| Abfragen der Daten des Symbols aus dem Bericht                   |
//+------------------------------------------------------------------+
int CProgram::GetReportDataToArray(string &headers[])
  { 
//--- Abfragen der Elemente des Header-Strings
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(m_source_data[0],u_sep,str_elements);
//--- Größe der Arrays
   int strings_total  =::ArraySize(m_source_data);
   int elements_total =::ArraySize(str_elements);
//--- Leeren der Arrays
   ::ArrayFree(m_dd_y);
   ::ArrayFree(m_dd_x);
//--- Abfragen der Anzahl der Serien
   int curves_total=elements_total-m_balance_index;
   curves_total=(curves_total<3)? 1 : curves_total;
//--- Setzen der Größe der Arrays nach der Anzahl der Serien
   ::ArrayResize(headers,curves_total);
   ::ArrayResize(m_symbol_balance,curves_total);
//--- Setzen der Größe der Serien
   for(int i=0; i<curves_total; i++)
      ::ArrayResize(m_symbol_balance[i].m_data,strings_total,RESERVE);
//--- Wenn es mehrere Symbole gibt (Headers abfragen)
   if(curves_total>2)
     { 
      for(int i=0,e=m_balance_index; e<elements_total; e++,i++)
         headers[i]=str_elements[e];
     }
   else
      headers[0]=str_elements[m_balance_index];
//--- Abfragen der Daten
   for(int i=1; i<strings_total; i++)
     { 
      ::StringSplit(m_source_data[i],u_sep,str_elements);
      //--- Hinzufügen der Daten zu Arrays
      if(str_elements[m_balance_index-1]!="")
         AddDrawDown(i,double(str_elements[m_balance_index-1]));
      //--- Wenn es mehrere Symbole gibt
      if(curves_total>2)
         for(int b=0,e=m_balance_index; e<elements_total; e++,b++)
            m_symbol_balance[b].m_data[i]=double(str_elements[e]);
      else
         m_symbol_balance[0].m_data[i]=double(str_elements[m_balance_index]);
     }
//--- Der erste Wert der Serien
   for(int i=0; i<curves_total; i++)
      m_symbol_balance[i].m_data[0]=(strings_total<2)? 0 : m_symbol_balance[i].m_data[1];
//--- Anzahl der Serien zurückgeben
   return(curves_total);
  }

Im nächsten Kapitel betrachten wir, wie man die erhaltenen Daten im Chart anzeigen kann.


Anzeige der Daten in Charts

Der Aufruf der Hilfsmethoden, die wir im vorherigen Abschnitt betrachtet haben, erfolgt am Anfang der Methode für die Aktualisierung des Bilanz-Charts — CProgram::UpdateBalanceGraph(). Danach werden die aktuellen Serien vom Chart gelöscht, weil sich die Anzahl der Symbole im letzten Test ändern könnte. In der Methode CProgram::GetReportDataToArray() wird die aktuelle Anzahl der Symbole bestimmt. Danach iterieren wir über die Symbole in der Schleife. Fügen wir die neuen Datenserien der Bilanz hinzu und ermitteln wir den minimalen und maximalen Wert auf der Y-Achse. 

Speichern wir die Größe der Serien und den Schritt auf der X-Achse in den Feldern der Klasse. Diese Werte werden auch für das Formatieren des Charts des Rückgangs benötigt. Für die Y-Achse werden die Abstände (von 5%) für die Extrema des Charts berechnet. Diese Werte werden auf den Bilanz-Chart angewandt, und der Chart wird aktualisiert, damit die neuesten Änderungen angezeigt werden. 

class CProgram : public CWndEvents
  { 
private:
   //--- Daten insgesamt in einer Serie
   double            m_data_total;
   //--- Schritt auf der X-Achse
   double            m_default_step;
   +//---+ 
private:
   //--- Aktualisiert Daten im Chart der Bilanzkurven
   void              UpdateBalanceGraph(void);
  };
//+------------------------------------------------------------------+
//| Aktualisieren des Charts                                         |
//+------------------------------------------------------------------+
void CProgram::UpdateBalanceGraph(void)
  { 
//--- Abfragen der Daten des Testzeitraums
   string from_date=NULL,to_date=NULL;
   GetDateRange(from_date,to_date);
//--- Bestimmen des Index, mit welchem das Kopieren von Daten beginnt
   if(!GetBalanceIndex(m_source_data[0]))
      return;
//--- Abfragen der Daten der Symbole aus dem Bericht
   string headers[];
   int curves_total=GetReportDataToArray(headers);

//--- Aktualisieren aller Serien des Charts
   CColorGenerator m_generator;
   CGraphic *graph=m_graph1.GetGraphicPointer();
//--- Leeren des Charts
   int total=graph.CurvesTotal();
   for(int i=total-1; i>=0; i--)
      graph.CurveRemoveByIndex(i);
//--- Der maximale und minimale Wert des Charts
   double y_max=0.0,y_min=m_symbol_balance[0].m_data[0];
//--- Daten hinzufügen
   for(int i=0; i<curves_total; i++)
     { 
      //--- Bestimmen des Maximums/Minimums auf der Y-Achse 
      y_max=::fmax(y_max,m_symbol_balance[i].m_data[::ArrayMaximum(m_symbol_balance[i].m_data)]);
      y_min=::fmin(y_min,m_symbol_balance[i].m_data[::ArrayMinimum(m_symbol_balance[i].m_data)]);
      //--- Hinzufügen der Serie zum Chart
      CCurve *curve=graph.CurveAdd(m_symbol_balance[i].m_data,m_generator.Next(),CURVE_LINES,headers[i]);
     }
//--- Anzahl der Werte und der Schritt auf der X-Achse
   m_data_total   =::ArraySize(m_symbol_balance[0].m_data)-1;
   m_default_step =(m_data_total<10)? 1 : ::MathFloor(m_data_total/5.0);
//--- Zeitraum und Abstände
   double range  =::fabs(y_max-y_min);
   double offset =range*0.05;
//--- Farbe für die erste Serie
   graph.CurveGetByIndex(0).Color(::ColorToARGB(clrCornflowerBlue));
//--- Eigenschaften der horizontalen Achse
   CAxis *x_axis=graph.XAxis();
   x_axis.AutoScale(false);
   x_axis.Min(0);
   x_axis.Max(m_data_total);
   x_axis.MaxGrace(0);
   x_axis.MinGrace(0);
   x_axis.DefaultStep(m_default_step);
   x_axis.Name(from_date+" - "+to_date);
//--- Eigenschaften der vertikalen Achse
   CAxis *y_axis=graph.YAxis();
   y_axis.AutoScale(false);
   y_axis.Min(y_min-offset);
   y_axis.Max(y_max+offset);
   y_axis.MaxGrace(0);
   y_axis.MinGrace(0);
   y_axis.DefaultStep(range/10.0);
//--- Chart aktualisieren
   graph.CurvePlotAll();
   graph.Update();
  }

Um den Chart des Rückgangs zu aktualisieren, wird die Methode CProgram::UpdateDrawdownGraph() verwendet. Da die Daten bereits in der Methode CProgram::UpdateBalanceGraph() berechnet wurden, wenden Sie die Daten auf den Chart an und aktualisieren Sie ihn.

class CProgram : public CWndEvents
  { 
private:
   //--- Aktualisiert die Daten im Chart des Rückgangs
   void              UpdateDrawdownGraph(void);
  };
//+------------------------------------------------------------------+
//| Aktualisieren des Charts des Rückgangs                           |
//+------------------------------------------------------------------+
void CProgram::UpdateDrawdownGraph(void)
  { 
//--- Aktualisieren des Charts des Rückgangs
   CGraphic *graph=m_graph2.GetGraphicPointer();
   CCurve *curve=graph.CurveGetByIndex(0);
   curve.Update(m_dd_x,m_dd_y);
   curve.PointsFill(false);
   curve.PointsSize(6);
   curve.PointsType(POINT_CIRCLE);
//--- Eigenschaften der horizontalen Achse
   CAxis *x_axis=graph.XAxis();
   x_axis.AutoScale(false);
   x_axis.Min(0);
   x_axis.Max(m_data_total);
   x_axis.MaxGrace(0);
   x_axis.MinGrace(0);
   x_axis.DefaultStep(m_default_step);
//--- Chart aktualisieren
   graph.CalculateMaxMinValues();
   graph.CurvePlotAll();
   graph.Update();
  }

Die Methoden CProgram::UpdateBalanceGraph() und CProgram::UpdateDrawdownGraph() werden in der Methode CProgram::UpdateGraphs() aufgerufen. Vor dem Aufruf dieser Methoden wird zuerst die Methode CProgram::ReadFileToArray() aufgerufen, die die Daten aus der Datei mit den Ergebnissen des letzten Tests des Expert Advisors erhält. 

class CProgram : public CWndEvents
  { 
private:
   //--- Aktualisiert die Daten auf den Charts der Ergebnisse des letzten Tests
   void              UpdateGraphs(void);
  };
//+------------------------------------------------------------------+
//| Aktualisieren der Charts                                         |
//+------------------------------------------------------------------+
void CProgram::UpdateGraphs(void)
  { 
//--- Füllen des Arrays mit den Daten aus der Datei
   if(!ReadFileToArray())
     { 
      ::Print(__FUNCTION__," > Could not open the test results file!");
      return;
     }
//--- Aktualisieren des Charts der Bilanz und des Rückgangs
   UpdateBalanceGraph();
   UpdateDrawdownGraph();
  }

Demonstration des erhaltenen Ergebnisses

Um die Ergebnisse des letzten Tests in den Charts des Interfaces anzuzeigen, muss man auf einen einzigen Button klicken. Das Ereignis des Klicks wird in der Methode CProgram::OnEvent() verarbeitet:

//+------------------------------------------------------------------+
//| Event-Handler                                                    |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  { 
//--- Ereignisse eines Klicks auf Buttons
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     { 
      //--- Klick auf den Button 'Update data'
      if(lparam==m_update_graph.Id())
        { 
         //--- Charts aktualisieren
         UpdateGraphs();
         return;
        }
      +//---+
      return;
     }
  }

Wenn der Expert Advisor vor dem Klick auf den Button getestet wurde, sehen wir ungefähr das Folgende:

Abb. 3 – Das Ergebnis des letzten Tests des Expert Advisors. 

Abb. 3. Das Ergebnis des letzten Tests des Expert Advisors.

Wenn der Expert Advisor auf den Chart geladen wurde, können Sie die Änderungen auf dem Multi-Symbol-Chart der Bilanz sofort sehen, wenn Sie sich die Ergebnisse der Tests nach der Optimierung von Parametern anschauen. 

Multi-Symbol-Chart der Bilanz während des Handels und der Tests

Nun betrachten wir die zweite Version des Expert Advisors, wenn der Multi-Symbol-Chart während des Handels gezeichnet und aktualisiert wird. 

Das grafische Interface bleibt fast gleich wie in der oben beschriebenen Version. Der Unterschied besteht nur darin, dass statt eines Buttons für die Aktualisierung von Daten ein Drop-Down-Kalender für die Angabe des Datums verwendet wird, ab wann das Handelsergebnis im Chart angezeigt werden muss.

Die Änderung der Historie wird beim Eintreffen eines Ereignisses in der Methode OnTrade() überprüft. Für die Überprüfung wird die Methode CProgram::IsLastDealTicket() verwendet. In dieser Methode erhalten wir die Historie für den Zeitraum, der nach dem letzten Aufruf gespeichert wurde. Danach prüfen wir das Ticket des letzten Abschlusses und das gespeicherte Ticket. Wenn sich die Tickets voneinander unterscheiden, aktualisieren wir das Ticket und die Zeit des letzten Abschlusses für eine weitere Überprüfung und geben wir einen Wert (true) zurück, dass sich die Historie geändert hat.

class CProgram : public CWndEvents
  { 
private:
   //--- Zeit und Ticket des letzten geprüften Abschlusses
   datetime          m_last_deal_time;
   ulong             m_last_deal_ticket;
   +//---+ 
private:
   //--- Überprüfen eines neuen Abschlusses
   bool              IsLastDealTicket(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_last_deal_time(NULL),
                           m_last_deal_ticket(WRONG_VALUE)
  { 
  }
//+------------------------------------------------------------------------------------+
//| Gibt das Ereignis des letzten Abschlusses für das angegebene Symbol zurück         |
//+------------------------------------------------------------------------------------+
bool CProgram::IsLastDealTicket(void)
  { 
//--- Beenden, wenn die Historie nicht erhalten wurde
   if(!::HistorySelect(m_last_deal_time,LONG_MAX))
      return(false);
//--- Erhalten der Anzahl der Abschlüsse in der erhaltenen Liste
   int total_deals=::HistoryDealsTotal();
//--- Iterieren wir über alle Abschlüsse in der erhaltenen Liste vom letzten zum ersten Abschluss
   for(int i=total_deals-1; i>=0; i--)
     { 
      //--- Abfragen des Tickets des Abschlusses
      ulong deal_ticket=::HistoryDealGetTicket(i);
      //--- Wenn die Tickets gleich sind, beenden
      if(deal_ticket==m_last_deal_ticket)
         return(false);
      //--- Wenn die Tickets nicht gleich sind, das melden
      else
        { 
         datetime deal_time=(datetime)::HistoryDealGetInteger(deal_ticket,DEAL_TIME);
         //--- Speichern der Zeit und des Tickets des letzten Abschlusses
         m_last_deal_time   =deal_time;
         m_last_deal_ticket =deal_ticket;
         return(true);
        }
     }
//--- Tickets des anderen Symbols
   return(false);
  }

Vor dem Durchlaufen der Historie und dem Füllen der Arrays mit Daten, muss man feststellen, welche und wie viele Symbole in der Historie vorhanden sind. Das wird für das Setzen der Arraygröße benötigt. Dafür wird die Methode CProgram::GetHistorySymbols() verwendet. Vor dem Aufruf der Methode muss man die Historie im gewünschten Zeitraum auswählen. Danach fügen wir dem String die Symbole hinzu, die in der Historie gefunden werden. Damit sich die Symbole im String nicht wiederholen, überprüfen wir das Vorhandensein des angegebenen Unterstrings. Danach fügen wir dem Array die in der Historie gefundenen Symbole hinzu und geben die Anzahl der Symbole zurück.

class CProgram : public CWndEvents
  { 
private:
   //--- Array der Symbole aus der Historie
   string            m_symbols_name[];
   +//---+ 
private:
   //--- Erhalten der Symbole aus der Historie und Zurückgeben deren Anzahl
   int               GetHistorySymbols(void);
  };
//+----------------------------------------------------------------------+
//| Erhalten der Symbole aus der Historie und Zurückgeben deren Anzahl   |
//+----------------------------------------------------------------------+
int CProgram::GetHistorySymbols(void)
  { 
   string check_symbols="";
//--- Iterieren in der Schleife und Erhalten der gehandelten Symbole
   int deals_total=::HistoryDealsTotal();
   for(int i=0; i<deals_total; i++)
     { 
      //--- Abfragen des Tickets des Abschlusses
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Wenn es den Namen des Symbols gibt,
      if(m_deal_info.Symbol()=="")
         continue;
      //--- Wenn solcher String nicht vorhanden ist, fügen wir ihn hinzu
      if(::StringFind(check_symbols,m_deal_info.Symbol(),0)==-1)
         ::StringAdd(check_symbols,(check_symbols=="")? m_deal_info.Symbol() : ","+m_deal_info.Symbol());
     }
//--- Abfragen der Elemente des Strings nach dem Trennzeichen
   ushort u_sep=::StringGetCharacter(",",0);
   int symbols_total=::StringSplit(check_symbols,u_sep,m_symbols_name);
//--- Anzahl der Symbole zurückgeben
   return(symbols_total);
  }

Für das Erhalten der Multi-Symbol-Bilanz muss die Methode CProgram::GetHistorySymbolsBalance() aufgerufen werden:

class CProgram : public CWndEvents
  { 
private:
   //--- Erhält die Gesamtbilanz und die Bilanz für jedes Symbol einzeln
   void              GetHistorySymbolsBalance(void);
  };
//+------------------------------------------------------------------+
//| Erhält die Gesamtbilanz und die Bilanz für jedes Symbol einzeln  |
//+------------------------------------------------------------------+
void CProgram::GetHistorySymbolsBalance(void)
  { 
   ...
  }

Hier muss man den anfänglichen Kontostand erhalten. Erhalten wir die Historie vom allerersten Abschluss, er wird der anfängliche Kontostand sein. Wir gehen davon aus, dass es die Möglichkeit gibt, das Datum im Kalender anzugeben, von welchem das Handelsergebnis angezeigt werden muss. Deswegen wählen wir die Historie noch einmal. Mithilfe der Methode CProgram::GetHistorySymbols() erhalten wir Symbole und deren Anzahl in der Historie, danach setzen wir die Arraygrößen. Für die Anzeige der historischen Zeitspanne legen wir das Anfansgs- und das Enddatum fest. 

...
//--- Initiale Einzahlung
   ::HistorySelect(0,LONG_MAX);
   double balance=(m_deal_info.SelectByIndex(0))? m_deal_info.Profit() : 0;
//--- Abfragen der Historie vom angegebenen Datum
   ::HistorySelect(m_from_trade.SelectedDate(),LONG_MAX);
//--- Abfragen der Anzahl der Symbole
   int symbols_total=GetHistorySymbols();
//--- Leeren der Arrays
   ::ArrayFree(m_dd_x);
   ::ArrayFree(m_dd_y);
//--- Setzen der Größe des Arrays der Bilanz nach der Zahl der Symbole + 1
   ::ArrayResize(m_symbols_balance,(symbols_total>1)? symbols_total+1 : 1);
//--- Setzen der Größe des Arrays der Abschlüsse für jedes Symbol
   int deals_total=::HistoryDealsTotal();
   for(int s=0; s<=symbols_total; s++)
     { 
      if(symbols_total<2 && s>0)
         break;
      +//---+
      ::ArrayResize(m_symbols_balance[s].m_data,deals_total);
      ::ArrayInitialize(m_symbols_balance[s].m_data,0);
     }
//--- Anzahl der Bilanzkurven 
   int balances_total=::ArraySize(m_symbols_balance);
//--- Anfang und Ende der Historie
   m_begin_date =(m_deal_info.SelectByIndex(0))? m_deal_info.Time() : m_from_trade.SelectedDate();
   m_end_date   =(m_deal_info.SelectByIndex(deals_total-1))? m_deal_info.Time() : ::TimeCurrent();
...

In der nächsten Schleife werden die Bilanz und der Rückgang der Symbole berechnet. Die erhaltenen Daten werden in einem Array gespeichert. Für die Berechnung des Rückgangs werden die Methoden verwendet, die in vorherigen Abschnitten betrachtet wurden.

...
//--- Maximaler Rückgang
   double max_drawdown=0.0;
//--- Schreiben der Arrays der Bilanz in das übergebene Array
   for(int i=0; i<deals_total; i++)
     { 
      //--- Abfragen des Tickets des Abschlusses
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Initialisierung auf dem ersten Abschluss
      if(i==0 && m_deal_info.DealType()==DEAL_TYPE_BALANCE)
         balance=0;
      //--- Vom angegebenen Datum
      if(m_deal_info.Time()>=m_from_trade.SelectedDate())
        { 
         //--- Gesamtbilanz berechnen
         balance+=m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
         m_symbols_balance[0].m_data[i]=balance;
         //--- Rückgang berechnen
         if(MaxDrawdownToString(i,balance,max_drawdown)!="")
            AddDrawDown(i,max_drawdown);
        }
      //--- Gibt es mehr als ein Symbol, schreiben wir die Bilanzwerte
      if(symbols_total<2)
         continue;
      //--- Nur vom angegebenen Datum
      if(m_deal_info.Time()<m_from_trade.SelectedDate())
         continue;
      //--- Iterieren über alle Symbole
      for(int s=1; s<balances_total; s++)
        { 
         int prev_i=i-1;
         //--- Wenn es sich um den Abschluss-Typ "Balance" (erster Abschluss) handelt,
         if(prev_i<0 || m_deal_info.DealType()==DEAL_TYPE_BALANCE)
           { 
            //--- ... ist die Bilanz für alle Symbole gleich
            m_symbols_balance[s].m_data[i]=balance;
            continue;
           }
         //--- Wenn die Symbole gleich sind und das Ergebnis des Abschlusses nicht gleich Null ist
         if(m_deal_info.Symbol()==m_symbols_name[s-1] && m_deal_info.Profit()!=0)
           { 
            //--- Anzeige des Abschlusses in der Bilanz dieses Symbols. Swap und Kommission berücksichtigen.
            m_symbols_balance[s].m_data[i]=m_symbols_balance[s].m_data[prev_i]+m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
           }
         //--- Andernfalls den vorherigen Wert schreiben
         else
            m_symbols_balance[s].m_data[i]=m_symbols_balance[s].m_data[prev_i];
        }
     }
...

Die Daten werden den Charts hinzugefügt und mit den Methoden CProgram::UpdateBalanceGraph() und CProgram::UpdateDrawdownGraph() aktualisiert. Ihr Code ist fast gleich dem Code in der ersten Version des Expert Advisors, der in den vorherigen Abschnitten betrachtet wurde, deswegen kommen wir direkt zum Teil, wo sie aufgerufen werden.

In erster Linie werden diese Methoden bei der Erstellung eines grafischen Interfaces aufgerufen, damit der Nutzer sofort das Ergebnis des Handels sehen kann. Danach werden die Charts beim Eintreffen von Handelsereignissen in der Methode OnTrade() aktualisiert. 

class CProgram : public CWndEvents
  { 
private:
   //--- Initialisierung der Charts
   void              UpdateBalanceGraph(const bool update=false);
   void              UpdateDrawdownGraph(void);
  };
//+------------------------------------------------------------------+
//| Ereignis einer Transaktion                                       |
//+------------------------------------------------------------------+
void CProgram::OnTradeEvent(void)
  { 
//--- Aktualisieren der Charts der Bilanz und des Rückgangs
   UpdateBalanceGraph();
   UpdateDrawdownGraph();
  }

Darüber hinaus kann der Nutzer im grafischen Interface ein Datum angeben, von welchem die Konstand-Charts gezeichnet werden sollen. Um den Chart zwingend zu aktualisieren, ohne das letzte Ticket zu überprüfen, muss man der Methode CProgram::UpdateBalanceGraph() den Wert true übergeben.

Das Ereignis der Änderung des Datums im Kalender (ON_CHANGE_DATE) wird wie folgt verarbeitet:

//+------------------------------------------------------------------+
//| Event-Handler                                                    |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  { 
//--- Das Ereignis der Auswahl des Datums im Kalender
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_DATE)
     { 
      if(lparam==m_from_trade.Id())
        { 
         UpdateBalanceGraph(true);
         UpdateDrawdownGraph();
         m_from_trade.ChangeComboBoxCalendarState();
        }
      +//---+
      return;
     }
  }

Unten ist dargestellt, wie das beim visuellen Testen im Strategietester funktioniert:

Abb. 4 – Demonstration des Ergebnisses im visuellen Testmodus im Strategietester.

Abb. 4. Demonstration des Ergebnisses im visuellen Testmodus im Strategietester.

Visualisieren der Berichte aus dem Signale-Service

Als eine weitere hilfreiche Ergänzung erstellen wir einen Expert Advisor, mit dem man die Ergebnisse des Handels aus den Berichten im Signale-Service visualisieren kann.

Öffnen Sie die Seite des gewünschten Signals und wählen Sie den Reiter Historie aus:

Abb. 5. Handelshistorie des Signals.

Am Ende der Liste kann man einen Link zum Downloaden einer CSV-Datei mit der Handelshistorie finden:

 Abb. 6 – Exportieren der Handelshistorie in eine CSV-Datei.

Abb. 6. Exportieren der Historie in eine CSV-Datei.

Für die Implementierung des Expert Advisors müssen diese Dateien im Verzeichnis des Terminals \MQL5\Files gespeichert werden. Fügen wir dem Expert Advisor einen externen Parameter hinzu. In diesem Parameter wird der Name der Berichtsdatei angegeben, deren Daten in den Charts visualisiert werden müssen.

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Externe Parameter
input string PathToFile=""; // Path to file
...

Abb. 7 – Externer Parameter für die Angabe der Berichtsdatei.

Abb. 7.  Externer Parameter für die Angabe der Berichts-Datei.

Im grafischen Interface dieser Version des Expert Advisors wird es nur zwei Charts geben. Beim Laden des Expert Advisors auf einen Chart im Terminal versucht er die in den Einstellungen angegebene Datei zu öffnen. Wenn die Datei nicht gefunden wird, gibt das Programm die Meldung Journal aus. Der Set von Methoden ist fast gleich dem in den oben beschriebenen Versionen. Es gibt einige Unterschiede, aber im Grunde genommen ist das Prinzip gleich. Betrachten wir nur die Methoden, in welchen der Ansatz komplett geändert wurde.

Die Datei ist gelesen und die Strings wurden ins Array der Ausgangsdaten verschoben. Nun muss man diese Daten in einem zweidimensionalen Array speichern, wie es in Tabellen getan wird. Das wird für eine bequeme aufsteigende Sortierung von Daten nach Eröffnungszeit benötigt. Dafür brauchen wir ein separates Array der Arrays. 

//--- Arrays für die Daten aus der Datei
struct CReportTable
  { 
   string            m_rows[];
  };
//+------------------------------------------------------------------+
//| Klasse für die Erstellung der Anwendung                          |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  { 
private:
   //--- Tabelle für den Bericht
   CReportTable      m_columns[];
   //--- Anzahl der Zeilen und der Spalten
   uint              m_rows_total;
   uint              m_columns_total;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_rows_total(0),
                           m_columns_total(0)
  { 
...
  }

Für die Sortierung des Arrays der Arrays werden folgende Methoden benötigt:

class CProgram : public CWndEvents
  { 
private:
   //--- Quick-Sort
   void              QuickSort(uint beg,uint end,uint column);
   //--- Überprüfen der Bedingungen der Sortierung
   bool              CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction);
   //--- Tauschen der Werte in den angegebenen Zellen
   void              Swap(uint r1,uint r2);
  };

All die Methoden wurden in einem vorherigen Artikel betrachtet. 

Alle Operationen werden in der Methode CProgram::GetData() durchgeführt. Gehen wir darauf ausführlicher ein. 

class CProgram : public CWndEvents
  { 
private:
   //--- Abfragen der Daten
   int               GetData(void);
  };
//+------------------------------------------------------------------+
//| Abfragen der Daten des Symbols aus dem Bericht                   |
//+------------------------------------------------------------------+
int CProgram::GetData(void)
  { 
...
  }

Bestimmen wir die Anzahl der Strings und der Elemente des Strings nach dem Trennzeichen ';'. Danach fragen wir die Namen und die Anzahl der Symbole aus dem Bericht in ein separates Array ab. Anschließend bereiten wir die Arrays auf und füllen diese mit den Daten aus dem Bericht.

...
//--- Abfragen der Elemente des Header-Strings
   string str_elements[];
   ushort u_sep=::StringGetCharacter(";",0);
   ::StringSplit(m_source_data[0],u_sep,str_elements);
//--- Anzahl der Strings und der Elemente des Strings
   int strings_total  =::ArraySize(m_source_data);
   int elements_total =::ArraySize(str_elements);
//--- Erhalten der Symbole
   if((m_symbols_total=GetHistorySymbols())==WRONG_VALUE)
     return;
//--- Leeren der Arrays
   ::ArrayFree(m_dd_y);
   ::ArrayFree(m_dd_x);
//--- Größe der Datenreihen
   ::ArrayResize(m_columns,elements_total);
   for(int i=0; i<elements_total; i++)
      ::ArrayResize(m_columns[i].m_rows,strings_total-1);
//--- Füllen der Arrays mit den Daten aus der Datei
   for(int r=0; r<strings_total-1; r++)
     { 
      ::StringSplit(m_source_data[r+1],u_sep,str_elements);
      for(int c=0; c<elements_total; c++)
         m_columns[c].m_rows[r]=str_elements[c];
     }
...

Alles ist fertig für das Sortieren der Daten. Hier muss man die Größe der Arrays der Bilanz setzen, bevor sie gefüllt werden:

...
//--- Anzahl der Zeilen und Spalten
   m_rows_total    =strings_total-1;
   m_columns_total =elements_total;
//--- Sortieren nach Zeit in der ersten Spalte
   QuickSort(0,m_rows_total-1,0);
//--- Größe der Serien
   ::ArrayResize(m_symbol_balance,m_symbols_total);
   for(int i=0; i<m_symbols_total; i++)
      ::ArrayResize(m_symbol_balance[i].m_data,m_rows_total);
...

Zuerst füllen wir das Array der Gesamtbilanz und des Rückgangs mit den Werten. Alle Abschlüsse, die mit dem Einzahlen verbunden sind, werden ausgelassen.

...
//--- Bilanz und maximaler Rückgang
   double balance      =0.0;
   double max_drawdown =0.0;
//--- Abfragen der Daten der Gesamtbilanz
   for(uint i=0; i<m_rows_total; i++)
     { 
      //--- initiale Bilanz 
      if(i==0)
        { 
         balance+=(double)m_columns[elements_total-1].m_rows[i];
         m_symbol_balance[0].m_data[i]=balance;
        }
      else
        { 
         //--- "Balance"-Transaktionen überspringen
         if(m_columns[1].m_rows[i]=="Balance")
            m_symbol_balance[0].m_data[i]=m_symbol_balance[0].m_data[i-1];
         else
           { 
            balance+=(double)m_columns[elements_total-1].m_rows[i]+(double)m_columns[elements_total-2].m_rows[i]+(double)m_columns[elements_total-3].m_rows[i];
            m_symbol_balance[0].m_data[i]=balance;
           }
        }
      //--- Rückgang berechnen
      if(MaxDrawdownToString(i,balance,max_drawdown)!="")
         AddDrawDown(i,max_drawdown);
     }
...

Danach füllen wir die Arrays der Bilanz für jedes Symbol. 

...
//--- Abfragen der Daten der Bilanz der Symbole
   for(int s=1; s<m_symbols_total; s++)
     { 
      //--- initiale Bilanz 
      balance=m_symbol_balance[0].m_data[0];
      m_symbol_balance[s].m_data[0]=balance;
      +//---+
      for(uint r=0; r<m_rows_total; r++)
        { 
         //--- Wenn die Symbole nicht übereinstimmen, dann der vorherige Wert
         if(m_symbols_name[s]!=m_columns[m_symbol_index].m_rows[r])
           { 
            if(r>0)
               m_symbol_balance[s].m_data[r]=m_symbol_balance[s].m_data[r-1];
            +//---+
            continue;
           }
         //--- Wenn das Ergebnis des Abschlusses nicht gleich Null ist
         if((double)m_columns[elements_total-1].m_rows[r]!=0)
           { 
            balance+=(double)m_columns[elements_total-1].m_rows[r]+(double)m_columns[elements_total-2].m_rows[r]+(double)m_columns[elements_total-3].m_rows[r];
            m_symbol_balance[s].m_data[r]=balance;
           }
         //--- Andernfalls den vorherigen Wert schreiben
         else
            m_symbol_balance[s].m_data[r]=m_symbol_balance[s].m_data[r-1];
        }
     }
...

Danach werden die Daten in den Charts des grafischen Interfaces angezeigt. Unten sind einige Beispiele unterschiedlicher Signalanbieter angezeigt:

 Abb. 8 – Demonstration der Ergebnisse (Beispiel 1).

Abb. 8.  Demonstration der Ergebnisse (Beispiel 1).

 Abb. 9 – Demonstration der Ergebnisse (Beispiel 2).

Abb. 9.  Demonstration der Ergebnisse (Beispiel 2).

 Abb. 10 – Demonstration der Ergebnisse (Beispiel 3).

Abb. 10. Demonstration der Ergebnisse (Beispiel 3).

 Abb. 11 – Demonstration der Ergebnisse (Beispiel 4).

Abb. 11.  Demonstration der Ergebnisse (Beispiel 4).

Fazit

Der Artikel beschreibt eine moderne Version der MQL-Anwendung für die Ansicht von Multi-Symbol-Charts der Bilanz. Früher musste man dafür Drittprogramme anwenden. Nun kann man alles mit MQL implementieren, ohne das MetaTrader 5 Terminal zu verlassen.

Unten können Sie Dateien für das Testen herunterladen. Jede Version des Programms hat die folgende Dateistruktur: 

Name der Datei Kommentar
MacdSampleMultiSymbols.mq5 Modifizierter Expert Advisor aus dem Standardpaket - MACD Sample
Program.mqh Datei mit der Klasse des Programms
CreateGUI.mqh Datei mit der Implementierung der Methoden aus der Klasse des Programms in der Datei Program.mqh
Strategy.mqh Datei mit der modifizierten Klasse der Strategie MACD Sample (Multi-Symbol-Version)
FormatString.mqh Datei mit Hilfsfunktionen für das Formatieren von Strings

Übersetzt aus dem Russischen von MetaQuotes Software Corp.
Originalartikel: https://www.mql5.com/ru/articles/4430

Beigefügte Dateien |
MQL5.zip (42.73 KB)
Wie man eine Anforderungsspezifikation bei der Bestellung eines Indikators erstellt Wie man eine Anforderungsspezifikation bei der Bestellung eines Indikators erstellt

Händler suchen nach Gesetzmäßigkeiten im Verhalten des Marktes, die auf günstige Gelegenheiten für die Ausführung von Trades hinweisen. Am häufigsten ist der erste Schritt bei der Entwicklung eines Handelssystems die Erstellung eines technischen Indikators, der es erlaubt, benötigte Informationen im Preischart zu sehen. Der Artikel hilft Ihnen, eine Anforderungsspezifikation bei der Bestellung eines Indikators im Freelance zu erarbeiten.

Erstellen eines eigenen Newsfeeds für MetaTrader 5 Erstellen eines eigenen Newsfeeds für MetaTrader 5

In diesem Artikel untersuchen wir die Möglichkeit, einen flexiblen Newsfeed zu erstellen, der mehr Optionen in Bezug auf die Art der Nachrichten und auch deren Quelle bietet. Der Artikel zeigt, wie eine Web-API in das MetaTrader 5 Terminal integriert werden kann.

Money Management von Vince. Implementierung als Modul für MQL5 Wizard Money Management von Vince. Implementierung als Modul für MQL5 Wizard

Der Artikel basiert auf dem Buch 'The Mathematics of Money Management' von Ralph Vince. Es bietet eine Beschreibung der empirischen und parametrischen Methoden zur Ermittlung der optimalen Größe des Handelsvolumens. Der Artikel beinhaltet auch die Implementierung von Handelsmodulen für den MQL5 Wizard, die auf diesen Methoden basieren.

Die Darstellung der Optimierung einer Handelsstrategie im MetaTrader 5 Die Darstellung der Optimierung einer Handelsstrategie im MetaTrader 5

Der Artikel implementiert eine MQL-Anwendung mit einem grafischen Interface zur erweiterten Darstellung der Optimierung. Das grafische Interface verwendet die letzte Version der Bibliothek EasyAndFast. Viele Anwender fragen sich, warum MQL-Anwendungen überhaupt grafische Interfaces benötigen. Dieser Artikel zeigt einen von mehreren Fällen, die für Händler nützlich sein können.