Die Behandlung der Ergebnisse der Optimierung mit einem grafischen Interface

13 Juli 2018, 16:17
Anatoli Kazharski
0
178

Inhaltsverzeichnis

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

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.

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.

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 Software Corp.
Originalartikel: https://www.mql5.com/ru/articles/4562

Beigefügte Dateien |
Experts.zip (23.7 KB)
Random Decision Forest und Reinforcement-Learning Random Decision Forest und Reinforcement-Learning

Random Forest (RF) mit dem Einsatz von Bagging ist eine der leistungsfähigsten maschinellen Lernmethoden, die dem Gradienten-Boosting etwas unterlegen ist. Dieser Artikel versucht, ein selbstlernendes Handelssystem zu entwickeln, das Entscheidungen basierend auf den Erfahrungen aus der Interaktion mit dem Markt trifft.

Erstellen multimodularer Expert Advisors Erstellen multimodularer Expert Advisors

Die Programmiersprache MQL erlaubt es, das Konzept der modularen Programmierung von Handelsstrategien umzusetzen. Der Artikel schildert ein Beispiel für die Erstellung eines multimodularen Expert Advisors, der aus separat kompilierten Dateimodulen besteht.

Wie man den Berechnungsblock eines Indikators in den Code eines Expert Advisors überträgt Wie man den Berechnungsblock eines Indikators in den Code eines Expert Advisors überträgt

Für die Übertragung des Codes eines Indikators in einen Expert Advisor kann es unterschiedliche Gründe geben. Aber wie kann man Vor- und Nachteile eines solchen Ansatzes bewerten? In diesem Artikel wird eine Technologie für die Übertragung des Codes eines Indikators in einen Expert Advisor vorgestellt. Es wurden mehrere Experimente hinsichtlich der Arbeitsgeschwindigkeit des Expert Advisors durchgeführt.

Die Entwicklung eines oszillierenden ZigZag-Indikator Beispiel für die Durchführung der Anforderungsspezifikationen Die Entwicklung eines oszillierenden ZigZag-Indikator Beispiel für die Durchführung der Anforderungsspezifikationen

Der Artikel demonstriert die Entwicklung des ZigZag-Indikators gemäß der im Artikel "Wie man eine Anforderungsspezifikation bei der Bestellung eines Indikators erstellt" beschriebenen Beispiele. Der Indikator wird durch Extrema gebildet, die mit Hilfe eines Oszillators definiert werden. Es besteht die Möglichkeit, einen von fünf Oszillatoren zu verwenden: WPR, CCI, Chaikin, RSI oder die Stochastik.