Die Behandlung der Ergebnisse der Optimierung mit einem grafischen Interface
Inhaltsverzeichnis
- Einführung
- Entwickeln des grafischen Interfaces
- Speichern der Optimierungsergebnisse
- Extrahieren der Daten aus einem Rahmen
- Datenvisualisierung und die Interaktion mit dem grafischen Interface
- Schlussfolgerungen
Einführung
Dies ist eine Fortsetzung der Idee der Verarbeitung und Analyse von Optimierungsergebnissen. Der vorherige Artikel enthielt die Beschreibung der Art und Weise, wie Optimierungsergebnisse mit der grafischen Interface der MQL5-Anwendung visualisiert werden können. Diesmal ist die Aufgabe komplizierter: Wir wählen die 100 besten Optimierungsergebnisse aus und zeigen sie im grafischen Interface an.
Darüber hinaus entwickeln wir die Idee eines Saldos mehrerer Symbole weiter, die auch in einem eigenen Artikel vorgestellt wurde. Lassen Sie uns die Ideen dieser beiden Artikel kombinieren und es dem Benutzer ermöglichen, eine Zeile in der Optimierungsergebnistabelle auszuwählen und eine Multi-Symbol-Saldo und eine Drawdown-Grafik auf separaten Diagrammen zu erhalten. Nach der Optimierung der Parameter des Expert Advisors kann der Händler eine schnelle Analyse der Ergebnisse durchführen und geeignete Werte auswählen.
Entwickeln des grafischen Interfaces
Das GUI des Test Expert Advisors besteht aus folgenden Elementen.
- Aussehen der Kontrollelemente
- Statusleiste für die Anzeige zusätzlicher Übersichtsinformationen
- Registerkarten zum Anordnen von Elementen in Gruppen:
- Rahmen (Frames)
- Eingabefeld zur Verwaltung der Anzahl der angezeigten Saldenergebnisse beim erneuten Blättern nach der Optimierung
- Verzögerung in Millisekunden beim Scrollen durch die Ergebnisse
- Taste zum Starten des erneuten Scrollens durch die Ergebnisse
- Grafische Darstellung der vorgegebenen Anzahl von Saldenergebnissen
- Grafik aller Ergebnisse
- Ergebnisse
- Tabelle der besten Ergebnisse
- Saldo
- Grafik des Multi-Symbol-Saldos für das in der Tabelle ausgewählte Ergebnis
- Drwadown-Diagramm für das in der Tabelle ausgewählte Ergebnis
- Indikation für den Rahmen-Wiedergabeprozess
Der Code der Methoden zur Erzeugung der oben aufgeführten Elemente ist als separate Include-Datei für die Verwendung mit der MQL-Programmklasse verfügbar:
//+------------------------------------------------------------------+ //| Klasse zum Erstellen einer Anwendung | //+------------------------------------------------------------------+ class CProgram : public CWndEvents { private: //--- Fenster CWindow m_window1; //--- Status Bar CStatusBar m_status_bar; //--- Tabs CTabs m_tabs1; //--- Bearbeitet CTextEdit m_curves_total; CTextEdit m_sleep_ms; //--- Tasten CButton m_reply_frames; //--- Charts CGraph m_graph1; CGraph m_graph2; CGraph m_graph3; CGraph m_graph4; //--- Tabellen CTable m_table_param; //--- Fortschrittsanzeige CProgressBar m_progress_bar; //--- public: //--- Erstellen des grafischen Interfaces bool CreateGUI(void); //--- private: //--- Form bool CreateWindow(const string text); //--- Status Bar bool CreateStatusBar(const int x_gap,const int y_gap); //--- Tabs bool CreateTabs1(const int x_gap,const int y_gap); //--- Bearbeitet bool CreateCurvesTotal(const int x_gap,const int y_gap,const string text); bool CreateSleep(const int x_gap,const int y_gap,const string text); //--- Tasten bool CreateReplyFrames(const int x_gap,const int y_gap,const string text); //--- Charts bool CreateGraph1(const int x_gap,const int y_gap); bool CreateGraph2(const int x_gap,const int y_gap); bool CreateGraph3(const int x_gap,const int y_gap); bool CreateGraph4(const int x_gap,const int y_gap); //--- Tasten bool CreateUpdateGraph(const int x_gap,const int y_gap,const string text); //--- Tabellen bool CreateMainTable(const int x_gap,const int y_gap); //--- Fortschrittsanzeige bool CreateProgressBar(const int x_gap,const int y_gap,const string text); }; //+------------------------------------------------------------------+ //| Methoden zur Erstellen der Kontrollelemente | //+------------------------------------------------------------------+ #include "CreateGUI.mqh" //+------------------------------------------------------------------+
Wie oben erwähnt, zeigt die Tabelle die 100 besten Optimierungsergebnisse (bezogen auf den größten Endgewinn). Da das GUI vor dem Start der Optimierung erstellt wird, ist die Tabelle zunächst leer. Die Anzahl der Spalten und Texte für Überschriften wird in der Verarbeitungsklasse des Optimierungsrahmens festgelegt.
Lassen Sie uns eine Tabelle mit den folgenden Funktionen erstellen.
- Anzeige der Überschriften
- Sortieroption
- Markieren einer Zeile
- Fixieren einer markierten Zeile (ohne die Möglichkeit, die Markierung aufzuheben)
- Manuelle Einstellung der Spaltenbreite
- Formatierung im Zebra-Stil
Der Code für die Erstellung der Tabelle ist unten dargestellt. Um die Tabelle in der zweiten Registerkarte zu fixieren, sollte das Tabellenobjekt mit der Angabe des Registerindexes an das Tabulatorobjekt übergeben werden. In diesem Fall ist die Hauptklasse der Tabelle das Element 'Tabs'. Wenn also die Größe des Tabulatorbereichs geändert wird, ändert sich die Größe der Tabelle relativ zu ihrem Hauptelement, vorausgesetzt, dies ist angegeben in Elementeigenschaften 'Tabelle'.
//+------------------------------------------------------------------+ //| Erstellen der Haupttabelle | //+------------------------------------------------------------------+ bool CProgram::CreateMainTable(const int x_gap,const int y_gap) { //--- Sichern des Pointers im Hauptsteuerelement m_table_param.MainPointer(m_tabs1); //--- Attach to tab m_tabs1.AddToElementsArray(1,m_table_param); //--- Eigenschaften m_table_param.TableSize(1,1); m_table_param.ShowHeaders(true); m_table_param.IsSortMode(true); m_table_param.SelectableRow(true); m_table_param.IsWithoutDeselect(true); m_table_param.ColumnResizeMode(true); m_table_param.IsZebraFormatRows(clrWhiteSmoke); m_table_param.AutoXResizeMode(true); m_table_param.AutoYResizeMode(true); m_table_param.AutoXResizeRightOffset(2); m_table_param.AutoYResizeBottomOffset(2); //--- Erstellen des Kontrollelements if(!m_table_param.CreateTable(x_gap,y_gap)) return(false); //--- Hinzufügen eines Objekts zum gemeinsamen Array der Objektgruppen CWndContainer::AddToElementsArray(0,m_table_param); return(true); }
Speichern der Optimierungsergebnisse
Die Klasse CFrameGenerator ist für die Arbeit mit Optimierungsergebnissen implementiert. Wir werden eine Version aus dem Artikel Visualisierung der Handelsstrategieoptimierung in MetaTrader 5 verwenden und die notwendige Methoden hinzufügen. Zusätzlich zur Speicherung des Gesamtsaldos und der Endstatistik im Rahmen müssen wir den Saldo und den Drawdown für jedes Symbol separat speichern. Die separate Array-Struktur CSymbolBalance wird zum Speichern der Salden verwendet. Die Struktur hat einen doppelten Zweck. Die in den Arrays gespeicherten Daten werden dann an einen Rahmen in einem gemeinsamen Array übergeben. Nach der Optimierung werden die Daten aus dem Rahmen-Array extrahiert und an die Arrays dieser Struktur zurückgegeben, um sie in Multi-Symbol-Salden-Graphen darzustellen.
//--- Array der Salden aller Symbole struct CSymbolBalance { double m_data[]; }; //+------------------------------------------------------------------+ //| Klasse für die Arbeit mit den Optimierungsergebnissen | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- Struktur der Salden CSymbolBalance m_symbols_balance[]; };
Die Aufzählung der durch ',' getrennten Symbole wird als String-Parameter an den Rahmen übergeben. Ursprünglich sollten die Daten in einem Rahmen als vollständiger Bericht in einem String-Array gespeichert werden. String-Arrays können derzeit jedoch nicht an einen Rahmen übergeben werden. Der Versuch, ein String-Array an die Funktion FrameAdd() zu übergeben, führt zu einem Fehler während der Kompilierung:
String-Arrays und Strukturen, die Objekte enthalten, sind nicht erlaubt.
Eine weitere Möglichkeit besteht darin, den Bericht in eine Datei zu schreiben und diese Datei dem Rahmen zu übergeben. Diese Option ist jedoch nicht geeignet: Wir müssten die Ergebnisse zu oft auf einer Festplatte speichern.
Deshalb habe ich beschlossen, alle notwendigen Daten in einem Array zu sammeln und dann die Daten auf Basis der in den Rahmenparametern enthaltenen Schlüssel zu extrahieren. Statistische Variablen werden am Anfang des Arrays enthalten sein. Es folgen der Gesamtsaldo und separate Saldenwerte pro Symbol. Am Ende befinden sich die Drawdowns auf zwei Achsen getrennt voneinander.
Das folgende Schema zeigt die Reihenfolge der gepackten Daten im Array. Eine Variante mit zwei Symbolen wird gezeigt, um das Schema kurz genug zu halten.
Abb. 1. Reihenfolge der Datenanordnung im Array.
Wir benötigen also Schlüssel für die Bestimmung der Indizes jedes Bereichs im Array. Die Anzahl der statistischen Variablen ist konstant und wird im Voraus festgelegt. Wir werden in der Tabelle fünf Variablen und eine Durchlaufnummer anzeigen, um sicherzustellen, dass auf die Daten dieses Ergebnisses nach der Optimierung zugegriffen werden kann:
//--- Anzahl der statistischen Werte #define STAT_TOTAL 6
- Durchlauf
- Testergebnis
- Profit (STAT_PROFIT)
- Positionsanzahl (STAT_TRADES)
- Drawdown (STAT_EQUITY_DDREL_PERCENT)
- Erholungsfaktor (STAT_RECOVERY_FACTOR)
Der Umfang der Saldendaten wird gleich sein für alle Daten und den individuellen Symboldaten. Dieser Wert wird an die Funktion FrameAdd() als double gesendet. Um die beim Testen verwendeten Symbole zu bestimmen, definieren wir sie bei jedem Durchgang in der Funktion OnTester() basierend auf der Historie der Positionen. Diese Information wird an die Funktion FrameAdd() als string gesendet.
::FrameAdd(m_report_symbols,1,data_count,stat_data);
Die im String-Parameter angegebene Zeichenfolge entspricht der Datenfolge im Array. Wenn wir alle diese Parameter haben, können wir die Daten, die in das Array gepackt sind, richtig extrahieren.
Die Methode CFrameGenerator::GetHistorySymbols() zur Bestimmung von Symbolen in der Historie von Geschäften wird im folgenden Code dargestellt:
#include <Trade\DealInfo.mqh> //+------------------------------------------------------------------+ //| Klasse für die Arbeit mit den Optimierungsergebnissen | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- Arbeiten mit den Positionen CDealInfo m_deal_info; //--- Symbole für den Bericht string m_report_symbols; //--- private: //--- Abfrage der Symbole aus der Kontohistorie und Rückgabe der Anzahl int GetHistorySymbols(void); }; //+------------------------------------------------------------------+ //| Abfrage der Symbole in der Kontohistorie und Rückgabe der Anzahl | //+------------------------------------------------------------------+ int CFrameGenerator::GetHistorySymbols(void) { //--- Erster Schleifendurchlauf und Abfrage der gehandelten Symbole int deals_total=::HistoryDealsTotal(); for(int i=0; i<deals_total; i++) { //--- Abfrage der Ticketnummer if(!m_deal_info.SelectByIndex(i)) continue; //--- Gibt es einen Symbolnamen if(m_deal_info.Symbol()=="") continue; //--- Gibt es das Symbol nicht, füge es hinzu if(::StringFind(m_report_symbols,m_deal_info.Symbol(),0)==-1) ::StringAdd(m_report_symbols,(m_report_symbols=="")? m_deal_info.Symbol() : ","+m_deal_info.Symbol()); } //--- Abfrage der Elemente entspr. der Trennzeichen ushort u_sep=::StringGetCharacter(",",0); int symbols_total=::StringSplit(m_report_symbols,u_sep,m_symbols_name); //--- Rückgabe der Nummer des Symbols return(symbols_total); }
Falls die geschlossenen Positionen mehr als ein Symbol betreffen, wird das Array um Eins erhöht. Das erste Element ist reserviert für den Gesamtsaldo.
//--- Setzen der Größe des Saldenarrays auf Nummer des Symbols +1 für das Gesamtsaldo ::ArrayResize(m_symbols_balance,(m_symbols_total>1)? m_symbols_total+1 : 1);
Sobald alle Daten aus der Positionshistorie in separaten Arrays gespeichert sind, sollten sie in einem gemeinsamen Array abgelegt werden. Dazu wird die Methode CFrameGenerator::CopyDataToMainArray() verwendet. Hier erhöhen wir sequentiell das gemeinsame Array um die Menge der hinzugefügten Daten in einer Schleife. Dann, während der letzten Iteration, kopieren wir den Drawdown.
class CFrameGenerator { private: //--- Ergebnissaldo double m_balances[]; //--- private: //--- Kopieren der Salden in das Hauptarray void CopyDataToMainArray(void); }; //+------------------------------------------------------------------+ //| Kopieren der Salden in das Hauptarray | //+------------------------------------------------------------------+ void CFrameGenerator::CopyDataToMainArray(void) { //--- Anzahl der Saldenkurven int balances_total=::ArraySize(m_symbols_balance); //--- Größe des Saldenarrays int data_total=::ArraySize(m_symbols_balance[0].m_data); //--- Ausfüllen des gemeinsamen Array mit Daten for(int i=0; i<=balances_total; i++) { //--- Aktueller Saldo int array_size=::ArraySize(m_balances); //--- Kopieren des Saldos in das Array if(i<balances_total) { //--- Kopieren des Saldos in das Array ::ArrayResize(m_balances,array_size+data_total); ::ArrayCopy(m_balances,m_symbols_balance[i].m_data,array_size); } //--- Kopieren des DD in das Array else { data_total=::ArraySize(m_dd_x); ::ArrayResize(m_balances,array_size+(data_total*2)); ::ArrayCopy(m_balances,m_dd_x,array_size); ::ArrayCopy(m_balances,m_dd_y,array_size+data_total); } } }
Statistische Variablen werden am Anfang des gemeinsamen Arrays von der Methode CFrameGenerator::GetStatData() hinzugefügt. Das Array, das schließlich im Rahmen gespeichert wird, wird dieser Methode per Referenz übergeben. Seine Größe wird als die Größe des Saldenarrays plus der Anzahl der statistischen Variablen festgelegt. Die Saldendaten werden aus dem letzten Index in den Bereich der statistischen Variablen gestellt.
class CFrameGenerator { private: //--- Abfrage der statistischen Daten void GetStatData(double &dst_array[],double on_tester_value); }; //+------------------------------------------------------------------+ //| Abfrage der statistischen Daten | //+------------------------------------------------------------------+ void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value) { //--- Kopieren des Arrays ::ArrayResize(dst_array,::ArraySize(m_balances)+STAT_TOTAL); ::ArrayCopy(dst_array,m_balances,STAT_TOTAL,0); //--- Eintragen der Testergebnisse in die ersten Werte des Arrays (STAT_TOTAL) dst_array[0] =0; // Durchlaufnummer dst_array[1] =on_tester_value; // Optimierungswert des Nutzerkriteriums dst_array[2] =::TesterStatistics(STAT_PROFIT); // Nettogewinn dst_array[3] =::TesterStatistics(STAT_TRADES); // Positionsanzahl dst_array[4] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // max. DD in % dst_array[5] =::TesterStatistics(STAT_RECOVERY_FACTOR); // Erholungsfaktor }
Das oben beschriebene Vorgehen wird von der Methode CFrameGenerator::OnTesterEvent() durchgeführt, die im Hauptprogramm von der Funktion OnTester() aufgerufen wird.
//+------------------------------------------------------------------+ //| Vorbereiten des Saldenarrays und an den Rahmen senden | //| Aufruf der Funktion in der Funktion OnTester() des EAs | //+------------------------------------------------------------------+ void CFrameGenerator::OnTesterEvent(const double on_tester_value) { //--- Abfragen des Saldos int data_count=GetBalanceData(); //--- Array zum Senden an den Rahmen double stat_data[]; GetStatData(stat_data,on_tester_value); //--- Erstellen des Datenrahmens und absenden zum Terminal if(!::FrameAdd(m_report_symbols,1,data_count,stat_data)) ::Print(__FUNCTION__," > Frame add error: ",::GetLastError()); else ::Print(__FUNCTION__," > Frame added, OK"); }
Die Tabellen-Arrays werden am Ende der Optimierung in der Methode FinalRecalculateFrames() gefüllt, die in der Methode CFrameGenerator::OnTesterDeinitEvent() aufgerufen wird. Hier werden folgende Aktionen durchgeführt: die endgültige Neuberechnung der Optimierungsergebnisse, die Bestimmung der Anzahl der optimierten Parameter, das Füllen des Arrays von Tabellenköpfen, das Sammeln von Daten in Tabellenarrays. Danach werden die Daten nach den angegebenen Kriterien sortiert.
Betrachten wir einige Hilfsmethoden, die im letzten Verarbeitungszyklus des Rahmens aufgerufen werden. Beginnen wir mit CFrameGenerator::GetParametersTotal(), der die Anzahl der bei der Optimierung verwendeten EA-Parameter bestimmt.
Die Funktion FrameInputs() wird aufgerufen, um die Parameter des Expert Advisors aus dem Rahmen zu erhalten. Durch die Übergabe der Nummer des Durchlaufs an diese Funktion können wir ein Array von Parametern und deren Anzahl erhalten. Die in der Optimierung verwendeten Parameter werden zuerst aufgelistet, dann werden andere Parameter angezeigt. In der Tabelle werden nur Optimierungsparameter angezeigt, deshalb müssen wir den Index des ersten nicht optimierten Parameters bestimmen - dies hilft uns, die Gruppe zu entfernen, die nicht in der Tabelle enthalten sein sollte. Wir können den ersten nicht optimierten externen EA-Parameter im Voraus angeben, den das Programm verwenden wird. In diesem Fall ist dies Symbols. Wenn wir den Index kennen, können wir die Anzahl der Optimierungsparameter des Expert Advisors berechnen.
class CFrameGenerator { private: //--- De erste nicht-optimierte Parameter string m_first_not_opt_param; //--- private: //--- Abfrage der Anzahl der zu optimierenden Parameter void GetParametersTotal(void); }; //+------------------------------------------------------------------+ //| Konstruktor | //+------------------------------------------------------------------+ CFrameGenerator::CFrameGenerator(void) : m_first_not_opt_param("Symbols") { } //+------------------------------------------------------------------+ //| Abfrage der Anzahl der zu optimierenden Parameter | //+------------------------------------------------------------------+ void CFrameGenerator::GetParametersTotal(void) { //--- im ersten Rahmen Bestimmen der Nummer des Optimierungsparameter if(m_frames_counter<1) { //--- Abfrage der Eingabeparameter des Expert Advisors, für den der Rahmen erstellt wurde ::FrameInputs(m_pass,m_param_data,m_par_count); //--- Finden des Index des ersten nicht-optimierten Parameters int limit_index=0; int params_total=::ArraySize(m_param_data); for(int i=0; i<params_total; i++) { if(::StringFind(m_param_data[i],m_first_not_opt_param)>-1) { limit_index=i; break; } } //--- Die Anzahl der zu optimierenden Parameter m_param_total=(m_par_count-(m_par_count-limit_index)); } }
Die Daten der Tabelle werden in dem Array der Struktur CReportTable gespeichert. Nachdem wir die Anzahl der zu optimierenden Parameter des EAs herausgefunden haben, könne wir die Anzahl der Spalten der Tabelle bestimmen. Dies geschieht in der Methode CFrameGenerator::SetColumnsTotal(). Die Anzahl der Zeilen ist anfangs Null.
//--- Tabellenarray struct CReportTable { string m_rows[]; }; //+------------------------------------------------------------------+ //| Klasse für die Arbeit mit den Optimierungsergebnissen | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- Tabelle des Berichts CReportTable m_columns[]; //--- private: //--- Setzen der Spaltenzahl der Tabelle void SetColumnsTotal(void); }; //+------------------------------------------------------------------+ //| Setzen der Spaltenzahl der Tabelle | //+------------------------------------------------------------------+ void CFrameGenerator::SetColumnsTotal(void) { //--- Bestimmen der Spaltenzahl der Ergebnistabelle if(m_frames_counter<1) { int columns_total=int(STAT_TOTAL+m_param_total); ::ArrayResize(m_columns,columns_total); for(int i=0; i<columns_total; i++) ::ArrayFree(m_columns[i].m_rows); } }
Die Zeilen werden in der Methode CFrameGenerator::AddRow() hinzugefügt. Bei der Arbeit mit Rahmen werden nur Ergebnisse auf Grund von Positionen zur Tabelle hinzugefügt. Die ersten Spalten der Tabelle zeigen die Durchlaufnummer, statistische Variablen und dann die Optimierungsparameter des Expert Advisors. Wenn Parameter aus einem Rahmen gewonnen wurden, stehen sie im Format "parameterN=valueN" [Parametername][Trennzeichen][Parameterwert] zur Verfügung. Wir benötigen nur Parameterwerte, die der Tabelle hinzugefügt werden sollen. Deshalb lassen Sie uns die Zeile nach dem Trennzeichen '=' zerteilen und den Wert aus dem zweiten Element des Arrays speichern.
class CFrameGenerator { private: //--- Hinzufügen einer Datenzeile void AddRow(void); }; //+------------------------------------------------------------------+ //| Hinzufügen einer Datenzeile | //+------------------------------------------------------------------+ void CFrameGenerator::AddRow(void) { //--- Setzen der Spaltenzahl der Ergebnistabelle SetColumnsTotal(); //--- Exit wen es keine Positionen gibt if(m_data[3]<1) return; //--- Ausfüllen der Tabelle int columns_total=::ArraySize(m_columns); for(int i=0; i<columns_total; i++) { //--- Hinzufügen einer Zeile int prev_rows_total=::ArraySize(m_columns[i].m_rows); ::ArrayResize(m_columns[i].m_rows,prev_rows_total+1,RESERVE); //--- Durchlaufnummer if(i==0) { m_columns[i].m_rows[prev_rows_total]=string(m_pass); continue; } //--- Statistical parameters if(i<STAT_TOTAL) m_columns[i].m_rows[prev_rows_total]=string(m_data[i]); //--- EA Optimierungsparameter else { string array[]; if(::StringSplit(m_param_data[i-STAT_TOTAL],'=',array)==2) m_columns[i].m_rows[prev_rows_total]=array[1]; } } }
Die Tabellenköpfe werden von der speziellen MethodeCFrameGenerator::GetHeaders() gewonnen - das erste Element des Arrayelements der zerteilten Zeile.
class CFrameGenerator { private: //--- Abfrage der Tabellenköpfe void GetHeaders(void); }; //+------------------------------------------------------------------+ //| Abfrage der Tabellenköpfe | //+------------------------------------------------------------------+ void CFrameGenerator::GetHeaders(void) { int columns_total =::ArraySize(m_columns); //--- Kopfzeile ::ArrayResize(m_headers,STAT_TOTAL+m_param_total); for(int c=STAT_TOTAL; c<columns_total; c++) { string array[]; if(::StringSplit(m_param_data[c-STAT_TOTAL],'=',array)==2) m_headers[c]=array[0]; } }
Verwenden wir die einfache Methode CFrameGenerator::ColumnSortIndex(), um dem Programm mitzuteilen, welches Kriterium es verwenden soll, um 100 Optimierungsergebnisse für die Tabelle auszuwählen. Der Spaltenindex wird an die Methode übergeben. Nach Abschluss der Optimierung wird die Ergebnistabelle nach diesem Index absteigend sortiert und die Top-100-Ergebnisse werden in die Tabelle aufgenommen und in der grafischen Oberfläche angezeigt. Die dritte Spalte (Index 2) ist standardmäßig gesetzt, d.h. die Ergebnisse werden nach dem maximalen Gewinn sortiert.
class CFrameGenerator { private: //--- Der Index der sortierten Tabelle uint m_column_sort_index; //--- public: //--- Bestimmen des Spaltenindex, nach dem die Tabelle sortiert werden soll void ColumnSortIndex(const uint index) { m_column_sort_index=index; } }; //+------------------------------------------------------------------+ //| Konstruktor | //+------------------------------------------------------------------+ CFrameGenerator::CFrameGenerator(void) : m_column_sort_index(2) { }
Wenn Sie Ergebnisse basierend auf einem anderen Kriterium abrufen müssen, sollte CFrameGenerator::ColumnSortIndex() in der CProgram::OnTesterInitEvent() Methode am Anfang der Optimierung aufgerufen werden:
//+------------------------------------------------------------------+ //| Ereignis zum Starten der Optimierung | //+------------------------------------------------------------------+ void CProgram::OnTesterInitEvent(void) { ... m_frame_gen.ColumnSortIndex(3); ... }
Die Methode CFrameGenerator::FinalRecalculateFrames() zur endgültigen Neuberechnung des Rahmens arbeitet nun nach folgendem Algorithmus.
- Bewegen Sie den Rahmenzeiger auf den Listenanfang. Setzen Sie den Zähler der Rahmen und Arrays zurück.
- Iterieren Sie über alle Rahmen in einer Schleife und:
- holen der Anzahl der Optimierungsparameter,
- negative und positive Ergebnisse auf die Arrays verteilen,
- einfügen einer Zeile zur Tabelle.
- Nach dem Iterationszyklus des Rahmens holen wir uns die Tabellenüberschriften.
- Dann folgt das Sortieren der Tabelle nach der Spalte angegeben in den Einstellungen.
- Die Methode wird durch die Aktualisierung des Optimierungsgraphen vervollständigt.
Der Code von CFrameGenerator::FinalRecalculateFrames():
class CFrameGenerator { private: //--- Letzte Datenberechnung aller Rahmen nach der Optimierung void FinalRecalculateFrames(void); }; //+------------------------------------------------------------------+ //| Letzte Datenberechnung aller Rahmen nach der Optimierung | //+------------------------------------------------------------------+ void CFrameGenerator::FinalRecalculateFrames(void) { //--- Verschieben des Pointers auf den Rahmen an de Anfang ::FrameFirst(); //--- Rücksetzen der Zähler und der Arrays ArraysFree(); m_frames_counter=0; //--- Start der Schleife durch die Rahmen while(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { //--- Abfrage der Anzahl der zu optimierenden Parameter GetParametersTotal(); //--- Negative Ergebnisse if(m_data[m_profit_index]<0) AddLoss(m_data[m_profit_index]); //--- Positive Ergebnisse else AddProfit(m_data[m_profit_index]); //--- Hinzufügen einer Datenzeile AddRow(); //--- Erhöhen des Rahmenzählers m_frames_counter++; } //--- Abfrage der Tabellenköpfe GetHeaders(); //--- Anzahl der Spalten und Zeilen int rows_total =::ArraySize(m_columns[0].m_rows); //--- Sortieren der Tabelle nach der angegebenen Spalte QuickSort(0,rows_total-1,m_column_sort_index); //--- Aktualisieren der Reihe im Chart CCurve *curve=m_graph_results.CurveGetByIndex(0); curve.Name("P: "+(string)ProfitsTotal()); curve.Update(m_profit_x,m_profit_y); //--- curve=m_graph_results.CurveGetByIndex(1); curve.Name("L: "+(string)LossesTotal()); curve.Update(m_loss_x,m_loss_y); //--- Eigenschaften der horizontalen Achse CAxis *x_axis=m_graph_results.XAxis(); x_axis.Min(0); x_axis.Max(m_frames_counter); x_axis.DefaultStep((int)(m_frames_counter/8.0)); //--- Aktualisieren des Diagramms m_graph_results.CalculateMaxMinValues(); m_graph_results.CurvePlotAll(); m_graph_results.Update(); }
Als nächstes betrachten wir die Methoden, die verwendet werden, um Daten von einem Rahmen auf Anforderung des Benutzers zu empfangen.
Extrahieren der Daten aus einem Rahmen
Wir haben die Struktur eines gemeinsamen Arrays mit der Reihenfolge der Daten verschiedener Kategorien betrachtet. Jetzt müssen wir verstehen, wie Daten aus diesem Array extrahiert werden. Die Rahmen enthalten die Salden und die Aufzählung der Symbole als Schlüssel. Wenn die Größe der Saldenarrays gleich der Größe der Drawdown-Arrays wäre, könnten wir die Indizes aller Bereiche der gepackten Daten durch eine einzige Formel in einem Zyklus bestimmen, wie im folgenden Schema. Aber die Größen der Arrays sind unterschiedlich. Daher müssen wir während der letzten Iteration im Zyklus bestimmen, wie viele Elemente im Datenbereich verbleiben, der sich auf Drawdowns bezieht, und sie durch zwei teilen, da die Größen der Drawdown-Arrays gleich sind.
Abb. 2. Ein Schema mit Parametern zur Berechnung des Index des Arrays aus der nächsten Kategorie.
Die public Methode CFrameGenerator::GetFrameData() ist implementiert, um Daten aus einem Rahmen zu erhalten. Betrachten wir das genauer.
Am Anfang der Methode müssen wir den Rahmenzeiger auf den Listenanfang bewegen. Danach beginnt der Iterationsprozess aller Rahmen mit den Optimierungsergebnissen. Wir müssen den Rahmen finden, dessen Durchlaufnummer als Argument an die Methode übergeben wurde. Wenn es gefunden wird, arbeitet das Programm nach dem folgenden Algorithmus weiter.
- Die Größe des gemeinsamen Arrays mit den Rahmendaten wird ermittelt.
- Elemente der String-Parameterzeile und die Anzahl solcher Elemente werden ermittelt. Gibt es mehr als ein Symbol, wird die Anzahl der Salden im Array um eins erhöht. Der erste Bereich ist also der Gesamtsaldo, andere Bereiche gelten für Salden nach Symbolen.
- Als nächstes müssen die Daten in die Arrays der Salden verschoben werden. Wir führen einen Zyklus durch, um Daten aus dem gemeinsamen Array zu extrahieren (die Anzahl der Iterationen ist gleich der Anzahl der Salden). Um den ersten Index zu bestimmen, der mit dem Kopieren von Daten beginnt, verschieben wir um die Anzahl der statistischen Variablen (STAT_TOTAL) und multiplizieren den Iterationsindex (i) mit der Größe des Salden-Arrays (m_value). So erhalten wir bei jeder Iteration die Daten aller Salden in separate Arrays.
- Während der letzten Iteration erhalten wir Drawdown-Daten in separate Arrays. Dies sind die letzten Daten im Array, so dass wir nur die verbleibende Anzahl der Elemente herausfinden und durch 2 teilen müssen. Weiter, in zwei aufeinander folgenden Schritten erhalten wir Drawdown-Daten.
- Der letzte Schritt besteht darin, die Diagramme mit neuen Daten zu aktualisieren und den Iterationszyklus zu stoppen.
class CFrameGenerator { public: //--- Datenabfrage der angegebenen Rahmennummer void GetFrameData(const ulong pass_number); }; //+------------------------------------------------------------------+ //| Datenabfrage der angegebenen Rahmennummer | //+------------------------------------------------------------------+ void CFrameGenerator::GetFrameData(const ulong pass_number) { //--- Verschieben des Pointers auf den Rahmen an de Anfang ::FrameFirst(); //--- Datenabfrage while(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { //--- Passt die Durchlaufnummer nicht, gehe eins weiter if(m_pass!=pass_number) continue; //--- Die Größe des Datenarrays int data_total=::ArraySize(m_data); //--- Abfrage der Elemente entspr. der Trennzeichen ushort u_sep =::StringGetCharacter(",",0); int symbols_total =::StringSplit(m_name,u_sep,m_symbols_name); int balances_total =(symbols_total>1)? symbols_total+1 : symbols_total; //--- Setzen der Größe des Saldenarrays ::ArrayResize(m_symbols_balance,balances_total); //--- Verteilen der Daten auf die Arrays for(int i=0; i<balances_total; i++) { //--- Freigeben des Datenarrays ::ArrayFree(m_symbols_balance[i].m_data); //--- Bestimmen des Index ab dem die Quelldaten kopiert werden sollen int src_index=STAT_TOTAL+int(i*m_value); //--- Kopieren der Daten in das Array der Struktur der Salden ::ArrayCopy(m_symbols_balance[i].m_data,m_data,0,src_index,(int)m_value); //--- Abfrage des DD, wenn es der letzte Schleifendurchlauf ist if(i+1==balances_total) { //--- Abfrage des Umfangs der verbliebenen Daten und des Arrays entlang beider Achsen double dd_total =data_total-(src_index+(int)m_value); double array_size =dd_total/2.0; //--- Anfangsindex für das Kopieren src_index=int(data_total-dd_total); //--- Größenbestimmung des Arrays der Drawdowns ::ArrayResize(m_dd_x,(int)array_size); ::ArrayResize(m_dd_y,(int)array_size); //--- Kopieren der Daten der Reihe nach ::ArrayCopy(m_dd_x,m_data,0,src_index,(int)array_size); ::ArrayCopy(m_dd_y,m_data,0,src_index+(int)array_size,(int)array_size); } } //--- Aktualisieren des Diagramms und Schleife beenden UpdateMSBalanceGraph(); UpdateDrawdownGraph(); break; } }
Um Daten aus den Zellen des Tabellenarrays zu erhalten, rufen wir die public Methode CFrameGenerator::GetValue() auf, die den Index der Tabellenspalte und -zeile in ihren Argumenten angibt.
class CFrameGenerator { public: //--- Rückgabewert der angegebenen Zelle string GetValue(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| Rückgabewert der angegebenen Zelle | //+------------------------------------------------------------------+ string CFrameGenerator::GetValue(const uint column_index,const uint row_index) { //--- Prüfung der Einhaltung der Spaltengrenzen uint csize=::ArraySize(m_columns); if(csize<1 || column_index>=csize) return(""); //--- Prüfung der Einhaltung der Zeilengrenzen uint rsize=::ArraySize(m_columns[column_index].m_rows); if(rsize<1 || row_index>=rsize) return(""); //--- return(m_columns[column_index].m_rows[row_index]); }
Datenvisualisierung und die Interaktion mit dem grafischen Interface
Zwei weitere Objekte vom Typ CGraphic werden in der Klasse CFrameGenerator für die Aktualisierung von Diagrammen durch Anwendung von Salden- und Drawdown-Daten deklariert. Wie bei anderen Objekten des gleichen Typs in CFrameGenerator, müssen wir Zeiger auf GUI-Elemente in ihnen, auf die CFrameGenerator::OnTesterInitEvent() Methode am Anfang der Optimierung übergeben.
#include <Graphics\Graphic.mqh> //+------------------------------------------------------------------+ //| Klasse für die Arbeit mit den Optimierungsergebnissen | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- Pointer auf das Diagramm der dargestellten Daten CGraphic *m_graph_ms_balance; CGraphic *m_graph_drawdown; //--- public: //--- Ereignisbehandlung des Strategietesters void OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results,CGraphic *graph_ms_balance,CGraphic *graph_drawdown); }; //+------------------------------------------------------------------+ //| Sollte innerhalb von OnTesterInit() aufgerufen werden | //+------------------------------------------------------------------+ void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results, CGraphic *graph_ms_balance,CGraphic *graph_drawdown) { m_graph_balance =graph_balance; m_graph_results =graph_results; m_graph_ms_balance =graph_ms_balance; m_graph_drawdown =graph_drawdown; }
Daten in der Tabelle der grafischen Oberfläche werden mit der Methode CProgram::GetFrameDataToTable() angezeigt. Lassen Sie uns die Anzahl der Spalten bestimmen, indem wir Tabellenköpfe in ein Array aufnehmen. Die Spaltenköpfe werden dem Objekt CFrameGenerator entnommen. Danach setzen wir die Tabellengröße (100 Zeilen) in der grafischen Oberfläche . Dann werden die Überschriften und der Datentyp gesetzt.
Nun müssen wir die Tabelle mit Hilfe der Optimierungsergebnisse initialisieren. Werte zur Tabelle werden über CTable::SetValue() gesetzt. Die Methode CFrameGenerator::GetValue() wird verwendet, um Werte aus Datentabellenzellen zu erhalten. Aktualisieren der zu übernehmenden Tabelle.
class CProgram { private: //--- Abfrage der Rahmendaten der Tabelle der Optimierungsergebnisse void GetFrameDataToTable(void); }; //+------------------------------------------------------------------+ //| Abfrage der Rahmendaten der Tabelle der Optimierungsergebnisse | //+------------------------------------------------------------------+ void CProgram::GetFrameDataToTable(void) { //--- Abfrage der Tabellenköpfe string headers[]; m_frame_gen.CopyHeaders(headers); //--- Setzen der Tabellengröße uint columns_total=::ArraySize(headers); m_table_param.Rebuilding(columns_total,100,true); //--- Festlegen der Kopfzeile und des Datentyp for(uint c=0; c<columns_total; c++) { m_table_param.DataType(c,TYPE_DOUBLE); m_table_param.SetHeaderText(c,headers[c]); } //--- Ausfüllen der Tabelle mit den Daten des Rahmens for(uint c=0; c<columns_total; c++) { for(uint r=0; r<m_table_param.RowsTotal(); r++) { if(c==1 || c==2 || c==4 || c==5) m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),2); else m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),0); } } //--- Tabelle aktualisieren m_table_param.Update(true); m_table_param.GetScrollHPointer().Update(true); m_table_param.GetScrollVPointer().Update(true); }
Die Methode CProgram::GetFrameDataToTable() wird nach Abschluss der EA-Parameteroptimierung in OnTesterDeinit() aufgerufen. Danach steht dem Anwender die grafische Oberfläche zur Verfügung. Die Registerkarte Ergebnisse enthält Optimierungsergebnisse, die nach den angegebenen Kriterien ausgewählt wurden. In unserem Beispiel wurden die Ergebnisse anhand des Wertes in der zweiten Spalte (Profit) ausgewählt.
Abb. 3. Die Tabelle der Optimierungsergebnisse in der grafischen Oberfläche.
Der Benutzer kann die Multi-Symbol-Bilanzwerte der Ergebnisse aus dieser Tabelle einsehen. Wenn Sie eine beliebige Tabellenzeile markieren, wird das benutzerdefinierte Ereignis ON_CLICK_LIST_ITEM mit dem Tabellenbezeichner erzeugt. Dies ermöglicht die Bestimmung der Tabelle, von der die Nachricht empfangen wurde (sofern es mehrere Tabellen gibt). Die erste Spalte speichert die Durchlaufnummer, so dass wir die Ergebnisdaten erhalten können, indem wir diese Nummer an den CFrameGenerator::GetFrameData() Methode übergeben.
//+------------------------------------------------------------------+ //| Ereignisbehandlung | //+------------------------------------------------------------------+ void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Ereignis eines Klicks auf eine Tabellenzeile if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM) { if(lparam==m_table_param.Id()) { //--- Abfrage der Durchlaufnummer aus der Tabelle ulong pass=(ulong)m_table_param.GetValue(0,m_table_param.SelectedItem()); //--- Datenabfrage auf Basis der Durchlaufnummer m_frame_gen.GetFrameData(pass); } //--- return; } ... }
Jedes Mal, wenn der Benutzer eine Zeile in der Tabelle auswählt, wird die Grafik der Multi-Symbol-Salden in der Registerkarte Saldo aktualisiert:
Abb. 4. Demonstration der erhaltenen Ergebnisse.
Wir haben ein nützliches Werkzeug, das eine schnelle Ansicht der Ergebnisse von Multi-Symbol-Tests ermöglicht.
Schlussfolgerungen
Ich habe eine weitere Möglichkeit aufgezeigt, wie Sie mit Optimierungsergebnissen arbeiten können. Dieses Thema ist noch nicht vollständig erforscht und sollte weiterentwickelt werden. Die GUI-Erstellungsbibliothek ermöglicht die Erstellung einer Vielzahl von interessanten und komfortablen Lösungen. Sie sind herzlich eingeladen, Ihre Ideen in Kommentaren zu diesem Artikel einzubringen. Möglicherweise beschreibt einer der folgenden Artikel das Optimierungswerkzeug, das Sie benötigen.
Nachfolgend können Sie die Dateien zum Testen und detaillierten Studium des im Artikel enthaltenen Codes herunterladen.
Dateiname | Kommentar |
---|---|
MacdSampleMSFrames.mq5 | Veränderter EA aus dem Standardpaket - MACD Sample |
Program.mqh | Datei mit den Klassen des Programms |
CreateGUI.mqh | Datei mit dem Implementierungsmethoden der Programmklassen aus der Datei Program.mqh file |
Strategy.mqh | Datei mit dem veränderten Klasse von MACD Sample (Multi-Symbol-Version) |
FormatString.mqh | Datei mit Hilfsfunktionen zur Formatierung von Zeichenketten |
FrameGenerator.mqh | Datei mit Klassen für die Arbeit mit den Ergebnissen der Optimierung |
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/4562
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.