Multi-Symbol-Chart der Bilanz in MetaTrader 5
Anatoli Kazharski | 9 April, 2018
Inhaltsverzeichnis
- Einleitung
- Erstellen eines grafischen Interfaces
- Multi-Symbol Expert Advisor für Tests
- Schreiben der Daten in eine Datei
- Exportieren der Daten aus der Datei
- Anzeige der Daten in Charts
- Demonstration des erhaltenen Ergebnisses
- Multi-Symbol-Chart der Bilanz während des Handels und der Tests
- Visualisieren der Berichte aus dem Signale-Service
- Fazit
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.
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 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.
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.
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 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 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. 9. Demonstration der Ergebnisse (Beispiel 2).
Abb. 10. Demonstration der Ergebnisse (Beispiel 3).
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 |