Die Visualisierung von Optimierungsergebnissen nach dem ausgewählten Kriterium

Anatoli Kazharski | 22 August, 2018

Inhalt

Einleitung

Wir entwickeln die Anwendung für die Arbeit mit Optimierungsergebnissen weiter, an der wir in den vorherigen Artikeln gearbeitet haben. Schauen wir uns ein Beispiel an, in welchem die Tabelle der besten Ergebnisse bereits nach der Optimierung von Parametern erstellt werden kann, indem man ein anderes Kriterium über das grafische Interface angibt. Im vorherigen Artikel habe ich gezeigt, wie man mit Datenkategorien arbeitet: sie in ein Array von Rahmen speichert und dann extrahiert. So haben wir mit statistischen Kennzahlen, Salden-Arrays von Symbolen und Drawdowns gearbeitet.

Nach der Optimierung können wir diese Daten auf separaten Charts sehen, indem wir die entsprechenden Zeilen in der Tabelle auswählen. Aber Perfektion kennt keine Grenzen. Um die nach einem oder dem anderen Kriterium ausgewählten Ergebnisse zu sehen, wäre es praktisch, wenn wir alle Salden auf einem separaten Chart sehen könnten. Das Markieren von Zeilen in der Tabelle wird eine markierte Saldenkurve auf diesem Gesamtchart bilden. So können wir das Optimierungsergebnis besser einschätzen. 

Auf Wunsch einiger Mitglieder der Community zeige ich, wie man das Auswählen von Zeilen in der Tabelle mit der Tastatur steuern kann. Dafür müssen wir die Klasse CTable in unserer Bibliothek erweitern.

Entwicklung des grafischen Interfaces

In der vorherigen Version der Anwendung gab es drei Registerkarten im grafischen Interface: Frames, Results und Balance.

Auf dem Reiter Frames befinden sich Elemente für die Arbeit und Ansicht aller Ergebnisse während und nach der Optimierung.

Den zweiten (Results) und den dritten Reiter (Balance) vereinen wir. Jetzt wenn wir eine Zeile in der Tabelle auswählen, können wir das Ergebnis direkt im Chart sehen, so brauchen wir den Reiter nicht zu wechseln.

Auf der Registerkarte Results platzieren wir eine weitere Gruppe von Tabs: Balances und Favorites. Im Tab Balances befinden sich die Charts für die Ansicht von Multi-Symbol-Salden und Drawdowns sowie die Liste der Symbole, die getestet wurden. Im Reiter Favorites platzieren wirden Chart mit allen besten Ergebnissen aus der Tabelle. Des Weiteren fügen wir ein Element vom Typ CComboBox (Dropdown-Liste) hinzu. Es hilft uns, ein Kriterium für die Auswahl der besten Ergebnissen aus der Liste der Rahmen auszusuchen.

Die komplette Hierarchie der Elemente des grafischen Interfaces sieht jetzt wie folgt aus:

Der Code der Methoden zur Erstellung dieser 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;
   //--- Statusleiste
   CStatusBar        m_status_bar;
   //--- Tabs
   CTabs             m_tabs1;
   CTabs             m_tabs2;
   //--- Eingabefelder
   CTextEdit         m_curves_total;
   CTextEdit         m_sleep_ms;
   //--- Buttons
   CButton           m_reply_frames;
   //--- Comboboxen
   CComboBox         m_criterion;
   //--- Charts
   CGraph            m_graph1;
   CGraph            m_graph2;
   CGraph            m_graph3;
   CGraph            m_graph4;
   CGraph            m_graph5;
   //--- Tabellen
   CTable            m_table_main;
   CTable            m_table_symbols;
   //--- Fortschrittsanzeige
   CProgressBar      m_progress_bar;
   //---
public:
   //--- Erstellen des grafischen Interfaces
   bool              CreateGUI(void);
   //---
private:
   //--- Form
   bool              CreateWindow(const string text);
   //--- Statusleiste
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- Tabs
   bool              CreateTabs1(const int x_gap,const int y_gap);
   bool              CreateTabs2(const int x_gap,const int y_gap);
   //--- Eingabefelder
   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);
   //--- Buttons
   bool              CreateReplyFrames(const int x_gap,const int y_gap,const string text);
   //--- Comboboxen
   bool              CreateCriterion(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);
   bool              CreateGraph5(const int x_gap,const int y_gap);
   //--- Buttons
   bool              CreateUpdateGraph(const int x_gap,const int y_gap,const string text);
   //--- Tabellen
   bool              CreateMainTable(const int x_gap,const int y_gap);
   bool              CreateSymbolsTable(const int x_gap,const int y_gap);
   //--- Fortschrittsanzeige
   bool              CreateProgressBar(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| Methoden zum Erstellen der Kontrollelemente                      |
//+------------------------------------------------------------------+
#include "CreateGUI.mqh"
//+------------------------------------------------------------------+

Auswahl der Optimierungsergebnisse

Um alle besten Optimierungsergebnisse auf einer Grafik anzuzeigen, wird eine Methode benötigt, die true solange zurückgibt, bis die in den Parametern gesetzte Anzahl der Ergebnisse gefunden wird. Sobald sie gefunden werden, gibt die Methode false zurück. Das ist die Methode CFrameGenerator::UpdateBestResultsGraph(), die unten angeführt wird. Standardmäßig werden die 100 besten Optimierungsergebnisse gezeichnet.

In der Methode wird eine doppelte Schleife verwendet. Die erste Schleife ist auf die Anzahl der angezeigten besten Ergebnisse und auf die Anzahl der Zeilen in der Tabelle begrenzt, um die Überschreitung der Grenzen des Bereichs in der Struktur von Arrays der Tabelle auszuschließen. Bei jeder Iteration dieser Schleife bewegen wir den Rahmenzeiger auf den Listenanfang.

Wenn wir über die Rahmen in der zweiten Schleife iterieren, suchen wir nach der Durchlaufnummer, die wir früher in der Struktur der Arrays gespeichert haben. Die Struktur der Arrays vor dem Aufruf der Methode CFrameGenerator::UpdateBestResultsGraph() muss nach dem angegebenen Kriterium sortiert werden. Nachdem die Durchlaufnummer gefunden wurde, erhalten wir die Parameter des Expert Advisors in diesem Durchlauf und deren Anzahl. Danach erhalten wir den Ergebnissaldo des aktuellen Durchlaufs aus seinem Datenarray (m_data[]). Man darf nicht vergessen, dass die Daten des Gesamtsaldos im Array des Rahmens nach den statistischen Kennzahlen enthalten sind, und die Arraygröße gleich dem Wert im double-Parameter des Rahmens ist. Dieses Array, genauso wie die Datenreihe, wird auf die Grafik der Salden der besten Ergebnisse gesetzt. Wenn das Endergebnis dieses Tests größer als die Ersteinzahlung ist, wird die Linie in grün dargestellt , im Gegensatz — rot. Die Größe der Reihe wird in einem separaten Array gespeichert, damit man nach dem Ende der Schleife die Reihe mit der kleinsten Anzahl der Elemente für das Setzen der Grenzen der X-Achse bestimmen kann. Und schließlich muss der Rahmenzähler um 1 erhöht werden, damit man beim nächsten Mal die Schleife fortsetzen könnte, ohne diesen Durchlauf zu berücksichtigen.

Wenn die Schleife komplett durchlaufen wurde:

Danach gibt die Methode CFrameGenerator::UpdateBestResultsGraph() false zurück. Das bedeutet, dass die Auswahl der Ergebnisse abgeschlossen ist.

//+------------------------------------------------------------------+
//| Klasse für die Arbeit mit den Optimierungsergebnissen            |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Anzahl der besten Ergebnisse
   int               m_best_results_total;
   //---
public:
   //--- Aktualisieren der Grafik der besten Ergebnisse
   bool              UpdateBestResultsGraph(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(void) : m_best_results_total(100)
  {
  }
//+------------------------------------------------------------------+
//| Aktualisieren der Grafik der besten Ergebnisse                   |
//+------------------------------------------------------------------+
bool CFrameGenerator::UpdateBestResultsGraph(void)
  {
   for(int i=(int)m_frames_counter; i<m_best_results_total && i<m_rows_total; i++)
     {
      //--- Bewegen des Rahmenzeigers auf den Listenanfang
      ::FrameFirst();
      //--- Extrahieren von Daten
      while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
        {
         //--- Die Nummern der Durchläufe stimmen nicht überein, zum nächsten
         if(m_pass!=(ulong)m_columns[0].m_rows[i])
            continue;
         //--- Abfrage der Parameter und deren Anzahl
         GetParametersTotal();
         //--- Abfrage des Saldos des aktuellen Ergebnisses
         double serie[];
         ::ArrayCopy(serie,m_data,0,STAT_TOTAL,(int)m_value);
         //--- Ausgabe des Arrays auf die Grafik des Saldos
         CCurve *curve=m_graph_best.CurveGetByIndex(i);
         curve.Name((string)m_pass);
         curve.Color((m_data[m_profit_index]>=0)? ::ColorToARGB(clrLimeGreen) : ::ColorToARGB(clrRed));
         curve.Update(serie);
         //--- Abfrage der Größe der Reihe
         m_curve_max[i]=::ArraySize(serie);
         //--- Erhöhen des Rahmenzählers
         m_frames_counter++;
         return(true);
        }
     }
//--- Setzen des Rahmenzählers auf Null
   m_frames_counter=0;
//--- Feststellen der Reihe mit der größten Anzahl der Elemente
   double x_max=m_curve_max[::ArrayMaximum(m_curve_max)];
//--- Eigenschaften der horizontalen Achse
   CAxis *x_axis=m_graph_best.XAxis();
   x_axis.Min(0);
   x_axis.Max(x_max);
   x_axis.DefaultStep((int)(x_max/8.0));
//--- Aktualisieren der Grafik
   m_graph_best.CalculateMaxMinValues();
   m_graph_best.CurvePlotAll();
   m_graph_best.Update();
   return(false);
  }

Um die Ergebnisse zu finden, müssen wir über alle Rahmen in der Gesamtliste iterieren. Das ist zeitaufwendig. Um den Stand der Suche zu verfolgen, verwenden wir das Element "Fortschrittsanzeige" (CProgressBar). Dafür wurde die Methode CProgram::GetBestOptimizationResults() in der Klasse der Anwendung (CProgram) implementiert. Hier, in der while-Schleife wird als Bedingung die Methode CFrameGenerator::UpdateBestResultsGraph() aufgerufen. Vor dem Beginn der Schleife machen wir den Fortschrittsbalken (Progress Bar) sichtbar. Da die Methode CFrameGenerator::UpdateBestResultsGraph() den Rahmenzähler verwendet, kann man seinen aktuellen Wert abfragen. Nach dem Ende der Schleife muss die Progress Bar ausgeblendet werden.

class CProgram : public CWndEvents
  {
private:
   //--- Abfrage der besten Optimierungsergebnisse
   void              GetBestOptimizationResults(void);
  };
//+------------------------------------------------------------------+
//| Abfrage der besten Optimierungsergebnisse                        |
//+------------------------------------------------------------------+
void CProgram::GetBestOptimizationResults(void)
  {
//--- Anzeige des Fortschrittsbalkens
   m_progress_bar.Show(); 
//--- Visualisieren des Fortschritts bei der Abfrage der besten Ergebnisse
   int best_results_total=m_frame_gen.BestResultsTotal();
   while(m_frame_gen.UpdateBestResultsGraph() && !::IsStopped())
     {
      //--- Aktualisieren des Fortschrittsbalkens
      m_progress_bar.LabelText("Selection of results: "+string(m_frame_gen.CurrentFrame())+"/"+string(best_results_total));
      m_progress_bar.Update((int)m_frame_gen.CurrentFrame(),best_results_total);
     }
//--- Ausblenden des Fortschrittsbalkens
   m_progress_bar.Hide();
  }

Die Methode CProgram::GetBestOptimizationResults() muss in der Methode des Abschlusses der Optimierung aufgerufen werden. So kann der Benutzer sehen, dass das Programm ausgeführt wird und nicht eingefroren ist. Andere Methoden wurden in den vorherigen Artikeln betrachtet, deswegen gehen wir darauf nicht ein.

//+------------------------------------------------------------------+
//| Ereignis des Abschlusses der Optimierung                         |
//+------------------------------------------------------------------+
void CProgram::OnTesterDeinitEvent(void)
  {
//--- Abschluss der Optimierung
   m_frame_gen.OnTesterDeinitEvent();
//--- Visualisieren des Fortschritts bei der Abfrage der besten Ergebnisse
   GetBestOptimizationResults();
//--- Entsperren des Interfaces
   IsLockedGUI(true);
//--- Berechnen des Verhältnisses zwischen den positiven und negativen Ergebnissen
   CalculateProfitsAndLosses();
//--- Erhalten der Daten in die Tabelle mit den Optimierungsergebnissen
   GetFrameDataToTable();
//--- Initialisierung des Kernes GUI
   CWndEvents::InitializeCore();
  }

Direkt nach dem Abschluss der Optimierung oder nachdem ein Stopp erzwungen wurde, erscheint neben der Statusleiste der Fortschrittsbalken. Er zeigt dem Benutzer, dass die Ergebnisse ausgewählt werden:

 Abb. 1 – Visualisierung des Prozesses der Auswahl der Ergebnisse.

Abb. 1.  Visualisierung des Prozesses der Auswahl der Ergebnisse.

Um sich die Visualisierung der Salden aller ausgewählten Ergebnisse anzuschauen, muss man die Registerkarte Results, und dann Favorites öffnen. Standardmäßig werden der Tabelle die 100 besten Ergebnisse nach dem Kriterium Profit hinzugefügt. Jederzeit kann man ein anderes Kriterium für die Auswahl der hundert besten Ergebnisse aus der Dropdown-Liste Criterion auswählen. Darauf gehen wir noch ein, und jetzt schauen wir uns die Methoden zum Gestalten dieses Prozesses an.

 Abb. 2 – Grafik der besten Optimierungsergebnisse.

Abb. 2. Grafik der besten Optimierungsergebnisse.

Markieren einer Zeile in der Tabelle durch das Programm

Bis jetzt war es möglich, eine Zeile mit dem linken Mausklick auszuwählen. Aber manchmal muss das durch das Programm getan werden — zum Beispiel, mit den Tasten Up, Down, Home und End. Für das Auswählen einer Zeile wurde der Klasse CTable die öffentliche Methode CTable::SelectRow() hinzugefügt. Ihr Code ist ähnlich der privaten Methode CTable::RedrawRow(). Die Methode wird zum Neuzeichnen von Zeilen nach den Ereignissen des Mausklicks und zum Bewegen des Mauszeigers über der Tabelle verwendet, wenn der Modus der Auswahl einer Zeile, sobald sich der Mauszeiger darüber befindet, aktiviert ist.

Der Großteil des Codes kann in den beiden Methoden verwendet werden. Deswegen habe ich ihn in eine separate Methode, CTable::DrawRow(), verschoben. Folgendes muss der Methode übergeben werden:

In der Methode werden die Koordinaten für das Neuzeichnen der Zeilen definiert und alle ihre Elemente werden eingezeichnet: Hintergrund, Gitter, Abbildungen und Text.
//+------------------------------------------------------------------+
//| Zeichnen der angegebenen Reihe der Tabelle im angegebenen Modus  |
//+------------------------------------------------------------------+
void CTable::DrawRow(int &indexes[],const int item_index,const int prev_item_index,const bool is_user=true)
  {
   int x1=0,x2=m_table_x_size-2;
   int y1[2]={0},y2[2]={0};
//--- Anzahl der Zeilen und Spalten, die gezeichnet werden müssen
   uint rows_total    =0;
   uint columns_total =m_columns_total-1;
//--- Wenn es die Programmmethode des Markierens der Zeile ist
   if(!is_user)
      rows_total=(prev_item_index!=WRONG_VALUE && item_index!=prev_item_index)? 2 : 1;
   else
      rows_total=(item_index!=WRONG_VALUE && prev_item_index!=WRONG_VALUE && item_index!=prev_item_index)? 2 : 1;
//--- Zeichnen des Hintergrunds für die Zeilen
   for(uint r=0; r<rows_total; r++)
     {
      //--- Berechnen der Koordinaten der oberen und unteren Grenzen der Zeile
      y1[r] =m_rows[indexes[r]].m_y+1;
      y2[r] =m_rows[indexes[r]].m_y2-1;
      //--- Fokus auf der Zeile entsprechend dem Modus des Hervorhebens
      bool is_item_focus=false;
      if(!m_lights_hover)
         is_item_focus=(indexes[r]==item_index && item_index!=WRONG_VALUE);
      else
         is_item_focus=(item_index==WRONG_VALUE)?(indexes[r]==prev_item_index) :(indexes[r]==item_index);
      //--- Hintergrund der Zeile zeichnen
      m_table.FillRectangle(x1,y1[r],x2,y2[r],RowColorCurrent(indexes[r],(is_user)? is_item_focus : false));
     }
//--- Zeichnen der Grenzen
   for(uint r=0; r<rows_total; r++)
     {
      for(uint c=0; c<columns_total; c++)
         m_table.Line(m_columns[c].m_x2,y1[r],m_columns[c].m_x2,y2[r],::ColorToARGB(m_grid_color));
     } 
//--- Zeichnen der Bilder
   for(uint r=0; r<rows_total; r++)
     {
      for(uint c=0; c<m_columns_total; c++)
        {
         //--- Das Bild zeichnen, wenn (1) es in dieser Zelle gibt und (2) wenn der Text in dieser Spalte links ausgerichtet
         if(ImagesTotal(c,indexes[r])>0 && m_columns[c].m_text_align==ALIGN_LEFT)
            CTable::DrawImage(c,indexes[r]);
        }
     }
//--- Für die Berechnung der Koordinaten
   int x=0,y=0;
//--- Ausrichtung des Textes
   uint text_align=0;
//--- Text zeichnen
   for(uint c=0; c<m_columns_total; c++)
     {
      //--- Abfrage der (1) X-Koordinate des Textes und (2) die Ausrichtung des Textes
      x          =TextX(c);
      text_align =TextAlign(c,TA_TOP);
      //---
      for(uint r=0; r<rows_total; r++)
        {
         //--- (1) Berechnen der Koordinate und (2) Zeichnen des Textes
         y=m_rows[indexes[r]].m_y+m_label_y_gap;
         m_table.TextOut(x,y,m_columns[c].m_rows[indexes[r]].m_short_text,TextColor(c,indexes[r]),text_align);
        }
     }
  }

Der Methode CTable::SelectRow() muss nur ein Argument übergeben werden — der Index der Zeile, die ausgewählt werden muss. Zuerst prüfen wir, ob der Index den Bereich der Tabelle überschreitet und ob die Zeile mit diesem Index bereits ausgewählt ist. Danach bestimmen wir den aktuellen und den vorherigen Index der ausgewählten Zeile und die Reihenfolge des Neuzeichnens. Der Methode CTable::DrawRow() übergeben wir die erhaltenen Werte. Wenn wir die Indizes an den Grenzen des sichtbaren Bereichs der Tabelle erhalten, können wir feststellen, an welche Position der Schieberegler der Scrollbar bewegt werden muss.

//+------------------------------------------------------------------+
//| Auswahl der angegebenen Zeile in der Tabelle                     |
//+------------------------------------------------------------------+
void CTable::SelectRow(const int row_index)
  {
//--- Prüfen der Arraygrenze
   if(!CheckOutOfRange(0,(uint)row_index))
      return;
//--- Wenn diese Zeile bereits ausgewählt ist
   if(m_selected_item==row_index)
      return;
//--- Der aktuelle und der vorherige Indizes der Zeilen
   m_prev_selected_item =(m_selected_item==WRONG_VALUE)? row_index : m_selected_item;
   m_selected_item      =row_index;
//--- Array für die Werte in einer bestimmten Reihenfolge
   int indexes[2];
//--- Wenn es hier zum ersten Mal ist
   if(m_prev_selected_item==WRONG_VALUE)
      indexes[0]=m_selected_item;
   else
     {
      indexes[0] =(m_selected_item>m_prev_selected_item)? m_prev_selected_item : m_selected_item;
      indexes[1] =(m_selected_item>m_prev_selected_item)? m_selected_item : m_prev_selected_item;
     }
//--- Zeichnen der angegebenen Reihe der Tabelle im angegebenen Modus
   DrawRow(indexes,m_selected_item,m_prev_selected_item,false);
//--- Abfrage der Indizes an den Grenzen des sichtbaren Bereichs
   VisibleTableIndexes();
//--- Bewegen der Scrollbar an die angegebene Zeile
   if(row_index==0)
     {
      VerticalScrolling(0);
     }
   else if((uint)row_index>=m_rows_total-1)
     {
      VerticalScrolling(WRONG_VALUE);
     }
   else if(row_index<(int)m_visible_table_from_index)
     {
      VerticalScrolling(m_scrollv.CurrentPos()-1);
     }
   else if(row_index>=(int)m_visible_table_to_index-1)
     {
      VerticalScrolling(m_scrollv.CurrentPos()+1);
     }
  }

Die aktualisierte Version der Klasse CTable kann am Ende des Artikels heruntergeladen werden. Die aktuellste Version der Bibliothek EasyAndFast ist in Code Base verfügbar.

Hilfsmethoden für die Arbeit mit den Daten von Rahmen

In der Version der Anwendung, die im vorherigen Artikel vorgestellt wurde, wurden beim Auswählen einer Zeile in der Tabelle der Ergebnisse Multi-Symbol-Salden und Drawdowns in Charts angezeigt. Um zu verstehen, zu welchem Symbol eine Kurve auf dem Multi-Symbol-Chart gehört, wurden die Namen der Kurven separat angezeigt, auf dem Chart rechts. In der aktuellen Version wird die Größe des Charts auf der Y-Achse fixiert. Wenn es zu viele Symbole in den Tests gibt, passen sie alle in den ausgewählten Bereich nicht rein. Deshalb platzieren wir eine Liste vom Typ CTable mit der Scrollbar von den Grafiken rechts. Die Liste beinhaltet alle Namen der Salden.

Für die Abfrage der Symbole wird die Methode CProgram::GetFrameSymbolsToTable() verwendet. Nachdem die Daten eines Rahmen erhalten wurden, ergibt sich die Möglichkeit, die Symbole des Ergebnisses vom string-Parameter abzufragen. Übergeben wir das String-Array und erhalten die Liste der Symbole. Wenn es mehr als ein Symbol gibt, muss man die Anzahl der Salden um ein Element erhöhen und das erste für den Gesamtsaldo reservieren.

Danach legen wir die Größe der Tabelle fest. Hier wird nur eine Spalte benötigt, die Anzahl der Zeilen ist gleich der Anzahl der Kurven auf der Grafik. Setzen wir die Spaltenbreite und eine Überschrift. In der Schleife füllen wir die Tabelle mit den Namen der Salden aus. Um zu verstehen, welche Kurve zu welchem Namen gehört, verknüpfen wir sie mit Farbkomponente. Um die vorgenommenen Änderungen anzuzeigen, müssen die Elemente aktualisiert werden.

//+------------------------------------------------------------------+
//| Abfrage der Symbole des Rahmens in die Tabelle                   |
//+------------------------------------------------------------------+
void CProgram::GetFrameSymbolsToTable(void)
  {
//--- Abfrage der Liste der Symbole und der Anzahl der Kurven
   string symbols[];
   int symbols_total  =m_frame_gen.CopySymbols(symbols);
   int balances_total =(symbols_total>1)? symbols_total+1 : symbols_total;
//--- Setzen der Größe der Tabelle
   m_table_symbols.Rebuilding(1,balances_total,true);
//--- Spaltenbreite der Liste
   int width[]={111};
   m_table_symbols.ColumnsWidth(width);
//--- Festlegen der Überschrift
   m_table_symbols.SetHeaderText(0,"Balances");
//--- Ausfüllen der Tabelle mit den Daten aus den Rahmen
   for(uint r=0; r<m_table_symbols.RowsTotal(); r++)
     {
      uint clr=m_graph3.GetGraphicPointer().CurveGetByIndex(r).Color();
      m_table_symbols.TextColor(0,r,::ColorToARGB(clr));
      m_table_symbols.SetValue(0,r,(symbols_total>1)?(r<1)? "BALANCE" : symbols[r-1]: symbols[r],0);
     }
//--- Tabelle aktualisieren
   m_table_symbols.Update(true);
   m_table_symbols.GetScrollHPointer().Update(true);
   m_table_symbols.GetScrollVPointer().Update(true);
  }

Es wäre praktisch, wenn beim Auswählen eines Ergebnisses in der Tabelle auch seine Kurve auf der Grafik hervorgehoben würde. Dafür schreiben wir die Methode CProgram::SelectCurve(). Der Methode übergeben wir die Durchlaufnummer für die Suche nach der benötigten Kurve im Chart. Die Namen der Kurven entsprechen den Durchlaufnummern, zu welchen sie gehören. Deswegen kann man sie finden, indem man einfach die übergebene Nummer eines Durchlaufs mit der Nummer im Namen der Kurve in einer Schleife vergleicht. Sobald die benötigte Kurve gefunden wurde, speichern wir ihren Index und beenden die Schleife.

Nun muss die gefundene Kurve in die obere Schicht übertragen werden. Wenn wir die Kurve hervorheben, nur indem wir ihre Farbe ändern, kann sie sich unter allen anderen verlieren. Deshalb müssen wir die gefundene Kurve und die letzte eingezeichnete Kurve tauschen.

Dafür fragen wir die Zeiger dieser zwei Kurven nach Indizes ab. Danach kopieren wir ihre Namen und Datenarrays. Anschließend tauschen wir sie. Für die letzte Kurve setzen wir die Linienbreite und Farbe. Stellen wir sie in schwarz dar, um sie hervorzuheben. Damit die Änderungen in Kraft treten, muss der Chart aktualisiert werden.

//+------------------------------------------------------------------+
//| Ereignis des Abschlusses der Optimierung                         |
//+------------------------------------------------------------------+
void CProgram::SelectCurve(const ulong pass)
  {
   CGraphic *graph=m_graph5.GetGraphicPointer();
//--- Suche der Kurve nach der Durchlaufnummer
   ulong curve_index =0;
   int curves_total  =graph.CurvesTotal();
   for(int i=0; i<curves_total; i++)
     {
      if(pass==(ulong)graph.CurveGetByIndex(i).Name())
        {
         curve_index=i;
         break;
        }
     }
//--- Die ausgewählte und die letzte Kurve auf dem Chart 
   CCurve *selected_curve =graph.CurveGetByIndex((int)curve_index);
   CCurve *last_curve     =graph.CurveGetByIndex((int)curves_total-1);
//--- Kopieren des ausgewählten und des letzten Datenarrays
   double y1[],y2[];
   string name1=selected_curve.Name();
   string name2=last_curve.Name();
   selected_curve.GetY(y1);
   last_curve.GetY(y2);
//---
   last_curve.Name(name1);
   selected_curve.Name(name2);
   last_curve.Update(y1);
   selected_curve.Update(y2);
//---
   last_curve.LinesWidth(2);
   last_curve.Color(clrBlack);
//--- Aktualisieren der Grafik
   graph.CurvePlotAll();
   graph.Update();
  }

Jetzt wenn wir eine Zeile in der Tabelle auswählen, sehen wir die entsprechende Saldenkurve auf dem Chart.

 Abb. 3 – Markieren der Kurven im Chart.

Abb. 3. Markieren der Kurven im Chart.


Die Verarbeitung von Ereignissen bei der Arbeit mit dem grafischen Interface

Es bleibt nur noch die Methoden zur Verarbeitung von Ereignissen zu betrachten, die bei der Arbeit mit dem grafischen Interface unserer Anwendung erzeugt werden. Hier sind diese Methoden:

Wenn wir eine Zeile auswählen, indem wir mit der Maus auf der Zeile klicken, wird das benutzerdefinierte Ereignis ON_CLICK_LIST_ITEM erzeugt. Für die Verarbeitung des Ereignisses wird die Methode CProgram::TableRowSelection() aufgerufen. Der Methode wird der long-Parameter des Ereignisses übergeben. Dieser Parameter stellt den Identifikator des Elements dar, aus dem dieses Ereignis erzeugt wurde. Wenn der Identifikator nicht zum Element gehört, verlässt das Programm die Methode und überprüft das nächste Element im Event Handler der Elemente der Anwendung. Wenn der Identifikator mit dem Identifikator der Tabelle der Ergebnisse übereinstimmt, erhalten wir die Durchlaufnummer aus der ersten Spalte der Tabelle. Deswegen um die Durchlaufnummer zu erhalten, muss man einfach die Indizes der Spalte und der ausgewählten Reihe angeben, indem man der Methode CTable::GetValue() diese Werte übergibt.

Wir haben die Durchlaufnummer erhalten. Nun können wir die Daten und dann die Symbole aus diesem Rahmen abfragen, die in diesem Ergebnis enthalten sind. Fügen wir sie der Tabelle auf der ersten Registerkarte der zweiten Gruppe hinzu. Am Ende markieren wir die Saldenkurve auf der Grafik aller Ergebnisse.

//+------------------------------------------------------------------+
//| Auswählen der Zeile der Tabelle mit der linken Maustaste         |
//+------------------------------------------------------------------+
bool CProgram::TableRowSelection(const long element_id)
  {
//--- Auswählen der Zeile der Tabelle
   if(element_id!=m_table_main.Id())
      return(false);
//--- Abfrage der Durchlaufnummer aus der Tabelle
   ulong pass=(ulong)m_table_main.GetValue(0,m_table_main.SelectedItem());
//--- Abfrage der Daten nach der Durchlaufnummer
   m_frame_gen.GetFrameData(pass);
//--- Hinzufügen der Symbole zur Tabelle
   GetFrameSymbolsToTable();
//--- Hervorheben der Kurve im Chart nach der Durchlaufnummer
   SelectCurve(pass);
   return(true);
  }

Wenn das benutzerdefinierte Ereignis ON_CLICK_LIST_ITEM eintrifft, wird die Auswahl des Kriteriums für die Auswahl der Ergebnisse in der Dropdown-Liste der Combobox (CComboBox) verarbeitet. Dafür ist die Methode CProgram::ShowResultsBySelectedCriteria() zuständig. Nach einer erfolgreichen Prüfung des Bezeichners des Elements erhalten wir den Index des ausgewählten Punktes in der Dropdown-Liste. In dieser Version werden drei Kriterien angeboten:

Danach bestimmen wir den Index der Spalte mit den Daten, die zum ausgewählten Kriterium gehören. Der erste Punkt gehört zur Spalte mit dem Index 1, der zweite — zur Spalte mit dem Index 2, der dritte — zur Spalte mit dem Index 5. Danach erhalten wir die Rahmen mit den besten Ergebnissen nach dem ausgewählten Kriterium. Dafür muss die Methode CFrameGenerator::OnChangedSelectionCriteria() aufgerufen werden, indem wir ihr den Index der Spalte übergeben. Nun ist alles bereit, um die Salden der besten Ergebnisse im Chart zu erhalten. Dieser Prozess wird durch die Fortschrittsanzeige visualisiert. Der letzte Aufruf — das Erhalten aller Daten in die Tabelle der besten Ergebnisse.

//+------------------------------------------------------------------+
//| Zeigt die Ergebnisse nach dem angegebenen Kriterium an           |
//+------------------------------------------------------------------+
bool CProgram::ShowResultsBySelectedCriteria(const long element_id)
  {
//--- Prüfen des Bezeichners des Elements
   if(element_id!=m_criterion.Id())
      return(false);
//--- Feststellen des Indexes des Kriteriums für das Erhalten der besten Ergebnisse
   int index=m_criterion.GetListViewPointer().SelectedItemIndex();
   int column_index=(index<1)? 1 : (index==1)? 2 : 5;
   m_frame_gen.OnChangedSelectionCriteria(column_index);
//--- Visualisieren des Fortschritts bei der Abfrage der besten Ergebnisse
   GetBestOptimizationResults();
//--- Erhalten der Daten in die Tabelle mit den Optimierungsergebnissen
   GetFrameDataToTable();
   return(true);
  }

Im Event Handler werden die oben betrachteten Methoden nach dem Eintreffen des Ereignisses ON_CLICK_LIST_ITEM nacheinander aufgerufen, bis eine von ihnen true zurückgibt.

//+------------------------------------------------------------------+
//| Event Handler                                                    |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Ereignisse des Klicks auf die Reihen der Tabelle
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      //--- Auswählen der Zeile der Tabelle
      if(TableRowSelection(lparam))
         return;
      //--- Kriterium für die Auswahl der Ergebnisse
      if(ShowResultsBySelectedCriteria(lparam))
         return;
      //---
      return;
     }
...
  }

So sieht der Prozess der Auswahl aus der Dropdown-Liste, das Erhalten der Daten und das Zeichnen auf dem Chart aus:

 Abb. 4. Auswahl der Ergebnisse nach dem angegebenen Kriterium.

Abb. 4. Auswahl der Ergebnisse nach dem angegebenen Kriterium.

Um eine Zeile mit der Tastatur auszuwählen, benötigen wir die Methode CProgram::SelectingResultsUsingKeys(). Der Methode muss der Code der gedrückten Taste übergeben werden. Er wird im long-Parameter des Ereignisses CHARTEVENT_KEYDOWN erhalten. Am Anfang der Methode erhalten wir den Index der aktuell ausgewählten Zeile in der Tabelle. Weiter stellen wir im switch-Operator fest, welche Taste gedrückt wurde. Hier ist ein Beispiel für die Verarbeitung des Drückens von vier Tasten:

Weiter müssen Prüfungen durchgeführt werden. Das Programm verlässt die Methode wenn:

Nach bestandenen Prüfungen wird die angegebene Zeile in der Tabelle markiert, die vertikale Scrollbar wird bewegt, wenn nötig.

Nachdem die Zeile ausgewählt ist, passiert Folgendes.

  1. Wir erhalten die Nummer des Durchlaufs aus der ersten Spalte der Tabelle.
  2. Erhalten wir die Daten nach der Nummer des Durchlaufs.
  3. Fügen die Symbole der Liste neben dem Multisymbol-Chart.
  4. Heben wir die Kontostand-Kurve auf dem Chart aller ausgewählten Ergebnisse hervor.

Der Code der Methode CProgram::SelectingResultsUsingKeys():

//+------------------------------------------------------------------+
//| Auswahl der Ergebnisse mit Tasten                                |
//+------------------------------------------------------------------+
bool CProgram::SelectingResultsUsingKeys(const long key)
  {
//--- Abfrage des Indexes der ausgewählten Zeile
   int selected_row=m_table_main.SelectedItem();
//--- Bestimmen der Richtung und der Zeile für das Verschieben der Scrollbar
   switch((int)key)
     {
      case KEY_UP :
         selected_row--;
         break;
      case KEY_DOWN :
         selected_row++;
         break;
      case KEY_HOME :
         selected_row=0;
         break;
      case KEY_END :
         selected_row=(int)m_table_main.RowsTotal()-1;
         break;
     }
//--- Verlassen, wenn (1) die Zeile nicht ausgewählt wurde oder (2) wenn dieselbe Zeile, wie vorher, ausgewählt wurde oder (3) wenn die Grenzen der Liste überschritten wurden
   if(selected_row==WRONG_VALUE || selected_row==m_table_main.SelectedItem() || 
      selected_row<0 || selected_row>=(int)m_table_main.RowsTotal())
      return(false);
//--- Markieren der Zeile und Verschieben der Scrollbar
   m_table_main.SelectRow(selected_row);
   m_table_main.Update();
   m_table_main.GetScrollVPointer().Update(true);
//--- Abfrage der Durchlaufnummer aus der ausgewählten Zeile der Tabelle
   ulong pass=(ulong)m_table_main.GetValue(0,m_table_main.SelectedItem());
//--- Abfrage der Daten nach der Durchlaufnummer
   m_frame_gen.GetFrameData(pass);
//--- Hinzufügen der Symbole zur Tabelle
   GetFrameSymbolsToTable();
//--- Hervorheben der Kurve im Chart nach der Durchlaufnummer
   SelectCurve(pass);
   return(true);
  }

Die Methode CProgram::SelectingResultsUsingKeys() wird beim Eintreffen des Ereignisses des Drückens der Tastatur (CHARTEVENT_KEYDOWN) im Event Handler des Programms aufgerufen:

//+------------------------------------------------------------------+
//| Event Handler                                                    |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Drücken der Taste
   if(id==CHARTEVENT_KEYDOWN)
     {
      //--- Auswahl der Ergebnisse mit Tasten
      if(SelectingResultsUsingKeys(lparam))
         return;
      //---
      return;
     }
...
  }

Es funktioniert wie folgt:

 Abb. 5. Auswählen der Zeilen der Tabelle mit der Tastatur.

Abb. 5. Auswählen der Zeilen der Tabelle mit der Tastatur.

Fazit

Der Artikel zeigt noch einen Fall, wo die Bibliothek der grafischen Interfaces EasyAndFast hilfreich sein kann. Es ist offensichtlich, dass die visuelle Komponente für die Analyse von Testergebnissen sehr wichtig ist. Der umfassende Einblick in die Arrays der Ergebnisse der Tests kann neue Denkanstöße und Ideen geben. Einige von ihnen wurden bereits von den Mitgliedern der MQL-Community vorgeschlagen.

Im Array der Rahmen können beispielsweise komplette Testberichte und nicht nur statistische Kennzahlen und Saldendaten gespeichert werden. Eine weitere Idee, die im Forum diskutiert wurde: ein benutzerdefiniertes Kriterium bei der Auswahl der Ergebnisse zu verwenden. Man kann zum Beispiel mehrere Dropdown-Listen oder Checkboxen für die Bildung des benutzerdefinierten Kriteriums nutzen, das nach der in den Einstellungen angegebenen Formel berechnet wird. Man kann sich schwer vorstellen, wie man das alles ohne grafisches Interface implementieren könnte.

Die Ideen, die Sie in den Kommentaren zum Artikel hinterlassen, können in einer der nächsten Versionen umgesetzt werden. Deshalb schlagen Sie bitte Ihre Varianten vor, wie die Anwendung für die Arbeit mit Optimierungsergebnissen weiterentwickelt werden kann.

Unten können Sie die Dateien für Tests herunterladen, um den im Artikel angeführten Code zu studieren.

Dateiname Kommentar
MacdSampleCFrames.mq5 Modifizierter Expert Advisor aus dem Standardpaket - MACD Sample
Program.mqh Datei mit der Klasse des Programms
CreateGUI.mqh Datei mit der Umsetzung der Methoden aus der Klasse des Programms in der Datei Program.mqh
Strategy.mqh Datei mit der modifizierten Klasse der Strategie MACD Sample (Multisymbol-Version)
FormatString.mqh Datei mit Hilfsfunktionen für die Formatierung von Zeilen
FrameGenerator.mqh Datei mit der Klasse für die Arbeit mit Optimierungsergebnissen.