English Русский 中文 Español 日本語 Português
Anwendung von OLAP im Handel (Teil 4): Quantitative und visuelle Analyse der Testberichte

Anwendung von OLAP im Handel (Teil 4): Quantitative und visuelle Analyse der Testberichte

MetaTrader 5Handel | 29 Juni 2020, 09:29
383 0
Stanislav Korotky
Stanislav Korotky

In diesem Artikel befassen wir uns weiter mit OLAP (On-Line Analytical Processing) und seiner Anwendbarkeit im Handel.

In früheren Artikeln erörterten wir allgemeine Techniken zur Konstruktion von Klassen, die mehrdimensionale Arrays akkumulierten und analysierten, und befassten uns mit der Visualisierung von Analyseergebnissen in der grafischen Nutzeroberfläche. Unter dem Gesichtspunkt der Anwendung befassten sich die ersten beiden Artikel mit Handelsberichten, die auf verschiedene Weise erhalten wurden: von einem Strategietester, aus der Online-Handelshistorie, aus HTML- und CSV-Dateien (einschließlich MQL5-Handelssignalen). Nach einem leichten Code-Refactoring im dritten Artikel wurde OLAP für die Analyse von Notierungen und für die Entwicklung von Handelsstrategien verwendet. Bitte lesen Sie die vorherigen Artikel, um das neue Material verstehen zu können (schauen Sie sich die Klammern an, um zu sehen, worauf Sie besonders achten sollten):

In diesem Artikel werden wir den OLAP-Bereich durch die Analyse der Optimierungsergebnisse von MetaTrader 5 erweitern.

Um dieses Projekt durchführen zu können, müssen wir zunächst die grafische Nutzeroberfläche verbessern, die bereits in Teil 2 betrachtet wurde. Alle in Teil 3 durchgeführten Code-Verbesserungen betrafen direkt die OLAP-Engine. Es wurde jedoch keine relevante Aktualisierung der Visualisierung durchgeführt. Daran werden wir arbeiten, indem wir den OLAPGUI-Handelsberichtsanalysator aus dem zweiten Artikel als Testaufgabe im Rahmen des aktuellen Artikels verwenden. Wir werden auch diesen grafischen Teil vereinheitlichen, so dass er leicht für jedes andere neue Anwendungsgebiet, insbesondere für den geplanten Analysator der Optimierungsergebnisse, angewendet werden kann.

Zu den Anwendungsgrafiken

Das Zentrum der GUI für OLAP ist die speziell entwickelte visuelle Komponente CGraphicInPlot. Ihre erste Implementierung, die in Artikel 2 vorgestellt wurde, hatte einige Nachteile. Dazu gehörte die Darstellung von Beschriftungen der Achsen. Wir schafften es, die Namen von Selektorzellen (wie die Namen der Wochentage oder die Namen von Währungen) auf der horizontalen X-Achse anzuzeigen, wenn dies erforderlich war. In allen anderen Fällen werden die Zahlen jedoch "so wie sie sind" angezeigt, was nicht immer nutzerfreundlich ist. Eine weitere Anpassung ist für die Y-Achse erforderlich, die normalerweise aggregierte Werte anzeigt. Abhängig von den Einstellungen kann sie Selektorwerte anzeigen, genau dort ist die Verbesserung notwendig. Ein Beispiel für eine schlechte Darstellung von Methoden ist die Abfrage der durchschnittlichen Positionshaltezeit für ein Symbol.

Durchschnittliche Positionshaltezeit nach Symbolen (Sekunden)

Durchschnittliche Positionshaltezeit nach Symbolen (Sekunden)

Da Y nicht einen Selektor (in dem die Werte auf Würfelzellengröße gerundet werden), sondern einen aggregierten Wert für die Dauer in Sekunden anzeigt, sind solche großen Zahlen schwer zu erkennen. Um dieses Problem zu lösen, wollen wir versuchen, die Sekunden durch die Dauer des aktuellen Zeitrahmens zu teilen. In diesem Fall stellen die Werte die Anzahl der Balken dar. Um dies zu erreichen, müssen wir ein bestimmtes Flag an die Klasse CGraphicInPlot und weiter an die Achsen-Handhabungsklasse CAxis übergeben. Flags, die den Betriebsmodus ändern, können zahlreich sein. Reservieren Sie daher für sie eine spezielle neue Klasse mit dem Titel AxisCustomizer in der Datei Plot.mqh.

  class AxisCustomizer
  {
    public:
      const CGraphicInPlot *parent;
      const bool y; // true for Y, false for X
      const bool periodDivider;
      const bool hide;
      AxisCustomizer(const CGraphicInPlot *p, const bool axisY,
        const bool pd = false, const bool h = false):
        parent(p), y(axisY), periodDivider(pd), hide(h) {}
  };

Möglicherweise können der Klasse verschiedene Etikettenanzeigefunktionen hinzugefügt werden. Aber im Moment speichert sie nur das Vorzeichen des Achsentyps (X oder Y) und einige logische Optionen, wie z.B. periodDivider und 'hide' (ausblenden). Die erste Option bedeutet, dass Werte durch PeriodSeconds() geteilt werden sollten. Die zweite Option wird später in Betracht gezogen.

Die Objekte dieser Klasse gelangen über spezielle Methoden in CGraphicInPlot:

  class CGraphicInPlot: public CGraphic
  {
    ...
      void InitAxes(CAxis &axe, const AxisCustomizer *custom = NULL);
      void InitXAxis(const AxisCustomizer *custom = NULL);
      void InitYAxis(const AxisCustomizer *custom = NULL);
  };
  
  void CGraphicInPlot::InitAxes(CAxis &axe, const AxisCustomizer *custom = NULL)
  {
    if(custom)
    {
      axe.Type(AXIS_TYPE_CUSTOM);
      axe.ValuesFunctionFormat(CustomDoubleToStringFunction);
      axe.ValuesFunctionFormatCBData((AxisCustomizer *)custom);
    }
    else
    {
      axe.Type(AXIS_TYPE_DOUBLE);
    }
  }
  
  void CGraphicInPlot::InitXAxis(const AxisCustomizer *custom = NULL)
  {
    InitAxes(m_x, custom);
  }
  
  void CGraphicInPlot::InitYAxis(const AxisCustomizer *custom = NULL)
  {
    InitAxes(m_y, custom);
  }

Wenn ein solches Objekt nicht erzeugt und nicht an Grafikklassen übergeben wird, zeigt die Standardbibliothek die Werte in der üblichen Weise an, als eine Zahl AXIS_TYPE_DOUBLE.

Hier verwenden wir den Standardbibliotheksansatz zur Anpassung von Achsenbeschriftungen: Der Achsentyp wird gleich AXIS_TYPE_CUSTOM gesetzt und ein Zeiger auf AxisCustomizer wird über ValuesFunctionFormatCBData übergeben. Weiter wird er von der Basisklasse CGraphic an die Etikettenzeichenfunktion CustomDoubleToStringFunction übergeben (er wird durch den Aufruf ValuesFunctionFormat im obigen Code gesetzt). Natürlich benötigen wir die Funktion CustomDoubleToStringFunction, die früher in vereinfachter Form ohne die Objekte der Klasse AxisCustomizer implementiert wurde (das Diagramm CGraphicInPlot fungierte als Setup-Objekt).

  string CustomDoubleToStringFunction(double value, void *ptr)
  {
    AxisCustomizer *custom = dynamic_cast<AxisCustomizer *>(ptr);
    if(custom == NULL) return NULL;
    
    // check options
    if(!custom.y && custom.hide) return NULL; // case of X axis and "no marks" mode
    
    // in simple cases return a string
    if(custom.y) return (string)(float)value;  
    
    const CGraphicInPlot *self = custom.parent; // obtain actual object with cache 
    if(self != NULL)
    {
      ... // retrieve selector mark for value
    }
  }

Die Objekte für die Anpassung AxisCustomizer werden in der Klasse CPlot gespeichert, bei der es sich um ein GUI-Steuerelement (geerbt von CWndClient) und einen Container für CGraphicInPlot handelt:

  class CPlot: public CWndClient
  {
    private:
      CGraphicInPlot *m_graphic;
      ENUM_CURVE_TYPE type;
      
      AxisCustomizer *m_customX;
      AxisCustomizer *m_customY;
      ...
    
    public:
      void InitXAxis(const AxisCustomizer *custom = NULL)
      {
        if(CheckPointer(m_graphic) != POINTER_INVALID)
        {
          if(CheckPointer(m_customX) != POINTER_INVALID) delete m_customX;
          m_customX = (AxisCustomizer *)custom;
          m_graphic.InitXAxis(custom);
        }
      }
      ...
  };

So können Achseneinstellungen m_customX und m_customY im Objekt nicht nur in der Phase der Werteformatierung in CustomDoubleToStringFunction verwendet werden, sondern sie können viel früher verwendet werden, wenn Daten-Arrays nur mit einer der Methoden CurveAdd an CPlot übergeben werden. Zum Beispiel:

  CCurve *CPlot::CurveAdd(const PairArray *data, const string name = NULL)
  {
    if(CheckPointer(m_customY) != POINTER_INVALID) && m_customY.periodDivider)
    {
      for(int i = 0; i < ArraySize(data.array); i++)
      {
        data.array[i].value /= PeriodSeconds();
      }
    }
    
    return m_graphic.CurveAdd(data, type, name);
  }

Der Code zeigt die Verwendung der Option periodDivider, die alle Werte durch PeriodSeconds() dividiert. Diese Operation wird ausgeführt, bevor die Standardbibliothek Daten empfängt und die Gittergröße für diese berechnet. Dieser Schritt ist wichtig, da es, nachdem das Gitter bereits gezählt wurde, für eine Anpassung in der Funktion CustomDoubleToStringFunction zu spät ist.

Der Aufrufcode im Dialogfeld muss das AxisCustomizer-Objekt zum Zeitpunkt der Cube-Bildung erstellen und initialisieren. Zum Beispiel:

  AGGREGATORS at = ...  // get aggregator type from GUI
  ENUM_FIELDS af = ...  // get aggregator field from GUI
  SORT_BY sb = ...      // get sorting mode from GUI
  
  int dimension = 0;    // calculate cube dimensions from GUI
  for(int i = 0; i < AXES_NUMBER; i++)
  {
    if(Selectors[i] != SELECTOR_NONE) dimension++;
  }
  
  bool hideMarksOnX = (dimension > 1 && SORT_VALUE(sb));
  
  AxisCustomizer *customX = NULL;
  AxisCustomizer *customY = NULL;
  
  customX = new AxisCustomizer(m_plot.getGraphic(), false, Selectors[0] == SELECTOR_DURATION, hideMarksOnX);
  if(af == FIELD_DURATION)
  {
    customY = new AxisCustomizer(m_plot.getGraphic(), true, true);
  }
  
  m_plot.InitXAxis(customX);
  m_plot.InitYAxis(customY);

Hier ist m_plot die Dialogvariable, die das CPlot-Steuerelement speichert. Der vollständige Code der folgenden Methode OLAPDialog::process zeigt, wie dies tatsächlich durchgeführt wird. Hier ist das obige Beispiel mit dem automatisch aktivierten Modus periodDivider:

Durchschnittliche Positionslebensdauer nach Symbolen (Balken des aktuellen Zeitrahmens, D1)

Durchschnittliche Positionslebensdauer nach Symbolen (Balken des aktuellen Zeitrahmens, D1)

Eine weitere Variable in AxisCustomizer, 'hide', bietet die Möglichkeit, Beschriftungen entlang der X-Achse vollständig auszublenden. Dieser Modus wird benötigt, wenn die Sortierung nach einem Wert im multidimensionalen Array ausgewählt wird. In diesem Fall haben die Etiketten in jeder Zeile ihre eigene Reihenfolge, so dass entlang der X-Achse nichts angezeigt werden kann. Der multidimensionale Würfel unterstützt die Sortierung, die in anderen Modi verwendet werden kann, insbesondere nach Beschriftungen.

Die Option 'hide' funktioniert innerhalb der Funktion CustomDoubleToStringFunction. Das Standardverhalten dieser Funktion impliziert das Vorhandensein von Selektoren; die Beschriftungen der Selektoren werden für die X-Achse in den spezialisierten Klasse CurveSubtitles zwischengespeichert, und sie werden durch den Gitterunterteilungsindex in das Diagramm zurückgegeben. Das gesetzte 'hide'-Flag beendet jedoch diesen Vorgang ganz am Anfang für jede Abszisse, und die Funktion gibt NULL (nicht anzeigbarer Wert) zurück.

Das zweite Problem, das in der Grafik behoben werden muss, steht im Zusammenhang mit der Darstellung eines Histogramms. Wenn mehrere Zeilen (Datenvektoren) im Diagramm angezeigt werden, überlappen die Histogrammbalken einander und der größte von ihnen kann alle anderen vollständig ausblenden.

Die Basisklasse CGraphic verfügt über die virtuelle Methode HistogramPlot. Sie muss außer Kraft gesetzt werden, um die Spalten visuell zu trennen. Es wäre gut, ein nutzerdefiniertes Feld im Objekt CCurve zu haben, in dem beliebige Daten gespeichert werden (die Daten würden vom Client-Code wie erforderlich interpretiert werden). Leider gibt es ein solches Feld nicht. Daher werden wir eine der Standardeigenschaften verwenden, die im aktuellen Projekt nicht verwendet wurde. Ich habe LinesSmoothStep gewählt. Unter Verwendung der Setter-Methode CCurve::LinesSmoothStep wird unser Anrufercode die Sequenznummer in das Feld schreiben. Dieser Code kann leicht durch Verwendung der Getter-Methode CCurve::LinesSmoothStep in der neuen HistogramPlot-Implementierung erhalten werden. Hier ist ein Beispiel dafür, wie eine Zeilennummer in LinesSmoothStep geschrieben wird:

  CCurve *CGraphicInPlot::CurveAdd(const double &x[], const double &y[], ENUM_CURVE_TYPE type, const string name = NULL)
  {
    CCurve *c = CGraphic::CurveAdd(x, y, type, name);
    c.LinesSmoothStep((int)CGraphic::CurvesTotal());    // +
    ...
    return CacheIt(c);
  }

Wenn Sie die Gesamtzahl der Zeilen und die Nummer der aktuellen Zeile kennen, können Sie jeden ihrer Punkte beim Rendern leicht nach links oder zum Schreiben verschieben. Hier ist eine angepasste Version von HistogramPlot. Die aktualisierten Zeilen sind mit einem Kommentar mit "*" gekennzeichnet; neu hinzugefügte Zeilen sind mit "+" markiert.

  void CGraphicInPlot::HistogramPlot(CCurve *curve) override
  {
      const int size = curve.Size();
      const double offset = curve.LinesSmoothStep() - 1;                   // +
      double x[], y[];
  
      int histogram_width = curve.HistogramWidth();
      if(histogram_width <= 0) return;
      
      curve.GetX(x);
      curve.GetY(y);
  
      if(ArraySize(x) == 0 || ArraySize(y) == 0) return;
      
      const int w = m_width / size / 2 / CGraphic::CurvesTotal();          // +
      const int t = CGraphic::CurvesTotal() / 2;                           // +
      const int half = ((CGraphic::CurvesTotal() + 1) % 2) * (w / 2);      // +
  
      int originalY = m_height - m_down;
      int yc0 = ScaleY(0.0);
  
      uint clr = curve.Color();
  
      for(int i = 0; i < size; i++)
      {
        if(!MathIsValidNumber(x[i]) || !MathIsValidNumber(y[i])) continue;
        int xc = ScaleX(x[i]);
        int yc = ScaleY(y[i]);
        int xc1 = xc - histogram_width / 2 + (int)(offset - t) * w + half; // *
        int xc2 = xc + histogram_width / 2 + (int)(offset - t) * w + half; // *
        int yc1 = yc;
        int yc2 = (originalY > yc0 && yc0 > 0) ? yc0 : originalY;
  
        if(yc1 > yc2) yc2++;
        else yc2--;
  
        m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr);
      }
  }

Bald werden wir prüfen, wie dies aussieht.

Ein weiterer ärgerlicher Moment hängt mit der Standard-Implementierung der Darstellung von Linien zusammen. Wenn Daten einen nicht-numerischen Wert haben, bricht CGraphic die Zeile um. Das ist schlecht für unsere Aufgabe, da einige der Würfelzellen möglicherweise keine Daten enthalten und Aggregatoren NaN in solche Zellen schreiben. Einige Würfel, wie z.B. der kumulierte Gesamtsaldo mehrerer Abschnitte, würden eine schlechte Darstellung haben, da der Wert in jedem Geschäft nur in einem Abschnitt geändert wird. Um die negativen Auswirkungen von gestrichelten Linien zu sehen, sehen Sie sich die Abbildung "Saldenkurve einzeln für jedes Symbol" in Artikel 2 an.

Um dieses Problem zu beheben, wurde die Methode LinesPlot zusätzlich neu definiert (siehe Quellcodes, Datei Plot.mqh). Das Ergebnis der Operation ist unten im Abschnitt über die Verarbeitung der Standarddateien der Tester dargestellt.

Das letzte Grafikproblem schließlich bezieht sich auf die Definition von Null-Achsen in der Standardbibliothek. Nullen werden in der CGraphic::CreateGrid-Methode auf folgende triviale Weise gesucht (zeigt einen Fall für Y; die X-Achse wird auf die gleiche Weise verarbeitet):

  if(StringToDouble(m_yvalues[i]) == 0.0)
  ...

Beachten Sie, dass m_yvalues Zeichenkettenbezeichnungen sind. Offensichtlich wird jedes Label, das keine Zahl enthält, 0 ergeben. Dies geschieht selbst dann, wenn der Anzeigemodus AXIS_TYPE_CUSTOM für ein Diagramm eingestellt ist. Infolgedessen werden in Diagrammen nach Werten, Wochentagen, Arten von Geschäften und anderen Selektoren alle Werte als Null behandelt, wenn sie in einer Schleife durch das gesamte Gitter geprüft werden. Der Endwert hängt jedoch von der letzten Stichprobe ab, die in einer fetteren Linie angezeigt wird (obwohl sie nicht Null ist). Da jede Stichprobe (wenn auch nur vorübergehend) zu einem Kandidaten für 0 wird, überspringt sie außerdem die Darstellung einer einfachen Gitterlinie, wodurch das gesamte Gitter verschwindet.

Da die Methode CreateGrid ebenfalls virtuell ist, werden wir sie mit einer intelligenteren Prüfung auf 0 neu definieren. Diese Prüfung ist als Hilfsfunktion isZero implementiert.

  bool CGraphicInPlot::isZero(const string &value)
  {
    if(value == NULL) return false;
    double y = StringToDouble(value);
    if(y != 0.0) return false;
    string temp = value;
    StringReplace(temp, "0", "");
    ushort c = StringGetCharacter(temp, 0);
    return c == 0 || c == '.';
  }
  
  void CGraphicInPlot::CreateGrid(void) override
  {
    int xc0 = -1.0;
    int yc0 = -1.0;
    for(int i = 1; i < m_ysize - 1; i++)
    {
      m_canvas.LineHorizontal(m_left + 1, m_width - m_right, m_yc[i], m_grid.clr_line);     // *
      if(isZero(m_yvalues[i])) yc0 = m_yc[i];                                               // *
      
      for(int j = 1; j < m_xsize - 1; j++)
      {
        if(i == 1)
        {
          m_canvas.LineVertical(m_xc[j], m_height - m_down - 1, m_up + 1, m_grid.clr_line); // *
          if(isZero(m_xvalues[j])) xc0 = m_xc[j];                                           // *
        }
        
        if(m_grid.has_circle)
        {
          m_canvas.FillCircle(m_xc[j], m_yc[i], m_grid.r_circle, m_grid.clr_circle);
          m_canvas.CircleWu(m_xc[j], m_yc[i], m_grid.r_circle, m_grid.clr_circle);
        }
      }
    }
    
    if(yc0 > 0) m_canvas.LineHorizontal(m_left + 1, m_width - m_right, yc0, m_grid.clr_axis_line);
    if(xc0 > 0) m_canvas.LineVertical(xc0, m_height - m_down - 1, m_up + 1, m_grid.clr_axis_line);
  }

OLAP GUI

Wir haben die erforderlichen Korrekturen in Grafiken implementiert. Jetzt überarbeiten wir die Fensteroberfläche und machen sie universell. In der nicht handelsüblichen EA OLAPGUI aus dem zweiten Artikel wurden Operationen mit dem Dialog in der OLAPGUI.mqh Header-Datei implementiert. Sie speicherte eine Menge angewandter Funktionen der vorherigen Aufgabe, der Analyse von Handelsberichten. Da wir den gleichen Dialog für beliebige Daten verwenden werden, müssen wir die Datei in zwei Teile aufteilen: der eine wird das allgemeine Schnittstellenverhalten implementieren, der andere Einstellungen eines spezifischen Projekts enthalten.

Benennen Sie die ex OLAPDialog-Klasse in OLAPDialogBase um. Die fest kodierten statistischen Arrays 'selectors', 'settings', 'defaults', die eigentlich die Dialog-Steuerelemente beschreiben, werden leere dynamische Vorlagen sein, die dann durch abgeleitete Klassen gefüllt werden. Variablen:

    OLAPWrapper *olapcore;    // <-- template <typename S,typename T> class OLAPEngine, since part 3
    OLAPDisplay *olapdisplay;

werden auch vererbt, weil sie durch Typen von Selektoren und Datensatzfeldern standardisiert werden müssen, die im Anwendungsteil jeder OLAP-Engine definiert sind. Denken Sie daran, dass die alte Klasse OLAPWrapper durch das Umfunktionieren in Artikel 3 in die Vorlagenklasse OLAPEngine<S,T> umgewandelt wurde.

Zwei neue abstrakte Methoden sind für die Hauptlogik reserviert:

  virtual void setup() = 0;
  virtual int process() = 0;

Die erste, setup(), konfiguriert die Schnittstelle, der zweite, process(), startet die Analyse. setup() wird von OLAPDialogBase::Create aufgerufen

  bool OLAPDialogBase::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    setup(); // +
    ...
  }

Der Nutzer initiiert den Start der Analyse durch Klicken auf die Schaltfläche, daher wurde die Methode OLAPDialogBase::OnClickButton am meisten verändert: Der größte Teil des Codes wurde daraus entfernt und die entsprechende Funktionalität (Lesen der Steuereigenschaften und Starten der darauf basierenden OLAP-Engine) wurde an die Methode prozess() delegiert.

  void OLAPDialogBase::OnClickButton(void)
  {
    if(processing) return; // prevent re-entrancy
    
    if(browsing)           // 3D-cube browsing support
    {
      currentZ = (currentZ + 1) % maxZ;
      validateZ();
    }
  
    processing = true;
    const int n = process();
    if(n == 0 && processing)
    {
      finalize();
    }
  }

Bitte beachten Sie, dass die Klasse OLAPDialogBase die gesamte Operationsschnittstellenlogik implementiert, angefangen bei der Erstellung von Steuerelementen bis hin zur Verarbeitung von Ereignissen, die den Zustand der Steuerelemente beeinflussen. Sie weiß jedoch nichts über den Inhalt von Controls.

Die Klasse OLAPDisplay implementiert die virtuelle Schnittstelle Display aus OLAPCommon.mqh (wurde in Artikel 3 besprochen). Erinnern Sie sich, dass die Anzeigeschnittstelle ein Rückrufpunkt vom OLAP-Kernel ist, um Analyseergebnisse bereitzustellen (die im ersten Parameter im MetaCube-Klassenobjekt übergeben werden). Dank des Zeigers auf das übergeordnete Fenster in der Klasse OLAPDisplay wird eine Kette der Weiterleitung der Daten des Würfels in den Dialog organisiert (eine solche "Weiterleitung" war erforderlich, da es in MQL5 keine Mehrfachvererbung gibt).

  class OLAPDisplay: public Display
  {
    private:
      OLAPDialogBase *parent;
  
    public:
      OLAPDisplay(OLAPDialogBase *ptr,): parent(ptr) {}
      virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override;
  };

An dieser Stelle möchte ich eine Besonderheit erwähnen, die sich auf die Gewinnung realer Namen von nutzerdefinierten Feldern aus abgeleiteten Adapterklassen bezieht. Zuvor haben wir im zweiten Teil unsere nutzerdefinierten Felder (wie MFE und MAE) zu den Standardfeldern hinzugefügt. Sie waren also im Voraus bekannt und wurden in den Code eingebaut. Wenn wir jedoch mit Optimierungsberichten arbeiten, müssen wir sie im Hinblick auf die Eingabeparameter von EA analysieren, während diese Parameter (ihre Namen) nur aus analysierten Daten gewonnen werden können.

Der Adapter übergibt die Namen der nutzerdefinierten Felder mit der neuen Methode assignCustomFields an den Aggregator (Metawürfel). Dies geschieht immer "hinter den Kulissen", d.h. automatisch in der Methode Analyst::acquireData. Wenn die Methode metaData.getDimensionTitle innerhalb von OLAPDisplay::display aufgerufen wird, um Abschnittsbezeichnungen entlang der Achsen zu erhalten, und wenn die Ordnungszahl des Feldes n die Kapazität der eingebauten Feldaufzählung überschreitet, wissen wir daher, dass wir es mit einem erweiterten Feld zu tun haben, und können eine Beschreibung vom Würfel anfordern. Die allgemeine Struktur der Methode OLAPDisplay::display hat sich nicht geändert. Sie können sie überprüfen, indem Sie den untenstehenden Quellcode mit dem Code aus Artikel 2 vergleichen.

Darüber hinaus müssen die Namen der nutzerdefinierten Felder im Dialog im Voraus bekannt sein, um die Oberflächenelemente ausfüllen zu können. Zu diesem Zweck enthält die Klasse OLAPDialogBase eine neue setCustomFields-Methode zum Festlegen nutzerdefinierter Felder.

    int customFieldCount;
    string customFields[];
    
    virtual void setCustomFields(const DataAdapter &adapter)
    {
      string names[];
      if(adapter.getCustomFields(names) > 0)
      {
        customFieldCount = ArrayCopy(customFields, names);
      }
    }

Natürlich müssen wir den Dialog und den Adapter mit dieser Methode in den Test-EA einbinden (siehe unten). Danach werden aussagekräftige Feldnamen (anstelle von nummerierten 'custom 1' und so weiter) in den Dialog-Steuerelementen sichtbar. Dies ist eine vorübergehende Lösung. Unter anderem dieser Aspekt bedarf weiterer Code-Optimierung. Aber sie werden in diesem Artikel als unbedeutend betrachtet.

Der Anwendungsteil der Schnittstelleneinrichtung in der modifizierten OLAPGUI wurde von OLAPGUI.mqh in die Header-Datei OLAPGUI_Trades.mqh "verschoben". Der Name der Dialogklasse hat sich nicht geändert: OLAPDialog. Er hängt jedoch von den Parametern der Vorlage ab, die dann zur Spezialisierung des OLAPEngine-Objekts verwendet werden:

  template<typename S, typename F>
  class OLAPDialog: public OLAPDialogBase
  {
    private:
      OLAPEngine<S,F> *olapcore;
      OLAPDisplay *olapdisplay;
  
    public:
      OLAPDialog(OLAPEngine<S,F> &olapimpl);
      ~OLAPDialog(void);
      virtual int process() override;
      virtual void setup() override;
  };
  
  template<typename S, typename F>
  OLAPDialog::OLAPDialog(OLAPEngine<S,F> &olapimpl)
  {
    curveType = CURVE_POINTS;
    olapcore = &olapimpl;
    olapdisplay = new OLAPDisplay(&this);
  }
  
  template<typename S, typename F>
  OLAPDialog::~OLAPDialog(void)
  {
    delete olapdisplay;
  }

Alle Arbeiten werden in den Methoden 'setup' und 'process' durchgeführt. Die Methode 'setup' füllt die Arrays 'settings', 'selectors', 'defaults' mit den gleichen Werten, die uns bereits aus dem zweiten Artikel bekannt sind (das Erscheinungsbild der Oberfläche ändert sich nicht). Die Methode 'process' startet die Analyse im angegebenen Abschnitt und ist fast vollständig identisch mit der vorherigen Ereignisbehandlung von OnClickButton.

  template<typename S, typename F>
  int OLAPDialog::process() override
  {
    SELECTORS Selectors[4];
    ENUM_FIELDS Fields[4];
    AGGREGATORS at = (AGGREGATORS)m_algo[0].Value();
    ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[1].Value();
    SORT_BY sb = (SORT_BY)m_algo[2].Value();
  
    ArrayInitialize(Selectors, SELECTOR_NONE);
    ArrayInitialize(Fields, FIELD_NONE);
  
    int matches[10] = // selectors in combo-boxes (specific record fields are bound internally)
    {
      SELECTOR_NONE, SELECTOR_SERIAL, SELECTOR_SYMBOL, SELECTOR_TYPE, SELECTOR_MAGIC,
      SELECTOR_WEEKDAY, SELECTOR_WEEKDAY, SELECTOR_DAYHOUR, SELECTOR_DAYHOUR, SELECTOR_DURATION
    };
    
    int subfields[] = // record fields listed in combo-boxes after selectors and accessible directly  
    {
      FIELD_LOT, FIELD_PROFIT_AMOUNT, FIELD_PROFIT_PERCENT, FIELD_PROFIT_POINT,
      FIELD_COMMISSION, FIELD_SWAP, FIELD_CUSTOM_1, FIELD_CUSTOM_2
    };
    
    for(int i = 0; i < AXES_NUMBER; i++) // up to 3 orthogonal axes are supported
    {
      if(!m_axis[i].IsVisible()) continue;
      int v = (int)m_axis[i].Value();
      if(v < 10) // selectors (every one is specialized for a field already)
      {
        Selectors[i] = (SELECTORS)matches[v];
        if(v == 5 || v == 7) Fields[i] = FIELD_OPEN_DATETIME;
        else if(v == 6 || v == 8) Fields[i] = FIELD_CLOSE_DATETIME;
      }
      else // pure fields
      {
        Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS;
        Fields[i] = (TRADE_RECORD_FIELDS)subfields[v - 10];
      }
    }
  
    m_plot.CurvesRemoveAll();
    AxisCustomizer *customX = NULL;
    AxisCustomizer *customY = NULL;
  
    if(at == AGGREGATOR_IDENTITY || at == AGGREGATOR_COUNT) af = FIELD_NONE;
    
    if(at != AGGREGATOR_PROGRESSIVE)
    {
      customX = new AxisCustomizer(m_plot.getGraphic(), false, Selectors[0] == SELECTOR_DURATION, (dimension > 1 && SORT_VALUE(sb)));
    }
    
    if((af == FIELD_DURATION)
    || (at == AGGREGATOR_IDENTITY && Selectors[1] == SELECTOR_DURATION))
    {
      customY = new AxisCustomizer(m_plot.getGraphic(), true, true);
    }
    
    m_plot.InitXAxis(customX);
    m_plot.InitYAxis(customY);
    m_button_ok.Text("Processing...");
    return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb);
  }

Die zuvor beschriebenen Objekte AxisCustomizer zum Einrichten der Achsen werden am Ende der Methode erstellt. Für beide Achsen (X und Y) wird die Division durch PeriodSeconds() aktiviert, wenn mit einem Dauerfeld gearbeitet wird (entweder im Aggregator oder im Selektor, wenn der Aggregatortyp AGGREGATOR_IDENTITY ist — in diesem Fall verteilen Selektoren den Inhalt der Felder nicht auf benannte Zellen, sondern der Inhalt wird direkt dem Würfel übergeben). Die X-Achse wird deaktiviert, wenn die Würfeldimension größer als 1 ist und wenn die Sortierung ausgewählt ist.

Werfen wir nun einen Blick auf die Programmdatei OLAPGUI.mq5. Zu den weiteren Unterschieden gegenüber der vorherigen Version gehört die geänderte Reihenfolge der Verbindung von Header-Dateien. Früher wurden Adapter für Berichte in den Kern aufgenommen (weil es keine anderen Datenquellen gab). Jetzt sollten sie explizit als HTMLcube.mqh und CSVcube.mqh geschrieben werden. Weiterhin wird im OnInit-Code der entsprechende Adaptertyp in Abhängigkeit von den Eingabedaten vorbereitet, und dann wird der Adapter durch Aufruf von _defaultEngine.setAdapter an die Engine übergeben. Dieser Codeteil wurde bereits im Programm OLAPRPRT.mq5 aus Artikel 3 verwendet, wo wir den korrekten Ansatz mit der Zerlegung in universelle und angewandte Teile getestet haben. Allerdings hatte OLAPRPRT im vorherigen Teil keine grafische Nutzeroberfläche. Lassen Sie uns diesen Fehler jetzt beheben.

Um die strikte Trennung von Standard- und nutzerdefinierten Feldern zu demonstrieren, wurde die Klasse CustomTradeRecord, die MFE- und MAE-Felder berechnet, von OLAPTrades.mqh in OLAPTradesCustom.mqh verschoben (ihr Code ist beigefügt). Auf diese Weise können wir die Entwicklung anderer nutzerdefinierter Felder auf der Grundlage von Geschäften vereinfachen, falls solche benötigt werden. Ändern Sie einfach den Algorithmus in OLAPTradesCustom.mqh, während sich der OLAP-Kernel nicht ändert. Alle Standardkomponenten, wie Handelsdatensatzfelder, verbundene Selektoren, die Basisklasse TradeRecord, die OLAPEngineTrade-Engine und der Adapter für die Historie bleiben in OLAPTrades.mqh. OLAPTradesCustom.mqh hat einen Link zu OLAPTrades.mqh, der es ermöglicht, alle oben genannten Elemente in das Projekt einzubinden.

  #include <OLAP/OLAPTradesCustom.mqh> // internally includes OLAPTrades.mqh 
  #include <OLAP/HTMLcube.mqh>
  #include <OLAP/CSVcube.mqh>
  #include <OLAP/GUI/OLAPGUI_trades.mqh>
  
  OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine);
  
  int OnInit()
  {
    if(ReportFile == "")
    {
      Print("Analyzing account history");
      _defaultEngine.setAdapter(&_defaultHistoryAdapter);
    }
    else
    {
      if(StringFind(ReportFile, ".htm") > 0 && _defaultHTMLReportAdapter.load(ReportFile))
      {
        _defaultEngine.setAdapter(&_defaultHTMLReportAdapter);
      }
      else
      if(StringFind(ReportFile, ".csv") > 0 && _defaultCSVReportAdapter.load(ReportFile))
      {
        _defaultEngine.setAdapter(&_defaultCSVReportAdapter);
      }
      else
      {
        Print("Unknown file format: ", ReportFile);
        return INIT_PARAMETERS_INCORRECT;
      }
    }
    
    ...
    
    if(!dialog.Create(0, "OLAPGUI" + (ReportFile != "" ? " : " + ReportFile : ""), 0,  0, 0, 750, 560)) return INIT_FAILED;
    
    if(!dialog.Run()) return INIT_FAILED;
    return INIT_SUCCEEDED;
  }

Starten Sie die aktualisierte OLAPGUI.mq5 und erstellen Sie mehrere Datenabschnitte, um sicherzustellen, dass das neue Prinzip für die dynamische Aktivierung der Kernel-Abhängigkeit von verwendeten Adaptern und von Datensatztypen ordnungsgemäß funktioniert. Wir werden auch den visuellen Effekt der Änderungen überprüfen.

Sie können die untenstehenden Ergebnisse mit Screenshots aus Artikel 2 vergleichen. Unten sehen Sie die Abhängigkeit der Felder "Profit" und "Duration" (Gewinn und Dauer) für jedes Geschäft. Nun wird die Dauer entlang der X-Achse in Balken des aktuellen Zeitrahmens (hier D1) und nicht in Sekunden ausgedrückt.

Abhängigkeit des Gewinns von der Dauer (in Balken des aktuellen Zeitrahmens, hier D1)

Abhängigkeit des Gewinns von der Dauer (in Balken des aktuellen Zeitrahmens, hier D1)

Die Aufschlüsselung der Gewinne nach Symbolen und Wochentagen zeigt die gespreizten Histogrammbalken und das richtige Raster.

Gewinne nach Symbolen und Wochentagen

Gewinne nach Symbolen und Wochentagen

Die Gewinnanalyse nach Losgröße bei Geschäften ist in der folgenden Abbildung dargestellt. Im Gegensatz zu Artikel 2 werden die Losgrößen nicht im Protokoll, sondern direkt auf der X-Achse angezeigt.

Gewinne nach Losgröße

Gewinne nach Losgröße

Die letzte Option ist "Anzahl der Geschäfte nach Symbolen und Typen". In der vorherigen Version wurden Linien verwendet, weil sich die Histogramme überlagerten. Diese Frage ist nicht mehr relevant.

Die Anzahl der Geschäfte nach Symbolen und Typen (Histogramm)

Die Anzahl der Geschäfte nach Symbolen und Typen (Histogramm)

Wir haben alle Elemente im Zusammenhang mit der Analyse von Handelsberichten berücksichtigt. Erwähnenswert ist auch eine neue Datenquelle, die den MQL-Programmierern zur Verfügung steht: tst-Dateien im internen Testerformat.

Verbindung von Standard-Tester-Dateien (*.tst)

Die Entwickler von MetaTrader 5 haben kürzlich die vom Tester gespeicherten Dateiformate offengelegt. Insbesondere die Daten eines einzelnen Durchgangs, die wir erst nach dem Export in einen HTML-Bericht analysieren konnten, stehen nun zum direkten Lesen aus einer tst-Datei zur Verfügung.

Auf Einzelheiten zur internen Struktur der Akte werden wir nicht näher eingehen. Stattdessen verwenden wir eine fertige Bibliothek zum Lesen von tst-Dateien - SingleTesterCache by fxsaber. Wenn man sie auf der Basis einer "Blackbox" verwendet, ist es einfach, eine Reihe von Datensätzen von Geschäften zu erhalten. Das Geschäft wird in der Bibliothek durch die Klasse TradeDeal präsentiert. Um die Liste der Geschäfte zu erhalten, binden Sie die Bibliothek ein, erstellen Sie das Hauptklassenobjekt SINGLETESTERCACHE und laden Sie die erforderliche Datei mit der Methode 'load'.

  #include <fxsaber/SingleTesterCache/SingleTesterCache.mqh>
  ...
  SINGLETESTERCACHE SingleTesterCache;
  if(SingleTesterCache.Load(file))
  {
    Print("Tester cache import: ", ArraySize(SingleTesterCache.Deals), " deals");
  }

Das Array SingleTesterCache.Deals enthält alle Deals. Die Daten jedes im Tester vorhandenen Geschäfts sind ebenfalls in den entsprechenden Feldern verfügbar.

Der Algorithmus zur Erzeugung von Handelspositionen auf der Grundlage von Geschäften ist genau der gleiche wie beim Import des HTML-Berichts. Ein guter OOP-Stil erfordert es, gemeinsame Codeteile in einer Basisklasse zu implementieren und dann HTMLReportAdapter und den TesterReportAdapter von dieser zu erben.

Der gemeinsame Vorfahre von Berichten ist die Klasse BaseReportAdapter (Datei ReportCubeBase.mqh). Sie können diese Datei im Kontext mit der alten Klasse HTMLcube.mqh vergleichen, um sich selbst davon zu überzeugen, dass es nur sehr wenige Unterschiede gibt (mit Ausnahme der neuen Klassennamen). Auffallend ist vor allem der minimalistische Inhalt der Methode 'load'. Sie fungiert nun als virtueller Stumpf:

    virtual bool load(const string file)
    {
      reset();
      TradeRecord::reset();
      return false;
    }

Abgeleitete Methoden müssen diese Methode überschreiben.

Der Code in der Methode 'generate' hat sich ebenfalls geändert. Diese Methode wandelt Geschäfte in Positionen um. Nun wird zu Beginn dieser Methode ein virtuell leeres "Dummy" fillDealsArray aufgerufen.

    virtual bool fillDealsArray() = 0;
    
    int generate()
    {
      ...
      if(!fillDealsArray()) return 0;
      ...
    }

Ein Teil des bestehenden Codes für die Arbeit mit HTML-Berichten wurde in die neue virtuelle Methode in der Klasse HTMLReportAdapter verschoben. Bitte beachten Sie: die gesamte Klasse HTMLReportAdapter wird weiter unten besprochen. Der Hauptcodeteil befindet sich in der Basisklasse, daher ist es hier nur notwendig, 2 virtuelle Methoden zu definieren.

  template<typename T>
  class HTMLReportAdapter: public BaseReportAdapter<T>
  {
    protected:
      IndexMap *data;
      
      virtual bool fillDealsArray() override
      {
        for(int i = 0; i < data.getSize(); ++i)
        {
          IndexMap *row = data[i];
          if(CheckPointer(row) == POINTER_INVALID || row.getSize() != COLUMNS_COUNT) return false; // something is broken
          string s = row[COLUMN_SYMBOL].get<string>();
          StringTrimLeft(s);
          if(StringLen(s) > 0) // there is a symbol -> this is a deal
          {
            array << new Deal(row);
          }
          else if(row[COLUMN_TYPE].get<string>() == "balance")
          {
            string t = row[COLUMN_PROFIT].get<string>();
            StringReplace(t, " ", "");
            balance += StringToDouble(t);
          }
        }
        return true;
      }
    
    public:
      ~HTMLReportAdapter()
      {
        if(CheckPointer(data) == POINTER_DYNAMIC) delete data;
      }
      
      virtual bool load(const string file) override
      {
        BaseReportAdapter<T>::load(file);
        if(CheckPointer(data) == POINTER_DYNAMIC) delete data;
        data = NULL;
        if(StringFind(file, ".htm") > 0)
        {
          data = HTMLConverter::convertReport2Map(file, true);
          if(data != NULL)
          {
            size = generate();
            Print(data.getSize(), " deals transferred to ", size, " trades");
          }
        }
        return data != NULL;
      }
  };

Der Code beider Methoden ist aus der vorherigen Version bekannt, es wurde nichts geändert.

Schauen wir uns nun die Implementierung des neuen Adapters TesterReportAdapter an. Zunächst einmal musste ich die Klasse TesterDeal hinzufügen, die von der in ReportCubeBase.mqh definierten Klasse Deal abgeleitet ist (Deal ist eine alte Klasse, die sich zuvor in HTMLcube.mqh befand). TesterDeal hat einen Konstruktor mit dem Parameter TradeDeal, der ein Deal aus der Bibliothek SingleTesterCache ist. Außerdem definiert TesterDeal eine Reihe von Hilfsmethoden zur Umwandlung von Aufzählungen von Typ- und Deal-Richtung zu Zeichenketten.

  class TesterDeal: public Deal
  {
    public:
      TesterDeal(const TradeDeal &td)
      {
        time = (datetime)td.time_create + TimeShift;
        price = td.price_open;
        string t = dealType(td.action);
        type = t == "buy" ? +1 : (t == "sell" ? -1 : 0);
        t = dealDir(td.entry);
        direction = 0;
        if(StringFind(t, "in") > -1) ++direction;
        if(StringFind(t, "out") > -1) --direction;
        volume = (double)td.volume;
        profit = td.profit;
        deal = (long)td.deal;
        order = (long)td.order;
        comment = td.comment[];
        symbol = td.symbol[];
        commission = td.commission;
        swap = td.storage;
      }
      
      static string dealType(const ENUM_DEAL_TYPE type)
      {
        return type == DEAL_TYPE_BUY ? "buy" : (type == DEAL_TYPE_SELL ? "sell" : "balance");
      }
      
      static string dealDir(const ENUM_DEAL_ENTRY entry)
      {
        string result = "";
        if(entry == DEAL_ENTRY_IN) result += "in";
        else if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_OUT_BY) result += "out";
        else if(entry == DEAL_ENTRY_INOUT) result += "in out";
        return result;
      }
  };

Die Klasse TesterReportAdapter enthält die Methoden 'load' und fillDealsArray sowie einen Zeiger auf das SINGLETESTERCACHE-Objekt, das die Hauptklasse der Bibliothek SingleTesterCache ist. Dieses Objekt lädt auf Anforderung eine tst-Datei. Im Erfolgsfall füllt die Methode das Deals-Array, auf dessen Grundlage das fillDealsArray-Array arbeitet.

  template<typename T>
  class TesterReportAdapter: public BaseReportAdapter<T>
  {
    protected:
      SINGLETESTERCACHE *ptrSingleTesterCache;
      
      virtual bool fillDealsArray() override
      {
        for(int i = 0; i < ArraySize(ptrSingleTesterCache.Deals); i++)
        {
          if(TesterDeal::dealType(ptrSingleTesterCache.Deals[i].action) == "balance")
          {
            balance += ptrSingleTesterCache.Deals[i].profit;
          }
          else
          {
            array << new TesterDeal(ptrSingleTesterCache.Deals[i]);
          }
        }
        return true;
      }
      
    public:
      ~TesterReportAdapter()
      {
        if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache;
      }
      
      virtual bool load(const string file) override
      {
        if(StringFind(file, ".tst") > 0)
        {
          // default cleanup
          BaseReportAdapter<T>::load(file);
          
          // specific cleanup
          if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache;
          
          ptrSingleTesterCache = new SINGLETESTERCACHE();
          if(!ptrSingleTesterCache.Load(file))
          {
            delete ptrSingleTesterCache;
            ptrSingleTesterCache = NULL;
            return false;
          }
          size = generate();
          
          Print("Tester cache import: ", size, " trades from ", ArraySize(ptrSingleTesterCache.Deals), " deals");
        }
        return true;
      }
  };
  
  TesterReportAdapter<RECORD_CLASS> _defaultTSTReportAdapter;

Am Ende wird eine Standard-Adapterinstanz für den Vorlagentyp RECORD_CLASS erstellt. Unser Projekt umfasst die Datei OLAPTradesCustom.mqh, die die nutzerdefinierte Datensatzklasse CustomTradeRecord definiert. In dieser Datei wird die Klasse durch die Präprozessor-Direktive als das Makro RECORD_CLASS definiert. Sobald der neue Adapter mit dem Projekt verbunden ist und der Nutzer in den Eingaben eine tst-Datei angibt, beginnt der Adapter also mit der Generierung der Klassenobjekte von CustomTradeRecord, für die die nutzerdefinierten MFE- und MAE-Felder automatisch generiert werden.

Lassen Sie uns sehen, wie der neue Adapter seine Aufgaben erfüllt. Unten sehen Sie ein Beispiel für Gleichgewichtskurven durch Symbole aus einer tst-Datei.

Saldenkurven nach Symbolen

Saldenkurven nach Symbolen

Achten Sie darauf, dass die Linien ununterbrochen sind, was bedeutet, dass unsere Implementation CGraphicInPlot::LinesPlot korrekt funktioniert. Wenn Sie mit einem "progressiven" Aggregator (kumulativ) arbeiten, sollte der erste Selektor immer die Seriennummer (oder der Index) der Datensätze sein.

Tester-Optimierungsberichte als Anwendungsgebiet der OLAP-Analyse

Zusätzlich zu einzelnen Testdateien erlaubt MetaQuotes jetzt auch den Zugriff auf Opt-Dateien mit dem Optimierungs-Cache. Solche Dateien können mit Hilfe der Bibliothek TesterCache gelesen werden (wiederum erstellt von fxsaber). Auf der Grundlage dieser Bibliothek können wir leicht eine Anwendungsschicht für die OLAP-Analyse von Optimierungsergebnissen erstellen. Was wir dafür brauchen: eine Datensatzklasse mit Feldern, die die Daten jedes Optimierungsdurchlaufs speichern, einen Adapter und Selektoren (optional). Wir haben die Implementierungen der Komponenten für andere Anwendungsbereiche, so dass wir sie als Leitfaden (Plan) verwenden können. Außerdem werden wir eine grafische Schnittstelle hinzufügen (fast alles ist fertig, wir müssen nur noch die Einstellungen ändern).

Es wird die Datei OLAPOpts.mqh erstellt, deren Zweck ähnlich dem von OLAPTrades.mqh ist. Die Header-Datei TesterCache.mqh wird hinzugefügt.

  #include <fxsaber/TesterCache/TesterCache.mqh>

Definieren wir eine Enumeration mit allen Feldern des Optimierers. Ich habe Felder aus der ExpTradeSummary-Struktur verwendet (sie befindet sich in fxsaber/TesterCache/ExpTradeSummary.mqh, die Datei ist automatisch mit der Bibliothek verbunden).

  enum OPT_CACHE_RECORD_FIELDS
  {
    FIELD_NONE,
    FIELD_INDEX,
    FIELD_PASS,
  
    FIELD_DEPOSIT,
    FIELD_WITHDRAWAL,
    FIELD_PROFIT,
    FIELD_GROSS_PROFIT,
    FIELD_GROSS_LOSS,
    FIELD_MAX_TRADE_PROFIT,
    FIELD_MAX_TRADE_LOSS,
    FIELD_LONGEST_SERIAL_PROFIT,
    FIELD_MAX_SERIAL_PROFIT,
    FIELD_LONGEST_SERIAL_LOSS,
    FIELD_MAX_SERIAL_LOSS,
    FIELD_MIN_BALANCE,
    FIELD_MAX_DRAWDOWN,
    FIELD_MAX_DRAWDOWN_PCT,
    FIELD_REL_DRAWDOWN,
    FIELD_REL_DRAWDOWN_PCT,
    FIELD_MIN_EQUITY,
    FIELD_MAX_DRAWDOWN_EQ,
    FIELD_MAX_DRAWDOWN_PCT_EQ,
    FIELD_REL_DRAWDOWN_EQ,
    FIELD_REL_DRAWDOWN_PCT_EQ,
    FIELD_EXPECTED_PAYOFF,
    FIELD_PROFIT_FACTOR,
    FIELD_RECOVERY_FACTOR,
    FIELD_SHARPE_RATIO,
    FIELD_MARGIN_LEVEL,
    FIELD_CUSTOM_FITNESS,
  
    FIELD_DEALS,
    FIELD_TRADES,
    FIELD_PROFIT_TRADES,
    FIELD_LOSS_TRADES,
    FIELD_LONG_TRADES,
    FIELD_SHORT_TRADES,
    FIELD_WIN_LONG_TRADES,
    FIELD_WIN_SHORT_TRADES,
    FIELD_LONGEST_WIN_CHAIN,
    FIELD_MAX_PROFIT_CHAIN,
    FIELD_LONGEST_LOSS_CHAIN,
    FIELD_MAX_LOSS_CHAIN,
    FIELD_AVERAGE_SERIAL_WIN_TRADES,
    FIELD_AVERAGE_SERIAL_LOSS_TRADES
  };
  
  #define OPT_CACHE_RECORD_FIELDS_LAST (FIELD_AVERAGE_SERIAL_LOSS_TRADES + 1)

Die Struktur weist alle üblichen Variablen auf, wie z.B. Gewinn, Saldo, Drawdown, Anzahl der Handelsoperationen, Sharpe-Ratio, etc. Das einzige Feld, das wir hinzugefügt haben, ist FIELD_INDEX: Datensatz-Indizes. Die Felder in der Struktur haben verschiedene Typen: long, double, int. All dies wird zu OptCacheRecord, abgeleit von der Datensatzklasse Record, hinzugefügt und in ihrem Array vom Typ double gespeichert.

Auf die Bibliothek wird über die spezielle Struktur OptCacheRecordInternal zugegriffen:

  struct OptCacheRecordInternal
  {
    ExpTradeSummary summary;
    MqlParam params[][5]; // [][name, current, low, step, high]
  };

Jeder Testerdurchlauf ist nicht nur durch Leistungsvariablen gekennzeichnet, sondern auch mit einem bestimmten Satz von Eingabeparametern verknüpft. In dieser Struktur werden die Eingabeparameter als ein MqlParam-Array nach ExpTradeSummary hinzugefügt. Mit dieser Struktur in der Hand können Sie leicht die Klasse OptCacheRecord schreiben, die mit Daten im Optimiererformat gefüllt wird.

  class OptCacheRecord: public Record
  {
    protected:
      static int counter; // number of passes
      
      void fillByTesterPass(const OptCacheRecordInternal &internal)
      {
        const ExpTradeSummary record = internal.summary;
        set(FIELD_INDEX, counter++);
        set(FIELD_PASS, record.Pass);
        set(FIELD_DEPOSIT, record.initial_deposit);
        set(FIELD_WITHDRAWAL, record.withdrawal);
        set(FIELD_PROFIT, record.profit);
        set(FIELD_GROSS_PROFIT, record.grossprofit);
        set(FIELD_GROSS_LOSS, record.grossloss);
        set(FIELD_MAX_TRADE_PROFIT, record.maxprofit);
        set(FIELD_MAX_TRADE_LOSS, record.minprofit);
        set(FIELD_LONGEST_SERIAL_PROFIT, record.conprofitmax);
        set(FIELD_MAX_SERIAL_PROFIT, record.maxconprofit);
        set(FIELD_LONGEST_SERIAL_LOSS, record.conlossmax);
        set(FIELD_MAX_SERIAL_LOSS, record.maxconloss);
        set(FIELD_MIN_BALANCE, record.balance_min);
        set(FIELD_MAX_DRAWDOWN, record.maxdrawdown);
        set(FIELD_MAX_DRAWDOWN_PCT, record.drawdownpercent);
        set(FIELD_REL_DRAWDOWN, record.reldrawdown);
        set(FIELD_REL_DRAWDOWN_PCT, record.reldrawdownpercent);
        set(FIELD_MIN_EQUITY, record.equity_min);
        set(FIELD_MAX_DRAWDOWN_EQ, record.maxdrawdown_e);
        set(FIELD_MAX_DRAWDOWN_PCT_EQ, record.drawdownpercent_e);
        set(FIELD_REL_DRAWDOWN_EQ, record.reldrawdown_e);
        set(FIELD_REL_DRAWDOWN_PCT_EQ, record.reldrawdownpercnt_e);
        set(FIELD_EXPECTED_PAYOFF, record.expected_payoff);
        set(FIELD_PROFIT_FACTOR, record.profit_factor);
        set(FIELD_RECOVERY_FACTOR, record.recovery_factor);
        set(FIELD_SHARPE_RATIO, record.sharpe_ratio);
        set(FIELD_MARGIN_LEVEL, record.margin_level);
        set(FIELD_CUSTOM_FITNESS, record.custom_fitness);
      
        set(FIELD_DEALS, record.deals);
        set(FIELD_TRADES, record.trades);
        set(FIELD_PROFIT_TRADES, record.profittrades);
        set(FIELD_LOSS_TRADES, record.losstrades);
        set(FIELD_LONG_TRADES, record.longtrades);
        set(FIELD_SHORT_TRADES, record.shorttrades);
        set(FIELD_WIN_LONG_TRADES, record.winlongtrades);
        set(FIELD_WIN_SHORT_TRADES, record.winshorttrades);
        set(FIELD_LONGEST_WIN_CHAIN, record.conprofitmax_trades);
        set(FIELD_MAX_PROFIT_CHAIN, record.maxconprofit_trades);
        set(FIELD_LONGEST_LOSS_CHAIN, record.conlossmax_trades);
        set(FIELD_MAX_LOSS_CHAIN, record.maxconloss_trades);
        set(FIELD_AVERAGE_SERIAL_WIN_TRADES, record.avgconwinners);
        set(FIELD_AVERAGE_SERIAL_LOSS_TRADES, record.avgconloosers);
        
        const int n = ArrayRange(internal.params, 0);
        for(int i = 0; i < n; i++)
        {
          set(OPT_CACHE_RECORD_FIELDS_LAST + i, internal.params[i][PARAM_VALUE].double_value);
        }
      }
    
    public:
      OptCacheRecord(const int customFields = 0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields)
      {
      }
      
      OptCacheRecord(const OptCacheRecordInternal &record, const int customFields = 0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields)
      {
        fillByTesterPass(record);
      }
      
      static int getRecordCount()
      {
        return counter;
      }
  
      static void reset()
      {
        counter = 0;
      }
  };
  
  static int OptCacheRecord::counter = 0;

Die Methode fillByTesterPass zeigt deutlich die Übereinstimmung zwischen den Enumerationselementen und den Feldern von ExpTradeSummary. Der Konstruktor akzeptiert eine aufgefüllte Struktur OptCacheRecordInternal als Parameter.

Der Vermittler zwischen der Bibliothek TesterCache und OLAP ist ein spezialisierter Datenadapter. Der Adapter generiert den Datensatz OptCacheRecord.

  template<typename T>
  class OptCacheDataAdapter: public DataAdapter
  {
    private:
      int size;
      int cursor;
      int paramCount;
      string paramNames[];
      TESTERCACHE<ExpTradeSummary> Cache;

Das Feld 'size' — die Gesamtanzahl der Datensätze, cursor — die Nummer des aktuellen Datensatzes im Cache, paramCount — die Anzahl der Optimierungsparameter. Die Namen der Parameter werden im Array paramNames gespeichert. Die Variable Cache vom Typ TESTERCACHE<ExpTradeSummary> ist das Arbeitsobjekt der TesterCache-Bibliothek.

Zu Beginn wird der Optimierungs-Cache initialisiert und in den Reset-, Lade- und Anpassungsmethoden eingelesen.

      void customize()
      {
        size = (int)Cache.Header.passes_passed;
        paramCount = (int)Cache.Header.opt_params_total;
        const int n = ArraySize(Cache.Inputs);
  
        ArrayResize(paramNames, n);
        int k = 0;
        
        for(int i = 0; i < n; i++)
        {
          if(Cache.Inputs[i].flag)
          {
            paramNames[k++] = Cache.Inputs[i].name[];
          }
        }
        if(k > 0)
        {
          ArrayResize(paramNames, k);
          Print("Optimized Parameters (", paramCount, " of ", n, "):");
          ArrayPrint(paramNames);
        }
      }
  
    public:
      OptCacheDataAdapter()
      {
        reset();
      }
      
      void load(const string optName)
      {
        if(Cache.Load(optName))
        {
          customize();
          reset();
        }
        else
        {
          cursor = -1;
        }
      }
      
      virtual void reset() override
      {
        cursor = 0;
        if(Cache.Header.version == 0) return;
        T::reset();
      }
      
      virtual int getFieldCount() const override
      {
        return OPT_CACHE_RECORD_FIELDS_LAST;
      }

Die Opt-Datei wird in der Load-Methode geladen, in der die Methode Cache.Load der Bibliothek aufgerufen wird. Bei Erfolg werden die Expert Advisor-Parameter aus der Kopfzeile ausgewählt (in der Hilfsmethode 'customize'). Die Methode 'reset' setzt die aktuelle Datensatznummer zurück, die beim nächsten Iterieren aller Datensätze des OLAP-Kernels durch getNext inkrementiert wird. Hier wird die Struktur OptCacheRecordInternal mit Daten aus dem Optimierungs-Cache aufgefüllt. Auf ihrer Grundlage wird ein neuer Datensatz der Template-Parameterklasse (T) erstellt.

      virtual Record *getNext() override
      {
        if(cursor < size)
        {
          OptCacheRecordInternal internal;
          internal.summary = Cache[cursor];
          Cache.GetInputs(cursor, internal.params);
          cursor++;
          return new T(internal, paramCount);
        }
        return NULL;
      }
      ...
  };

Der Template-Parameter ist die oben erwähnte Klasse OptCacheRecord.

  #ifndef RECORD_CLASS
  #define RECORD_CLASS OptCacheRecord
  #endif
  
  OptCacheDataAdapter<RECORD_CLASS> _defaultOptCacheAdapter;

Es ist auch als Makro definiert, ähnlich wie RECORD_CLASS, das in anderen Teilen des OLAP-Kernels verwendet wird. Es folgt das Diagramm der Klassen mit allen unterstützten früheren und neuen Datenadaptern.

Das Diagramm der Datenadapterklassen

Das Diagramm der Datenadapterklassen

Jetzt müssen wir entscheiden, welche Selektortypen für die Analyse von Optimierungsergebnissen nützlich sein können. Die folgende Enumeration wird als erste minimale Option vorgeschlagen.

  enum OPT_CACHE_SELECTORS
  {
    SELECTOR_NONE,       // none
    SELECTOR_INDEX,      // ordinal number
    /* all the next require a field as parameter */
    SELECTOR_SCALAR,     // scalar(field)
    SELECTOR_QUANTS,     // quants(field)
    SELECTOR_FILTER      // filter(field)
  };

Alle Datensatzfelder gehören zu einem der beiden Typen: Handelsstatistiken und EA-Parameter. Eine bequeme Lösung besteht darin, die Parameter in Zellen zu organisieren, die genau den getesteten Werten entsprechen. Wenn Parameter beispielsweise eine MA-Periode enthalten, für die 10 Werte verwendet wurden, muss der OLAP-Würfel 10 Zellen für diesen Parameter haben. Dies wird durch einen Quantisierungsselektor (SELECTOR_QUANTS) mit einer "Korb"-Größe von Null erreicht.

Bei variablen Feldern sollten die Zellen besser in einem bestimmten Schritt gesetzt werden. Beispielsweise können Sie die Verteilung der Durchläufe nach Gewinn mit einem Schritt von 100 Einheiten anzeigen. Auch dies kann durch den Quantisierungsselektor erfolgen. Allerdings muss die "Korb"-Größe auf den erforderlichen Schritt eingestellt werden. Andere hinzugefügte Selektoren führen andere Dienstleistungsfunktionen aus. Zum Beispiel wird SELECTOR_INDEX zur Berechnung der kumulativen Gesamtsumme verwendet. SELECTOR_SCALAR ermöglicht es, eine Zahl als Merkmal der gesamten Auswahl zu erhalten.

Die Selektorklassen sind bereit und befinden sich in der Datei OLAPCommon.mqh.

Schreiben wir für diese Selektortypen die Methode createSelector in der Spezialisierung der Vorlage der Klasse OLAPEngine:

  class OLAPEngineOptCache: public OLAPEngine<OPT_CACHE_SELECTORS,OPT_CACHE_RECORD_FIELDS>
  {
    protected:
      virtual Selector<OPT_CACHE_RECORD_FIELDS> *createSelector(const OPT_CACHE_SELECTORS selector, const OPT_CACHE_RECORD_FIELDS field) override
      {
        const int standard = adapter.getFieldCount();
        switch(selector)
        {
          case SELECTOR_INDEX:
            return new SerialNumberSelector<OPT_CACHE_RECORD_FIELDS,OptCacheRecord>(FIELD_INDEX);
          case SELECTOR_SCALAR:
            return new OptCacheSelector(field);
          case SELECTOR_QUANTS:
            return field != FIELD_NONE ? new QuantizationSelector<OPT_CACHE_RECORD_FIELDS>(field, (int)field < standard ? quantGranularity : 0) : NULL;
        }
        return NULL;
      }
  
    public:
      OLAPEngineOptCache(): OLAPEngine() {}
      OLAPEngineOptCache(DataAdapter *ptr): OLAPEngine(ptr) {}
  };
  
  OLAPEngineOptCache _defaultEngine;

Wenn Sie einen Quantisierungsselektor erstellen, setzen Sie die Korbgröße auf die Variable quantGranularity oder auf Null, je nachdem, ob das Feld "Standard" (speichert die Standard-Testerstatistik) oder nutzerdefiniert (Parameter Expert Advisor) ist. Das Feld quantGranularity wird in der Basisklasse OLAPEngine beschrieben. Es kann im Konstruktor der Engine oder später mit der Methode setQuant gesetzt werden.

OptCacheSelector ist eine einfache Hülle für BaseSelector<OPT_CACHE_RECORD_FIELDS>.

Grafische Schnittstelle zur Analyse von Tester-Optimierungsberichten

Die Analyse der Optimierungsergebnisse wird mit der gleichen Oberfläche visualisiert, die auch für Handelsberichte verwendet wurde. Wir können die Datei OLAPGUI_Trade.mqh tatsächlich unter einem neuen Namen OLAPGUI_Opts.mqh kopieren und kleinere Anpassungen vornehmen. Die Anpassungen betreffen die virtuellen Methoden 'setup' und 'process'.

  template<typename S, typename F>
  void OLAPDialog::setup() override
  {
    static const string _settings[ALGO_NUMBER][MAX_ALGO_CHOICES] =
    {
      // enum AGGREGATORS 1:1, default - sum
      {"sum", "average", "max", "min", "count", "profit factor", "progressive total", "identity", "variance"},
      // enum RECORD_FIELDS 1:1, default - profit amount
      {""},
      // enum SORT_BY, default - none
      {"none", "value ascending", "value descending", "label ascending", "label descending"},
      // enum ENUM_CURVE_TYPE partially, default - points
      {"points", "lines", "points/lines", "steps", "histogram"}
    };
    
    static const int _defaults[ALGO_NUMBER] = {0, FIELD_PROFIT, 0, 0};
  
    const int std = EnumSize<F,PackedEnum>(0);
    const int fields = std + customFieldCount;
  
    ArrayResize(settings, fields);
    ArrayResize(selectors, fields);
    selectors[0] = "(<selector>/field)"; // none
    selectors[1] = "<serial number>"; // the only selector, which can be chosen explicitly, it corresponds to the 'index' field
  
    for(int i = 0; i < ALGO_NUMBER; i++)
    {
      if(i == 1) // pure fields
      {
        for(int j = 0; j < fields; j++)
        {
          settings[j][i] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std];
        }
      }
      else
      {
        for(int j = 0; j < MAX_ALGO_CHOICES; j++)
        {
          settings[j][i] = _settings[i][j];
        }
      }
    }
  
    for(int j = 2; j < fields; j++) // 0-th is none
    {
      selectors[j] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std];
    }
    
    ArrayCopy(defaults, _defaults);
  }

Es gibt fast keinen Unterschied zwischen Feldern und Selektoren, da jedes Feld einen Quantisierungsselektor für dasselbe Feld impliziert. Mit anderen Worten, der Quantisierungsselektor ist für alles verantwortlich. In früheren Projekten, die sich auf Berichte und Kurse bezogen, haben wir spezielle Selektoren für separate Felder verwendet (z.B. Rentabilitätsselektor, Wochentagsselektor, Selektor für den Kerzenleuchtertyp und andere).

Die Namen aller Elemente von Dropdown-Listen mit Feldern (die auch als Selektoren für die X-, Y- und Z-Achse fungieren) werden aus den Namen der Enumerationselemente OPT_CACHE_RECORD_FIELDS und aus dem Array customFields für die EA-Parameter gebildet. Zuvor haben wir die Methode setCustomFields in der Basisklasse OLAPDialogBase betrachtet, die das Array customFields mit den Namen aus dem Adapter füllt. Diese beiden Methoden können im Code des analytischen EA OLAPGUI_Opts.mq5 miteinander verknüpft werden (siehe unten).

Standardfelder werden in der Reihenfolge der Enumerationselemente angezeigt. Auf Standardfelder folgen nutzerdefinierte Felder, die sich auf die Parameter des zu optimierenden EA beziehen. Die Reihenfolge der nutzerdefinierten Felder entspricht der Reihenfolge der Parameter in der Opt-Datei.

Das Lesen der Kontrollzustände und der Start des Analyseprozesses werden nach der Methode 'process' durchgeführt.

  template<typename S, typename F>
  int OLAPDialog::process() override
  {
    SELECTORS Selectors[4];
    ENUM_FIELDS Fields[4];
    AGGREGATORS at = (AGGREGATORS)m_algo[0].Value();
    ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[1].Value();
    SORT_BY sb = (SORT_BY)m_algo[2].Value();
    
    if(at == AGGREGATOR_IDENTITY)
    {
      Print("Sorting is disabled for Identity");
      sb = SORT_BY_NONE;
    }
  
    ArrayInitialize(Selectors, SELECTOR_NONE);
    ArrayInitialize(Fields, FIELD_NONE);
  
    int matches[2] =
    {
      SELECTOR_NONE,
      SELECTOR_INDEX
    };
    
    for(int i = 0; i < AXES_NUMBER; i++)
    {
      if(!m_axis[i].IsVisible()) continue;
      int v = (int)m_axis[i].Value();
      if(v < 2) // selectors (which is specialized for a field already)
      {
        Selectors[i] = (SELECTORS)matches[v];
      }
      else // pure fields
      {
        Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS;
        Fields[i] = (ENUM_FIELDS)(v);
      }
    }
    
    m_plot.CurvesRemoveAll();
  
    if(at == AGGREGATOR_IDENTITY) af = FIELD_NONE;
  
    m_plot.InitXAxis(at != AGGREGATOR_PROGRESSIVE ? new AxisCustomizer(m_plot.getGraphic(), false) : NULL);
    m_plot.InitYAxis(at == AGGREGATOR_IDENTITY ? new AxisCustomizer(m_plot.getGraphic(), true) : NULL);
  
    m_button_ok.Text("Processing...");
    return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb);
  }

OLAP-Analyse und Visualisierung von Optimierungsberichten

Der MetaTrader Tester bietet verschiedene Möglichkeiten, Optimierungsergebnisse zu testen, die jedoch auf den Standardsatz beschränkt sind. Der verfügbare Satz kann durch die Verwendung der erstellten OLAP-Engine erweitert werden. Zum Beispiel zeigt die eingebaute 2D-Visualisierung immer den maximalen Gewinnwert für eine Kombination von zwei EA-Parametern an, jedoch gibt es normalerweise mehr als zwei Parameter. An jedem Punkt auf der Oberfläche sehen wir Ergebnisse für verschiedene Kombinationen anderer Parameter, die nicht auf der Achse angezeigt werden. Dies kann zu einer allzu optimistischen Einschätzung der Rentabilität bestimmter Werte der angezeigten Parameter führen. Eine ausgewogenere Bewertung könnte aus dem durchschnittlichen Gewinnwert und aus der Spanne seiner Werte gewonnen werden. Diese Bewertung kann, neben anderen Bewertungen, mit OLAP durchgeführt werden.

Die OLAP-Analyse von Optimierungsberichten wird durch den neuen nicht-handelnden Expertenberater OLAPGUI_Opts.mq5 durchgeführt. Seine Struktur ist vollständig identisch mit OLAPGUI.mq5. Darüber hinaus ist sie einfacher, da es nicht notwendig ist, Adapter je nach dem angegebenen Dateityp anzuschließen. Es handelt sich immer um eine Opt-Datei für Optimierungsergebnisse.

Geben Sie den Dateinamen in Eingaben und einen Quantisierungsschritt für statistische Parameter an.

  input string OptFileName = "Integrity.opt";
  input uint QuantGranularity = 0;

Bitte beachten Sie, dass es wünschenswert ist, für jedes Feld einen separaten Quantisierungsschritt zu haben. Jetzt setzen wir ihn jedoch nur einmal, während der Wert von der GUI aus nicht geändert wird. Dieser Fehler bietet einen potenziellen Bereich für weitere Verbesserungen. Denken Sie daran, dass der Schrittwert für ein Feld geeignet sein kann und für ein anderes nicht geeignet ist (er kann zu groß oder zu klein sein). Rufen Sie daher ggf. den EA-Eigenschaften-Dialog auf, um den Quantenwert zu ändern, bevor Sie das Feld aus der Dropdown-Liste in der OLAP-Schnittstelle auswählen.

Nachdem Sie Header-Dateien mit allen Klassen eingebunden haben, erstellen Sie eine Dialoginstanz und binden Sie diese an die OLAP-Engine.

  #include <OLAP/OLAPOpts.mqh>
  #include <OLAP/GUI/OLAPGUI_Opts.mqh>
  
  OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine);

Verbinden Sie im OnInit() den neuen Adapter mit der Engine und initiieren Sie das Laden von Daten aus der Datei.

  int OnInit()
  {
    _defaultEngine.setAdapter(&_defaultOptCacheAdapter);
    _defaultEngine.setShortTitles(true);
    _defaultEngine.setQuant(QuantGranularity);
    _defaultOptCacheAdapter.load(OptFileName);
    dialog.setCustomFields(_defaultOptCacheAdapter);
  
    if(!dialog.Create(0, "OLAPGUI" + (OptFileName != "" ? " : " + OptFileName : ""), 0,  0, 0, 750, 560)) return INIT_FAILED;
    if(!dialog.Run()) return INIT_FAILED;
    
    return INIT_SUCCEEDED;
  }

Versuchen wir, einige analytische Abschnitte für die Datei Integrity.opt mit QuantGranularity = 100 zu erstellen. Die folgenden drei Parameter wurden während der Optimierung ausgewählt: PreisPeriode, Momentum, Sigma.

Der untere Screenshot zeigt den Gewinn aufgeschlüsselt nach PricePeriod-Werten.

Durchschnittlicher Gewinn in Abhängigkeit vom EA-Parameterwert

Durchschnittlicher Gewinn in Abhängigkeit vom EA-Parameterwert

Das Ergebnis liefert wenig Informationen ohne Streuung.

Gewinnstreuung in Abhängigkeit vom EA-Parameterwert

Gewinnstreuung in Abhängigkeit vom EA-Parameterwert

Durch den Vergleich dieser beiden Histogramme können wir abschätzen, bei welchen Parameterwerten die Streuung den Mittelwert nicht überschreitet, was den Breakeven bedeutet. Eine bessere Lösung ist es, den Vergleich automatisch auf demselben Diagramm durchzuführen. Aber dies würde den Rahmen dieses Artikels sprengen.

Lassen Sie uns alternativ die Rentabilität für diesen Parameter betrachten (Gewinn-/Verlust-Verhältnis für alle Durchläufe).

Strategie-Profit-Faktor in Abhängigkeit vom Wert des EA-Parameters

Strategie-Profit-Faktor in Abhängigkeit vom Wert des EA-Parameters

Eine andere, heikle Methode zur Bewertung ist die Bewertung der durchschnittlichen Periodengröße, aufgeschlüsselt nach Gewinnstufen, in Schritten von 100 (der Schritt wird im Eingabeparameter QuantGranularity festgelegt).

Der durchschnittliche Wert des Parameters für die Gewinnerzeugung in verschiedenen Bereichen (in Schritten von 100 Einheiten)

Der durchschnittliche Wert des Parameters für die Gewinnerzeugung in verschiedenen Bereichen (in Schritten von 100 Einheiten)

Die folgende Abbildung zeigt die Verteilung der Gewinne in Abhängigkeit von der Periode (alle Durchläufe werden durch die Verwendung des Aggregators 'identity' dargestellt).

Profit vs. parameter value für alle Positionen

Profit vs. parameter value für alle Positionen

Die Aufschlüsselung des Gewinns nach Momentum und Sigma sieht wie folgt aus.

Durchschnittlicher Gewinn nach zwei Parametern

Durchschnittlicher Gewinn nach zwei Parametern

Um die allgemeine Verteilung der Gewinne nach Ebenen in Schritten von 100 anzuzeigen, wählen Sie das Feld 'profit' aus der Statistik entlang der X-Achse und den Aggregator 'count'.

Verteilung der Gewinne nach Bereichen in Schritten von 100 Einheiten

Verteilung aller Gewinne nach Bereichen in Schritten von 100 Einheiten

Mit Hilfe des Aggregators 'identity' können wir den Einfluss der Anzahl der gewinnbringenden Geschäfte bewerten. Im Allgemeinen ermöglicht dieser Aggregator die visuelle Bewertung vieler anderer Abhängigkeiten.

Profit vs. number of trades

Profit vs. number of trades

Schlussfolgerung

In diesem Artikel haben wir den Anwendungsbereich von MQL OLAP erweitert. Jetzt kann es zur Analyse von Testberichten aus einzelnen Durchläufen und Optimierungen verwendet werden. Die aktualisierte Struktur der Klassen ermöglicht eine weitere Erweiterung der OLAP-Fähigkeiten. Die vorgeschlagene Implementierung ist nicht ideal und kann stark verbessert werden (insbesondere im Hinblick auf die 3D-Visualisierung, die Implementierung von Filtereinstellungen und die Quantisierung auf verschiedenen Achsen in der interaktiven GUI). Nichtsdestotrotz dient sie als eine minimale Ausgangsbasis, die das Kennenlernen der OLAP-Welt erleichtert. Die OLAP-Analyse ermöglicht es Händlern, große Mengen an Rohdaten zu verarbeiten und neue Erkenntnisse für die weitere Entscheidungsfindung zu gewinnen.

Beigefügte Dateien:

Expert Advisor

  • OLAPRPRT.mq5 — Expert Advisor für die Analyse der Kontenhistorie sowie von HTML- und CSV-Berichten (aktualisierte Datei aus Artikel 3, ohne GUI)
  • OLAPQTS.mq5 — Expert Advisor für die Analyse von Kursofferten (aktualisierte Datei aus Artikel 3, ohne GUI)
  • OLAPGUI.mq5 — Expert Advisor für die Analyse der Kontenhistorie, Berichte im HTML- und CSV-Format sowie TST-Standard-Testerdateien (aktualisierte Datei aus Artikel 2, ohne GUI)
  • OLAPGUI_Opts.mq5 — Expert Advisor für die Analyse von Optimierungsergebnissen aus Standard-OPT-Testerdateien (neu, GUI)

Include

Kernel

  • OLAP/OLAPCommon.mqh — die Haupt-Header-Datei mit OLAP-Klassen
  • OLAP/OLAPTrades.mqh — Standardklassen für die OLAP-Analyse der Handelsgeschichte
  • OLAP/OLAPTradesCustom.mqh — nutzerdefinierte Klassen für die OLAP-Analyse der Handelsgeschichte
  • OLAP/OLAPQuotes.mqh — Klassen für die OLAP-Analyse der Kurse
  • OLAP/OLAPOpts.mqh — Klassen für die OLAP-Analyse der Optimierungsergebnisse des Expert Advisors
  • OLAP/ReportCubeBase.mqh — Basisklassen für die OLAP-Analyse der Handelsgeschichte
  • OLAP/HTMLcube.mqh — Klassen für die OLAP-Analyse der Handelsgeschichte im HTML-Format
  • OLAP/CSVcube.mqh — Klassen für die OLAP-Analyse der Handelsgeschichte im CSV-Format
  • OLAP/TSTcube.mqh — Klassen für die OLAP-Analyse der Handelsgeschichte im TST-Format
  • OLAP/PairArray.mqh — eine Klasse des Arrays von Paaren [Wert;Name], die alle Sortiertypen unterstützt
  • OLAP/GroupReportInputs.mqh — eine Gruppe von Eingabeparametern für die Analyse von Handelsberichten
  • MT4Bridge/MT4Orders.mqh — MT4Orders-Bibliothek für die Arbeit mit Aufträgen im einheitlichen Stil für MetaTrader 4 und für MetaTrader 5
  • MT4Bridge/MT4Time.mqh — eine zusätzliche Header-Datei, die Datenverarbeitungsfunktionen im Stil von MetaTrader 4 implementiert
  • Marketeer/IndexMap.mqh — eine zusätzliche Header-Datei, die ein Array mit einem kombinierten Zugriff auf Schlüssel- und Indexbasis implementiert
  • Marketeer/Converter.mqh — eine zusätzliche Header-Datei zur Konvertierung von Datentypen
  • Marketeer/GroupSettings.mqh — eine zusätzliche Header-Datei, die Gruppeneinstellungen von Eingabeparametern enthält
  • Marketeer/WebDataExtractor.mqh — HTML-Parser
  • Marketeer/empty_strings.h — Liste der leeren HTML-Tags
  • Marketeer/HTMLcolumns.mqh — Definition von Spaltenindizes in HTML-Berichten
  • Marketeer/RubbArray.mqh — eine zusätzliche Header-Datei mit dem "Gummi"-Array
  • Marketeer/CSVReader.mqh — CSV-Parser
  • Marketeer/CSVcolumns.mqh — Definition von Spaltenindizes in CSV-Berichten

Grafische Nutzeroberfläche

  • OLAP/GUI/OLAPGUI.mqh — allgemeine Implementierung der Schnittstelle des interaktiven Fensters
  • OLAP/GUI/OLAPGUI_Trades.mqh — Spezialisierungen der grafischen Oberfläche für die Analyse von Handelsberichten
  • OLAP/GUI/OLAPGUI_Opts.mqh — Spezialisierungen der grafischen Oberfläche für die Analyse von Optimierungsergebnissen
  • Layouts/Box.mqh — Container mit Steuerelementen
  • Layouts/ComboBoxResizable.mqh — das Dropdown-Steuerelement mit der Möglichkeit der dynamischen Größenanpassung
  • Layouts/MaximizableAppDialog.mqh — das Dialogfenster, mit der Möglichkeit der dynamischen Größenänderung
  • PairPlot/Plot.mqh — ein Steuerelement mit Diagrammgrafiken, mit der Unterstützung für dynamische Größenänderung
  • Layouts/res/expand2.bmp — Schaltfläche zum Maximieren des Fensters
  • Layouts/res/size6.bmp — Schaltfläche zum Ändern der Größe
  • Layouts/res/size10.bmp — Schaltfläche zum Ändern der Größe

TypeToBytes

  • TypeToBytes.mqh

SingleTesterCache

  • fxsaber/SingleTesterCache/SingleTesterCache.mqh
  • fxsaber/SingleTesterCache/SingleTestCacheHeader.mqh
  • fxsaber/SingleTesterCache/String.mqh
  • fxsaber/SingleTesterCache/ExpTradeSummaryExt.mqh
  • fxsaber/SingleTesterCache/ExpTradeSummarySingle.mqh
  • fxsaber/SingleTesterCache/TradeDeal.mqh
  • fxsaber/SingleTesterCache/TradeOrder.mqh
  • fxsaber/SingleTesterCache/TesterPositionProfit.mqh
  • fxsaber/SingleTesterCache/TesterTradeState.mqh

TesterCache

  • fxsaber/TesterCache/TesterCache.mqh
  • fxsaber/TesterCache/TestCacheHeader.mqh
  • fxsaber/TesterCache/String.mqh
  • fxsaber/TesterCache/ExpTradeSummary.mqh
  • fxsaber/TesterCache/TestCacheInput.mqh
  • fxsaber/TesterCache/TestInputRange.mqh
  • fxsaber/TesterCache/Mathematics.mqh
  • fxsaber/TesterCache/TestCacheRecord.mqh
  • fxsaber/TesterCache/TestCacheSymbolRecord.mqh

Patch der Standardbibliothek

  • Controls/Dialog.mqh
  • Controls/ComboBox.mqh

Dateien

  • 518562.history.csv
  • Integrity.tst
  • Integrity.opt

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/7656

Beigefügte Dateien |
MQLOLAP4.zip (365.7 KB)
Zeitreihen in der Bibliothek DoEasy (Teil 37): Kollektion von Zeitreihen - Datenbank der Zeitreihen nach Symbolen und Zeitrahmen Zeitreihen in der Bibliothek DoEasy (Teil 37): Kollektion von Zeitreihen - Datenbank der Zeitreihen nach Symbolen und Zeitrahmen
Der Artikel befasst sich mit der Entwicklung der Zeitreihenkollektion spezifizierter Zeitrahmen für alle im Programm verwendeten Symbole. Wir werden die Zeitreihenkollektion, die Methoden zur Parametereinstellung der Zeitreihenkollektion und das anfängliche Ausfüllen der entwickelten Zeitreihen mit historischen Daten erstellen.
Kontinuierliche Walk-Forward-Optimierung (Teil 5): Projektübersicht Auto-Optimizer und Erstellen einer GUI Kontinuierliche Walk-Forward-Optimierung (Teil 5): Projektübersicht Auto-Optimizer und Erstellen einer GUI
Dieser Artikel bietet eine weitere Beschreibung der Walk-Forward-Optimierung im MetaTrader 5-Terminal. In früheren Artikeln betrachteten wir Methoden zur Erstellung und Filterung des Optimierungsberichts und begannen mit der Analyse der internen Struktur der für den Optimierungsprozess verantwortlichen Anwendung. Der Auto-Optimizer ist als C#-Anwendung implementiert und verfügt über eine eigene grafische Oberfläche. Der fünfte Artikel ist der Erstellung dieser grafischen Oberfläche gewidmet.
Die Handelssignale mehrerer Währungen überwachen (Teil 3): Einführung von Suchalgorithmen Die Handelssignale mehrerer Währungen überwachen (Teil 3): Einführung von Suchalgorithmen
Im vorherigen Artikel haben wir den visuellen Teil der Anwendung sowie die grundlegende Interaktion der GUI-Elementen entwickelt. Diesmal fügen wir die interne Logik und den Algorithmus der Vorbereitung der Handelssignaldaten hinzu, sowie die Fähigkeit, Signale einzurichten, sie zu durchsuchen und auf dem Monitor zu visualisieren.
Prognose von Zeitreihen (Teil 2): Least-Square Support-Vector Machine (LS-SVM) Prognose von Zeitreihen (Teil 2): Least-Square Support-Vector Machine (LS-SVM)
Dieser Artikel befasst sich mit der Theorie und der praktischen Anwendung des Algorithmus zur Vorhersage von Zeitreihen, basierend auf der Support-Vektor-Methode. Er schlägt auch seine Implementierung in MQL vor und stellt Testindikatoren und Expert Advisor zur Verfügung. Diese Technologie ist noch nicht in MQL implementiert worden. Aber zuerst müssen wir uns mit der Mathematik dafür vertraut machen.