Anwendung von OLAP im Handel (Teil 1): Online-Analyse multidimensionaler Daten

1 Juli 2019, 09:04
Stanislav Korotky
0
217

Händler müssen oft riesige Datenmengen analysieren. Dazu gehören oft Zahlen, Kurse, Indikatorwerte und Handelsberichte. Aufgrund der Vielzahl von Parametern und Bedingungen, von denen diese Zahlen abhängen, betrachten wir sie in Teilen und betrachten den gesamten Prozess aus verschiedenen Perspektiven. Die gesamte Informationsmenge bildet eine Art virtuellen Hyperwürfel, in dem jeder Parameter seine eigene Dimension definiert, die senkrecht zum Rest steht. Solche Hyperwürfel können mit der gängigen OLAP ( Online Analytical Processing) Technologie verarbeitet und analysiert werden.

Das Wort "online" im Annäherungsnamen bezieht sich nicht auf das Internet, sondern bedeutet Schnelligkeit der Ergebnisse. Das Funktionsprinzip impliziert die Vorberechnung der Zellen des Hyperwürfels, woraufhin Sie schnell jeden beliebigen Querschnitt des Würfels in visueller Form extrahieren und betrachten können. Dies ist vergleichbar mit dem Optimierungsprozess in MetaTrader: Der Tester berechnet zunächst Handelsvarianten (die sehr lange dauern können, d.h. nicht zeitnah sind) und gibt dann einen Bericht aus, der die Ergebnisse in Verbindung mit den Eingabeparametern darstellt. Ab Build 1860 unterstützt die MetaTrader 5-Plattform dynamische Änderungen der betrachteten Optimierungsergebnisse durch Umschalten verschiedener Optimierungskriterien. Das ist nahe an der OLAP-Idee. Aber für eine vollständige Analyse benötigen wir die Möglichkeit, viele andere Bereiche des Hyperwürfels auszuwählen.

Wir werden versuchen, den OLAP-Ansatz im MetaTrader anzuwenden und die multidimensionale Analyse mit MQL-Tools zu implementieren. Bevor wir mit der Implementierung fortfahren, müssen wir die zu analysierenden Daten ermitteln. Dazu können Handelsberichte, Optimierungsergebnisse oder Indikatorwerte gehören. Die Auswahl an dieser Stelle ist nicht ganz so wichtig, da wir bestrebt sind, eine universelle objektorientierte Engine zu entwickeln, die auf alle Daten anwendbar ist. Aber wir müssen die Engine auf bestimmte Ergebnisse anwenden. Eine der gängigsten Aufgaben ist die Analyse des Handelsberichts. Wir werden diese Aufgabe berücksichtigen.

Innerhalb eines Handelsberichts kann eine Aufschlüsselung des Gewinns nach Symbolen, Wochentagen, Kauf- und Verkaufsvorgängen sinnvoll sein. Eine weitere Möglichkeit ist der Vergleich der Performance-Ergebnisse verschiedener Handelsroboter (d.h. getrennt für jede Magicnummer). Die nächste logische Frage ist, ob es möglich ist, verschiedene Dimensionen zu kombinieren: Symbole nach Wochentagen in Bezug auf Expert Advisors oder eine andere Gruppierung. All dies kann mit OLAP erreicht werden.

Architektur

Nach dem objektorientierten Ansatz sollte eine große Aufgabe in einfache logisch zusammenhängende Teile zerlegt werden, wobei jeder Teil seine eigene Rolle auf der Grundlage eingehender Daten, des internen Zustands und einiger Regelwerke übernimmt.

Die erste Klasse, die wir verwenden werden, ist ein Datensatz mit Quelldaten — 'Record'. Ein solcher Datensatz kann Daten speichern, die sich auf einen Handelsvorgang oder einen Optimierungspass usw. beziehen.

Ein 'Record' ist ein Vektor mit einer beliebigen Anzahl von Feldern. Da es sich um eine abstrakte Einheit handelt, ist die Bedeutung der einzelnen Felder nicht von Bedeutung. Für jede spezifische Anwendung erstellen wir eine abgeleitete Klasse, die den Zweck der Felder "kennt" und entsprechend verarbeitet.

Eine weitere Klasse 'DataAdapter' wird benötigt, um Datensätze aus einer abstrakten Quelle zu lesen (z.B. die Historie eines Handelskontos, eine CSV-Datei, ein HTML-Bericht oder Daten, die über WebRequest im Web erhalten wurden). Zu diesem Zeitpunkt erfüllt sie nur eine Funktion: Sie iteriert durch die Datensätze und ermöglicht den Zugriff darauf. Später werden wir in der Lage sein, abgeleitete Klassen für jede reale Anwendung zu erstellen. Diese Klassen füllen Arrays mit Datensätzen relevanter Quellen aus.

Alle Datensätze können irgendwie in den Zellen des Hyperwürfels angezeigt werden. Wir wissen zum jetzigen Zeitpunkt nicht, wie wir das machen sollen, aber das ist die Idee des Projekts: Eingabewerte aus den Datensatzfeldern auf die Würfelzellen zu verteilen und für sie die generalisierte Statistik mit den ausgewählten Aggregatfunktionen zu berechnen.

Die Ebene des Basiswürfels stellt nur die wichtigsten Eigenschaften wie die Anzahl der Dimensionen, deren Namen und die Größe jeder Dimension zur Verfügung. Diese Daten werden in der Klasse MetaCube bereitgestellt.

Abgeleitete Klassen füllen dann relevante Statistiken zu diesen Zellen aus. Die häufigsten Beispiele für spezifische Aggregatoren sind die Summe aller Werte oder der Durchschnittswert des gleichen Feldes für alle Datensätze. Es wird jedoch viel mehr unterschiedliche Arten von Aggregatoren geben.

Um die Aggregation von Werten in den Zellen zu ermöglichen, muss jeder Datensatz den Satz von Indizes erhalten, die ihn eindeutig in eine bestimmte Zelle des Würfels abbilden. Diese Aufgabe wird von der speziellen Klasse 'Selector' übernommen. Der Selektor entspricht einer Seite (Achse, Koordinate) des Hyperwürfels.

Die abstrakte Basisklasse Selector bietet eine Programmierschnittstelle, um einen Satz gültiger Werte zu definieren und jeden Eintrag in einen dieser Werte abzubilden. Wenn der Zweck beispielsweise darin besteht, Datensätze durch Wochentage zu teilen, dann sollte die abgeleitete Selector-Klasse die Nummer des Wochentages von 0 bis 6 zurückgeben. Die Anzahl der zulässigen Werte für einen bestimmten Selektor definiert die Größe dieser Würfeldimension. Dies ist für den Wochentag eindeutig, nämlich 7.

Darüber hinaus ist es manchmal sinnvoll, einige der Datensätze zu filtern (um sie von der Analyse auszuschließen). Deshalb benötigen wir eine Filterklasse. Es ist ähnlich wie der Selektor, setzt aber zusätzliche Einschränkungen für die zulässigen Werte. So können wir beispielsweise einen Filter erstellen, der auf dem Selektor der Wochentage basiert. In diesem Filter ist es möglich, die Tage anzugeben, die von der Berechnung ausgeschlossen oder in die Berechnung einbezogen werden sollen.

Nach dem Erstellen des Würfels (d.h. der Berechnung der Aggregatfunktionen für alle Zellen) kann das Ergebnis visualisiert und analysiert werden. Zu diesem Zweck reservieren wir die spezielle Klasse 'Display'.

Um alle oben genannten Klassen zu einer ganzen Einheit zusammenzufassen, lassen Sie uns eine Art Kontrollzentrum, die Klasse Analyst, schaffen.

Dies sieht in der UML-Notation wie folgt aus (dies kann als Aktionsplan betrachtet werden, der in jeder Entwicklungsphase überprüft werden kann).

Die online Verarbeitung der Analyse im MetaTrader

Die online Verarbeitung der Analyse im MetaTrader

Einige der Klassen wurden hier weggelassen. Es spiegelt jedoch die allgemeine Grundlage der Konstruktion des Hyperwürfels wider und zeigt die Aggregatfunktionen, die für Berechnungen in der Zellen des Hyperwürfels zur Verfügung stehen.

Implementierung der Basisklasse

Nun kommen wir zur Implementierung der oben beschriebenen Klassen. Beginnen wir mit der Klasse Record.

  class Record
  {
    private:
      double data[];
      
    public:
      Record(const int length)
      {
        ArrayResize(data, length);
        ArrayInitialize(data, 0);
      }
      
      void set(const int index, double value)
      {
        data[index] = value;
      }
      
      double get(const int index) const
      {
        return data[index];
      }
  };

Er speichert einfach beliebige Werte im Array (Vektor) 'data'. Die Vektorlänge wird im Konstruktor festgelegt.

Datensätze aus verschiedenen Quellen werden mit dem DataAdapter gelesen.

  class DataAdapter
  {
    public:
      virtual Record *getNext() = 0;
      virtual int reservedSize() = 0;
  };

Die Methode getNext muss in einer Schleife aufgerufen werden, bis sie NULL zurückgibt (was bedeutet, dass es keine Datensätze mehr gibt). Alle empfangenen Datensätze sollten irgendwo gespeichert werden (diese Aufgabe wird später besprochen). Die Methode reservedSize ermöglicht eine optimierte Speicherverteilung (wenn die Anzahl der Datensätze in der Quelle im Voraus bekannt ist).

Jede Dimension des Hyperwürfels wird basierend auf einem oder mehreren Datensatzfeldern berechnet. Es ist sinnvoll, jedes Feld als Element einer Enumeration zu markieren. Für die Analyse der Kontohistorie kann beispielsweise die folgende Enumeration verwendet werden.

  // MT4 and MT5 hedge
  enum TRADE_RECORD_FIELDS
  {
    FIELD_NONE,          // none
    FIELD_NUMBER,        // serial number
    FIELD_TICKET,        // ticket
    FIELD_SYMBOL,        // symbol
    FIELD_TYPE,          // type (OP_BUY/OP_SELL)
    FIELD_DATETIME1,     // open datetime
    FIELD_DATETIME2,     // close datetime
    FIELD_DURATION,      // duration
    FIELD_MAGIC,         // magic number
    FIELD_LOT,           // lot
    FIELD_PROFIT_AMOUNT, // profit amount
    FIELD_PROFIT_PERCENT,// profit percent
    FIELD_PROFIT_POINT,  // profit points
    FIELD_COMMISSION,    // commission
    FIELD_SWAP,          // swap
    FIELD_CUSTOM1,       // custom 1
    FIELD_CUSTOM2        // custom 2
  };

Die letzten beiden Felder können für die Berechnung von nicht standardisierten Variablen verwendet werden.

Die folgende Enumeration kann für die Analyse der Ergebnisse der MetaTrader-Optimierung vorgeschlagen werden.

  enum OPTIMIZATION_REPORT_FIELDS
  {
    OPTIMIZATION_PASS,
    OPTIMIZATION_PROFIT,
    OPTIMIZATION_TRADE_COUNT,
    OPTIMIZATION_PROFIT_FACTOR,
    OPTIMIZATION_EXPECTED_PAYOFF,
    OPTIMIZATION_DRAWDOWN_AMOUNT,
    OPTIMIZATION_DRAWDOWN_PERCENT,
    OPTIMIZATION_PARAMETER_1,
    OPTIMIZATION_PARAMETER_2,
    //...
  };

Für jeden praktischen Anwendungsfall sollte eine individuelle Enumeration erstellt werden. Dann kann es als Parameter der Vorlagenklasse Selector verwendet werden.

  template<typename E>
  class Selector
  {
    protected:
      E selector;
      string _typename;
      
    public:
      Selector(const E field): selector(field)
      {
        _typename = typename(this);
      }
      
      // returns index of cell to store values from the record
      virtual bool select(const Record *r, int &index) const = 0;
      
      virtual int getRange() const = 0;
      virtual float getMin() const = 0;
      virtual float getMax() const = 0;
      
      virtual E getField() const
      {
        return selector;
      }
      
      virtual string getLabel(const int index) const = 0;
      
      virtual string getTitle() const
      {
        return _typename + "(" + EnumToString(selector) + ")";
      }
  };

Das Auswahlfeld speichert nur einen Wert, ein Element der Enumeration. Wenn beispielsweise TRADE_RECORD_FIELDS verwendet wird, kann ein Selektor für den Kauf/Verkaufsvorgang wie folgt angelegt werden:

  new Selector<TRADE_RECORD_FIELDS>(FIELD_TYPE);

Das Feld _typame ist ein Hilfsfeld. Es wird in allen abgeleiteten Klassen überschrieben, um Selektoren zu identifizieren, was bei der Visualisierung von Ergebnissen nützlich ist. Das Feld wird in der virtuellen Methode getTitle verwendet.

Der größte Teil der Operation wird von einer Klasse in der Methode 'select' durchgeführt. Dabei wird jeder Eingabesatz als spezifischer Indexwert entlang der Koordinatenachse abgebildet, der durch den aktuellen Selektor gebildet wird. Der Index muss im Bereich zwischen den von den Methoden getMin und getMax zurückgegebenen Werten liegen, während die Gesamtzahl der Indizes gleich der von getRange zurückgegebenen Zahl ist. Wenn der Datensatz aus irgendeinem Grund nicht korrekt im Sektordefinitionsbereich zugeordnet werden kann, gibt die Methode 'select' false zurück. Wenn die Zuordnung korrekt durchgeführt wurde, wird true zurückgegeben.

Die Methode getLabel liefert eine benutzerfreundliche Beschreibung des spezifischen Index. So muss beispielsweise bei Kauf/Verkaufsoperationen der Index 0 "Kaufen" generieren, während der Index 1 "Verkaufen" generieren muss.

Implementierung spezifischer Selektor- und Datenadapterklassen für die Handelshistorie

Da wir die Handelsgeschichte analysieren werden, stellen wir eine Zwischenklasse von Selektoren vor, die auf der Enumeration TRADE_RECORD_FIELDS basiert.

  class TradeSelector: public Selector<TRADE_RECORD_FIELDS>
  {
    public:
      TradeSelector(const TRADE_RECORD_FIELDS field): Selector(field)
      {
        _typename = typename(this);
      }
  
      virtual bool select(const Record *r, int &index) const
      {
        index = 0;
        return true;
      }
      
      virtual int getRange() const
      {
        return 1; // this is a scalar by default, returns 1 value
      }
      
      virtual double getMin() const
      {
        return 0;
      }
      
      virtual double getMax() const
      {
        return (double)(getRange() - 1);
      }
      
      virtual string getLabel(const int index) const
      {
        return EnumToString(selector) + "[" + (string)index + "]";
      }
  };

Standardmäßig werden alle Datensätze auf der gleichen Zelle abgebildet. Mit diesem Selektor können Sie z.B. die gesamten Gewinndaten ermitteln.

Basierend auf diesem Selektor können wir nun leicht spezifische Ableitungsarten von Selektoren bestimmen. Dies wird auch für die Gruppierung von Datensätzen nach der Vorgangsart (Kauf/Verkauf) verwendet.

  class TypeSelector: public TradeSelector
  {
    public:
      TypeSelector(): TradeSelector(FIELD_TYPE)
      {
        _typename = typename(this);
      }
  
      virtual bool select(const Record *r, int &index) const
      {
        ...
      }
      
      virtual int getRange() const
      {
        return 2; // OP_BUY, OP_SELL
      }
      
      virtual double getMin() const
      {
        return OP_BUY;
      }
      
      virtual double getMax() const
      {
        return OP_SELL;
      }
      
      virtual string getLabel(const int index) const
      {
        const static string types[2] = {"buy", "sell"};
        return types[index];
      }
  };

Wir haben die Klasse mit dem Element FIELD_TYPE im Konstruktor definiert. Die Methode getRange gibt 2 zurück, da wir hier nur 2 mögliche Typen haben: OP_BUY oder OP_SELL. Die Methoden getMin und getMax geben die entsprechende Konstante zurück. Was sollte die Methode 'select' enthalten?

Zunächst müssen wir entscheiden, welche Informationen in jedem Datensatz gespeichert werden. Dies kann mit der Klasse TradeRecord erfolgen, die von Record abgeleitet ist und für die Arbeit mit der Handelshistorie angepasst ist.

  class TradeRecord: public Record
  {
    private:
      static int counter;
  
    protected:
      void fillByOrder()
      {
        set(FIELD_NUMBER, counter++);
        set(FIELD_TICKET, OrderTicket());
        set(FIELD_TYPE, OrderType());
        set(FIELD_DATETIME1, OrderOpenTime());
        set(FIELD_DATETIME2, OrderCloseTime());
        set(FIELD_DURATION, OrderCloseTime() - OrderOpenTime());
        set(FIELD_MAGIC, OrderMagicNumber());
        set(FIELD_LOT, (float)OrderLots());
        set(FIELD_PROFIT_AMOUNT, (float)OrderProfit());
        set(FIELD_PROFIT_POINT, (float)((OrderType() == OP_BUY ? +1 : -1) * (OrderClosePrice() - OrderOpenPrice()) / SymbolInfoDouble(OrderSymbol(), SYMBOL_POINT)));
        set(FIELD_COMMISSION, (float)OrderCommission());
        set(FIELD_SWAP, (float)OrderSwap());
      }
      
    public:
      TradeRecord(): Record(TRADE_RECORD_FIELDS_NUMBER)
      {
        fillByOrder();
      }
  };

Die Hilfsmethode fillByOrder zeigt, wie die meisten Datensatzfelder basierend auf der aktuellen Bestellung gefüllt werden können. Natürlich muss die Reihenfolge an einer anderen Stelle im Code vorselektiert werden. Hier verwenden wir die Notation der MetaTrader 4 Handelsfunktionen. Die Unterstützung von MetaTrader 5 wird durch die Einbindung der Bibliothek MT4Orders implementiert (eine der Versionen ist unten angehängt, immer die aktuelle Version prüfen und herunterladen). So können wir einen plattformübergreifenden Code erstellen.

Die Nummer der Felder TRADE_RECORD_FIELDS_NUMBER kann entweder fest als Makrodefinition kodiert oder dynamisch aus der Enumeration TRADE_RECORD_FIELDS berechnet werden. Der zweite Ansatz ist im beigefügten Code implementiert, für den die spezielle Vorlage-Funktion EnumToArray verwendet wird.

Wie aus der Methode fillByOrder ersichtlich, wird das Feld FIELD_TYPE durch die Betriebsart aus OrderType gefüllt. Nun können wir uns wieder der Klasse TypeSelector zuwenden und ihre Methode 'select' implementieren.

    virtual bool select(const Record *r, int &index) const
    {
      index = (int)r.get(selector);
      return index >= getMin() && index <= getMax();
    }

Hier lesen wir den Feldwert (Selektor) aus dem Eingabesatz (r) und weisen seinen Wert (der entweder OP_BUY oder OP_SELL sein kann) dem Indexausgabeparameter zu. Die Kalkulation berücksichtigt nur Marktorder, daher wird für alle anderen Typen falsch zurückgegeben. Später werden wir andere Selektortypen berücksichtigen.

Nun ist es an der Zeit, einen Datenadapter für die Handelshistorie zu entwickeln. Dies ist die Klasse, in der die Datensätze TradeRecord, basierend auf der realen Handelsgeschichte des Kontos, generiert werden.

  class HistoryDataAdapter: public DataAdapter
  {
    private:
      int size;
      int cursor;
      
    protected:
      void reset()
      {
        cursor = 0;
        size = OrdersHistoryTotal();
      }
      
    public:
      HistoryDataAdapter()
      {
        reset();
      }
      
      virtual int reservedSize()
      {
        return size;
      }
      
      virtual Record *getNext()
      {
        if(cursor < size)
        {
          while(OrderSelect(cursor++, SELECT_BY_POS, MODE_HISTORY))
          {
            if(OrderType() < 2)
            {
              return new TradeRecord();
            }
          }
          return NULL;
        }
        return NULL;
      }
  };

Der Adapter durchläuft sequentiell alle in der Historie verfügbaren Orders und erzeugt für jede Marktorder eine TradeRecord-Instanz. Der Code wird hier in vereinfachter Form dargestellt. Während der tatsächlichen Verwendung müssen wir möglicherweise Objekte erstellen, die nicht der Klasse TradeRecord, sondern einer abgeleiteten Klasse angehören: Wir haben zwei benutzerdefinierte Felder für die Enumeration TRADE_RECORD_FIELDS reserviert. Daher ist HistoryDataAdapter eine Vorlagenklasse, während der Vorlagenparameter die eigentliche Klasse der erzeugten Aktenobjekte ist. Die Klasse Record muss eine leere virtuelle Methode zum Füllen von benutzerdefinierten Feldern enthalten:

    virtual void fillCustomFields() {/* does nothing */};

Sie können den vollständigen Implementierungsansatz selbst analysieren: Im Kern wird die Klasse CustomTradeRecord verwendet. In fillCustomFields berechnet diese Klasse (die ein Kind von TradeRecord ist) MFE (Maximum Favorable Excursion) und MAE (Maximum Adverse Excursion) für jede Position und schreibt diese Werte in die Felder FIELD_CUSTOM1 und FIELD_CUSTOM2.

Implementierung von Aggregatoren und einer Control-Klasse

Wir benötigen einen Platz, um den Adapter zu erstellen und deren Methode getNext aufzurufen. Nun befassen wir uns mit der "Leitstelle", die Klasse Analyst. Zusätzlich zum Adapterstart muss die Klasse die empfangenen Datensätze in einem internen Array speichern.

  template<typename E>
  class Analyst
  {
    private:
      DataAdapter *adapter;
      Record *data[];
      
    public:
      Analyst(DataAdapter &a): adapter(&a)
      {
        ArrayResize(data, adapter.reservedSize());
      }
      
      ~Analyst()
      {
        int n = ArraySize(data);
        for(int i = 0; i < n; i++)
        {
          if(CheckPointer(data[i]) == POINTER_DYNAMIC) delete data[i];
        }
      }
      
      void acquireData()
      {
        Record *record;
        int i = 0;
        while((record = adapter.getNext()) != NULL)
        {
          data[i++] = record;
        }
        ArrayResize(data, i);
      }
  };

Die Klasse legt keinen Adapter an, erhält aber einen fertigen als Parameter des Konstruktors. Dies ist ein bekanntes Konstruktionsprinzip — Dependency Injection. Es ermöglicht die Trennung von Analyst und einer bestimmten Implementierung von DataAdapter. Mit anderen Worten, wir können verschiedene Adaptervarianten ohne Modifikationen in der Analyst-Klasse problemlos ersetzen.

Die Analyst-Klasse ist nun in der Lage, das interne Array von Datensätzen zu füllen, weiß aber immer noch nicht, wie man die Hauptfunktion, d.h. die Aggregation von Daten, ausführt. Diese Aufgabe wird vom Aggregator implementiert.

Aggregatoren sind Klassen, die vordefinierte Variablen (Statistiken) für die ausgewählten Datensatzfelder berechnen können. Die Basisklasse für Aggregatoren ist MetaCube, ein Speicher, der auf einem multidimensionalen Array basiert.

  class MetaCube
  {
    protected:
      int dimensions[];
      int offsets[];
      double totals[];
      string _typename;
      
    public:
      int getDimension() const
      {
        return ArraySize(dimensions);
      }
      
      int getDimensionRange(const int n) const
      {
        return dimensions[n];
      }
      
      int getCubeSize() const
      {
        return ArraySize(totals);
      }
      
      virtual double getValue(const int &indices[]) const = 0;
  };

Das Array 'dimensions' beschreibt die Struktur des Hyperwürfels. Seine Größe entspricht der Anzahl der verwendeten Selektoren, d.h. den Dimensionen. Jedes Element des Arrays 'dimensions' enthält die Würfelgröße in dieser Dimension, die durch den Wertebereich des entsprechenden Selektors bestimmt wird. Um beispielsweise Gewinne nach Wochentag zu sehen, müssen wir einen Selektor erstellen, der die Tageszahl als Index von 0 bis 6 zurückgibt, je nach Reihenfolge (Position) der Öffnungs- oder Schließzeit. Da dies der einzige Selektor ist, hat das Array 'dimensions' ein Element, und sein Wert ist 7. Wenn wir einen weiteren Selektor hinzufügen, z.B. den zuvor beschriebenen TypeSelector, um die Gewinne in Bezug auf den Wochentag und die Art der Operation zu betrachten, enthält das Array 'dimensions' 2 Elemente mit den Werten 7 und 2. Das bedeutet auch, dass der Hyperwürfel 14 Zellen mit Statistiken enthält.

Das Array mit allen Werten (in unserem Beispiel 14) ist in 'totals' enthalten. Da der Hyperwürfel mehrdimensional ist, kann es vorkommen, dass das Array als nur eine Dimension deklariert wird. Dies liegt daran, dass wir die Dimension des Hyperwürfels, die der Benutzer hinzufügen muss, nicht im Voraus kennen. Darüber hinaus unterstützt MQL keine mehrdimensionalen Arrays, in denen absolut alle Dimensionen dynamisch verteilt werden. Daher wird das übliche "flache" Array (Vektor) verwendet. Eine spezielle Indexierung wird verwendet, um Zellen in mehreren Dimensionen in diesem Array zu speichern. Als Nächstes betrachten wir die Berechnung von Offsets für jede Dimension.

Die Basisklasse reserviert keine Arrays zu und initialisiert sie nicht, da dies von abgeleiteten Klassen durchgeführt wird.

Da von allen Aggregatoren erwartet wird, dass sie viele gemeinsame Merkmale haben, sollten wir sie alle in einer Zwischenklasse packen.

  template<typename E>
  class Aggregator: public MetaCube
  {
    protected:
      const E field;

Jeder Aggregator verarbeitet ein bestimmtes Datensatzfeld. Dieses Feld wird in der Klasse, in der Variablen 'field' angegeben, die im Konstruktor gefüllt wird (siehe unten). Dies kann z.B. der Gewinn sein (FIELD_PROFIT_AMOUNT).

      const int selectorCount;
      const Selector<E> *selectors[];

Die Berechnungen werden in einem mehrdimensionalen Raum durchgeführt, der aus einer beliebigen Anzahl von Selektoren (selectorCount) besteht. Wir haben bisher die Berechnung der Gewinne mit einer Aufschlüsselung nach Wochentag und nach Betriebsart, die zwei Selektoren erfordert, berücksichtigt. Sie werden in der Referenzliste der 'selectors' gespeichert. Die Selektorobjekte werden als Parameter dem Konstruktor übergeben.

    public:
      Aggregator(const E f, const Selector<E> *&s[]): field(f), selectorCount(ArraySize(s))
      {
        ArrayResize(selectors, selectorCount);
        for(int i = 0; i < selectorCount; i++)
        {
          selectors[i] = s[i];
        }
        _typename = typename(this);
      }

Wie Sie sich erinnern, ist das Array 'totals' zum Speichern der berechneten Werte eindimensional. Die folgende Funktion wird verwendet, um die Indizes des multidimensionalen Selektorraums in einen Offset in einem eindimensionalen Array zu konvertieren.

      int mixIndex(const int &k[]) const
      {
        int result = 0;
        for(int i = 0; i < selectorCount; i++)
        {
          result += k[i] * offsets[i];
        }
        return result;
      }

Es akzeptiert ein Array mit Indizes als Eingabe und gibt die laufende Nummer des Elements zurück. Hier wird das Array 'offsets' verwendet — zu diesem Zeitpunkt muss das Array bereits gefüllt sein. Seine Initialisierung ist einer der wichtigsten Punkte und wird in der Methode setSelectorBounds durchgeführt.

      virtual void setSelectorBounds()
      {
        ArrayResize(dimensions, selectorCount);
        int total = 1;
        for(int i = 0; i < selectorCount; i++)
        {
          dimensions[i] = selectors[i].getRange();
          total *= dimensions[i];
        }
        ArrayResize(totals, total);
        ArrayInitialize(totals, 0);
        
        ArrayResize(offsets, selectorCount);
        offsets[0] = 1;
        for(int i = 1; i < selectorCount; i++)
        {
          offsets[i] = dimensions[i - 1] * offsets[i - 1]; // 1, X, Y*X
        }
      }

Ihr Zweck ist es, die Bereiche aller Selektoren zu erhalten und sequentiell zu multiplizieren: So können wir die Anzahl der Elemente bestimmen, die beim Erhöhen der Koordinate um eins in jeder Hyperwürfel-Dimension "übersprungen" werden.

Die Berechnung der aggregierten Variablen erfolgt in der Berechnungsmethode.

      // build an array with number of dimensions equal to number of selectors
      virtual void calculate(const Record *&data[])
      {
        int k[];
        ArrayResize(k, selectorCount);
        int n = ArraySize(data);
        for(int i = 0; i < n; i++)
        {
          int j = 0;
          for(j = 0; j < selectorCount; j++)
          {
            int d;
            if(!selectors[j].select(data[i], d)) // does record satisfy selector?
            {
              break;                             // skip it, if not
            }
            k[j] = d;
          }
          if(j == selectorCount)
          {
            update(mixIndex(k), data[i].get(field));
          }
        }
      }

Die Methode wird für das Array der Datensätze aufgerufen. Jeder Datensatz wird in der Schleife nacheinander an jeden Selektor übergeben. Wenn er erfolgreich in gültigen Indizes in allen Selektoren zugeordnet wird (jeder Selektor hat seinen eigenen Index), dann wird der gesamte Satz von Indizes im lokalen Array k gespeichert. Wenn alle Selektoren Indizes bestimmt haben, wird die Methode 'update' aufgerufen. In das Verfahren wird eingegeben: der Offset im Array 'totals' (der Offset wird mit dem oben genannten mixIndex berechnet) und der Wert des angegebenen 'field' (er wird in den Aggregatoren gesetzt) aus dem aktuellen Datensatz. Im Beispiel der Gewinnverteilungsanalyse ist die Variable 'field' gleich FIELD_PROFIT_AMOUNT, während die Werte für dieses Feld vom Aufruf OrderProfit() bereitgestellt werden.

      virtual void update(const int index, const float value) = 0;

Die Aktualisierungsmethode ist in dieser Klasse abstrakt und muss in ihren Nachkommen neu definiert werden.

Außerdem muss der Aggregator mindestens eine Methode für den Zugriff auf die Berechnungsergebnisse bereitstellen. Die Einfachste von ihnen ist das Empfangen des Wertes einer bestimmten Zelle, basierend auf dem gesamten Satz von Indizes.

      double getValue(const int &indices[]) const
      {
        return totals[mixIndex(indices)];
      }
  };

Die Basisklasse Aggregator führt fast alle Grobarbeiten aus. Jetzt können wir einfach viele spezifische Aggregatoren implementieren.

Aber zuerst kommen wir auf die Klasse Analyst zurück: Wir müssen ihr eine Referenz auf den Aggregator hinzufügen, der auch über den Parameter des Konstruktors übergeben wird.

  template<typename E>
  class Analyst
  {
    private:
      DataAdapter *adapter;
      Record *data[];
      Aggregator<E> *aggregator;
      
    public:
      Analyst(DataAdapter &a, Aggregator<E> &g): adapter(&a), aggregator(&g)
      {
        ArrayResize(data, adapter.reservedSize());
      }

In der Methode acquireData konfigurieren wir die Hyperwürfel-Dimensionen mit dem zusätzlichen Aufruf der Methode setSelectorBounds des Aggregators.

    void acquireData()
    {
      Record *record;
      int i = 0;
      while((record = adapter.getNext()) != NULL)
      {
        data[i++] = record;
      }
      ArrayResize(data, i);
      aggregator.setSelectorBounds(i);
    }

Die Hauptaufgabe, d.h. die Berechnung aller Werte des Hyperwürfels, wird im Aggregator implementiert (wir haben die Methode 'calculate' bereits vorher berücksichtigt; hier wird das Array der Datensätze ihr übergeben).

    void build()
    {
      aggregator.calculate(data);
    }

Es geht hier nicht nur um die Klasse Analyst. Früher hatten wir geplant, sie in die Lage zu versetzen, die Ergebnisse anzuzeigen, indem wir sie als eine spezielle Display-Schnittstelle formalisieren. Die Schnittstelle ist in ähnlicher Weise mit Analyst verbunden (durch Übergabe einer Referenz an den Konstruktor):

  template<typename E>
  class Analyst
  {
    private:
      ...
      Display *output;
      
    public:
      Analyst(DataAdapter &a, Aggregator<E> &g, Display &d): adapter(&a), aggregator(&g), output(&d)
      {
        ...
      }
      
      void display()
      {
        output.display(aggregator);
      }
  };

Die Inhalte von 'Display' sind einfach:

  class Display
  {
    public:
      virtual void display(MetaCube *metaData) = 0;
  };

Die Klasse enthält eine abstrakte virtuelle Methode, in die der Hyperwürfel als Datenquelle eingegeben wird. Einige der Parameter, die die Reihenfolge des Wertdrucks beeinflussen, werden hier aus Gründen der Kürze weggelassen. Die Visualisierungsspezifika und die notwendigen zusätzlichen Einstellungen erscheinen in den abgeleiteten Klassen.

Um die analytischen Klassen zu testen, benötigen wir mindestens eine Implementierung der Schnittstelle 'Display'. Erstellen wir sie, indem wir Nachrichten an das Expertenjournal schreiben. Es wird LogDisplay genannt. Die Schnittstelle durchläuft alle Koordinaten des Hyperwürfels und gibt die aggregierten Werte zusammen mit den entsprechenden Koordinaten ungefähr wie folgt aus:

  class LogDisplay: public Display
  {
    public:
      virtual void display(MetaCube *metaData) override
      {
        int n = metaData.getDimension();
        int indices[], cursors[];
        ArrayResize(indices, n);
        ArrayResize(cursors, n);
        ArrayInitialize(cursors, 0);
  
        for(int i = 0; i < n; i++)
        {
          indices[i] = metaData.getDimensionRange(i);
        }
        
        bool looping = false;
        int count = 0;
        do
        {
          ArrayPrint(cursors);
          Print(metaData.getValue(cursors));
  
          for(int i = 0; i < n; i++)
          {
            if(cursors[i] < indices[i] - 1)
            {
              looping = true;
              cursors[i]++;
              break;
            }
            else
            {
              cursors[i] = 0;
            }
            looping = false;
          }
        }
        while(looping && !IsStopped());
      }
  };

Ich sage "ungefähr", weil die Implementierung von LogDisplay mit einer komfortablen Formatierung von Protokollen etwas komplizierter wäre. Die Vollversion der Klasse ist im angehängten Quellcode verfügbar.

Natürlich ist dies nicht so effizient wie ein Diagramm, aber die Erstellung von zwei- oder dreidimensionalen Bildern ist ein weiteres separates Thema, das wir nicht berücksichtigen werden (obwohl Sie dafür verschiedene Technologien wie Objekte, Leinwände und externe grafische Bibliotheken verwenden können, einschließlich solcher, die auf Webtechnologien basieren).

Somit haben wir die Basisklasse Aggregator. Auf seiner Grundlage können wir leicht mehrere abgeleitete Klassen mit spezifischen Berechnungen von aggregierten Variablen in der Update-Methode erhalten. Insbesondere kann mit dem folgenden einfachen Code die Summe der Werte berechnet werden, die von einem bestimmten Selektor aus allen Datensätzen extrahiert werden:

  template<typename E>
  class SumAggregator: public Aggregator<E>
  {
    public:
      SumAggregator(const E f, const Selector<E> *&s[]): Aggregator(f, s)
      {
        _typename = typename(this);
      }
      
      virtual void update(const int index, const float value) override
      {
        totals[index] += value;
      }
  };

Die Berechnung des Durchschnitts ist nur ein wenig komplizierter:

  template<typename E>
  class AverageAggregator: public Aggregator<E>
  {
    protected:
      int counters[];
      
    public:
      AverageAggregator(const E f, const Selector<E> *&s[]): Aggregator(f, s)
      {
        _typename = typename(this);
      }
      
      virtual void setSelectorBounds() override
      {
        Aggregator<E>::setSelectorBounds();
        ArrayResize(counters, ArraySize(totals));
        ArrayInitialize(counters, 0);
      }
  
      virtual void update(const int index, const float value) override
      {
        totals[index] = (totals[index] * counters[index] + value) / (counters[index] + 1);
        counters[index]++;
      }
  };

Nachdem wir alle beteiligten Klassen betrachtet haben, lassen Sie uns ihren Interaktionsalgorithmus verallgemeinern:

  • Wir erstellen wir das Objekt HistoryDataAdapter;
  • Wir erstellen mehrere spezifische Selektoren (jeder der Selektoren ist an mindestens ein Feld gebunden, z.B. Handelsbetriebsart usw.) und speichern diese in einem Array;
  • Wir erstellen das spezifische Aggregatorobjekt, z.B. SumAggregator. Wir übergeben ihm das Array der Selektoren und die Angabe des Feldes, auf dessen Grundlage die Aggregation durchgeführt werden soll;
  • Wir erstellen das Objekt LogDisplay;
  • Wir erstellen das Objekt Analyst mit den Objekten des Adapters, des Aggregators und der Anzeige;
  • Nacheinander rufen wir auf:
      analyst.acquireData();
      analyst.build();
      analyst.display();
  • Vergessen wir nicht, die Objekte am Ende zu löschen.

Sonderfall: dynamische Selektoren

Unser Programm ist fast fertig. Zuvor haben wir einen Teil davon weggelassen, um die Beschreibung zu vereinfachen. Jetzt ist es an der Zeit, sie zu beseitigen.

Alle oben genannten Selektoren hatten einen konstanten Wertebereich. Zum Beispiel gibt es immer 7 Tage in der Woche, während Marktorder entweder Kaufen oder Verkaufen sind. Der Bereich ist jedoch möglicherweise nicht im Voraus bekannt, was sehr häufig vorkommt.

Möglicherweise benötigen wir einen Hyperwürfel, der Arbeitssymbole oder die Magicnummern der EAs widerspiegelt. Für die Lösung dieser Aufgabe müssen wir zuerst alle eindeutigen Instrumente oder die Magicnummern in einem internen Array sammeln, und dann verwenden wir die Array-Größe für den jeweiligen Auswahlbereich.

Erstellen wir die Klasse 'Vocabulary' für die Verwaltung dieser internen Arrays. Wir werden die Verwendung in Verbindung mit der Klasse SymbolSelector analysieren.

Unsere Implementierung des Vokabulars ist recht einfach (man kann es durch ein beliebiges ersetzen).

  template<typename T>
  class Vocabulary
  {
    protected:
      T index[];

Das Array 'index' ist reserviert für die zu speichernden individuellen Werten.

    public:
      int get(const T &text) const
      {
        int n = ArraySize(index);
        for(int i = 0; i < n; i++)
        {
          if(index[i] == text) return i;
        }
        return -(n + 1);
      }

Mit der Methode 'get' wird überprüft, ob ein bestimmter Wert bereits im Array vorhanden ist. Wenn es einen solchen Wert gibt, gibt die Methode den gefundenen Index zurück. Wenn der Wert nicht in dem Array vorhanden ist, gibt das Verfahren die um 1 erhöhte Array-Größe mit einem Minuszeichen zurück. Dies ermöglicht eine leichte Optimierung der nächsten Methode zum Hinzufügen eines neuen Wertes zum Array.

      int add(const T text)
      {
        int n = get(text);
        if(n < 0)
        {
          n = -n;
          ArrayResize(index, n);
          index[n - 1] = text;
          return n - 1;
        }
        return n;
      }

Außerdem müssen wir Methoden zum Empfangen der Array-Größe und der darin gespeicherten Werte nach Index bereitstellen.

      int size() const
      {
        return ArraySize(index);
      }
      
      T operator[](const int slot) const
      {
        return index[slot];
      }
  };

In unserem Fall werden die Arbeitssymbole im Rahmen von Orders (Positionen) analysiert, lassen Sie uns daher das Vokabular in die Klasse TradeRecord einbetten.

  class TradeRecord: public Record
  {
    private:
      ...
      static Vocabulary<string> symbols;
  
    protected:
      void fillByOrder(const double balance)
      {
        ...
        set(FIELD_SYMBOL, symbols.add(OrderSymbol())); // symbols are stored as indices from vocabulary
      }
  
    public:
      static int getSymbolCount()
      {
        return symbols.size();
      }
      
      static string getSymbol(const int index)
      {
        return symbols[index];
      }
      
      static int getSymbolIndex(const string s)
      {
        return symbols.get(s);
      }

Das Vokabular wird als statische Variable deklariert, da es für die gesamte Handelsgeschichte verwendet wird.

Jetzt können wir SymbolSelector implementieren.

  class SymbolSelector: public TradeSelector
  {
    public:
      SymbolSelector(): TradeSelector(FIELD_SYMBOL)
      {
        _typename = typename(this);
      }
      
      virtual bool select(const Record *r, int &index) const override
      {
        index = (int)r.get(selector);
        return (index >= 0);
      }
      
      virtual int getRange() const override
      {
        return TradeRecord::getSymbolCount();
      }
      
      virtual string getLabel(const int index) const override
      {
        return TradeRecord::getSymbol(index);
      }
  };

Der Selektor der Magicnummern ist in ähnlicher Weise implementiert.

Die allgemeine Liste der zur Verfügung gestellten Selektoren umfasst Folgendes (die Notwendigkeit einer externen Bindung an das Feld ist in Klammern angegeben; wenn sie weggelassen wird, bedeutet dies, dass die Bindung an ein bestimmtes Feld bereits innerhalb der Selektorklasse vorgesehen ist):

  • TradeSelector (beliebiges Feld) — ein Skalar, ein Wert (eine Zusammenfassung aller Datensätze für "echte" Aggregatoren oder der Feldwert eines bestimmten Datensatzes für IdentityAggregator (siehe unten));
  • TypeSelector — Kaufen oder Verkaufen je nach OrderType();
  • WeekDaySelector (Feld vom Typ datetime) — der Wochentag, z.B. in OrderOpenTime() oder OrderCloseTime();
  • DayHourSelector (Feld vom Typ datetime) — Stunde innerhalb des Tages;
  • HourMinuteSelector (Feld vom Typ datetime) — Minute innerhalb der Stunde;
  • SymbolSelector — Arbeitssymbol, ein Index im eindeutigen OrderSymbol() Vokabular;
  • SerialNumberSelector — die Sequenznummer des Datensatzes (Order);
  • MagicSelector — die magische Zahl, ein Index im eindeutig OrderMagicNumber() Vokabular;
  • ProfitableSelector — true = Gewinn, false = Verlust, aus dem Feld OrderProfit();
  • Quantisierungsselektor (Doppeltypfeld) — ein Vokabular aus zufälligen Doppeltypwerten (z.B. für die Losgröße);
  • DaysRangeSelector — Beispiel für einen benutzerdefinierten Selektor mit zwei Feldern vom Typ Datetime (OrderCloseTime() und OrderOpenTime()), der auf der Klasse DateTimeSelector basiert, dem gemeinsamen übergeordneten Element aller Selektoren für Feldern vom Typ Datetime; im Gegensatz zu den anderen im Kern definierten Selektoren ist dieser Selektor in der Demo EA implementiert (siehe unten).

SerialNumberSelector unterscheidet sich deutlich von anderen Selektoren. Sein Bereich entspricht der Gesamtzahl der Datensätze. Dies ermöglicht die Erzeugung eines Hyperwürfels, bei dem die Datensätze in einer der Dimensionen (in der Regel in der ersten, X) sequentiell gezählt werden, während die angegebenen Felder in die andere Dimension kopiert werden. Die Felder werden durch die Selektoren definiert: Spezielle Selektoren beinhalten bereits eine Feldbindung; wenn Sie ein Feld benötigen, für das es keinen fertigen Selektor gibt, wie z.B. "Swap", dann kann der universelle TradeSelector verwendet werden. Mit anderen Worten, SerialNumberSelector erlaubt die Möglichkeit, die Daten des Quelldatensatzes innerhalb der aggregierenden Metapher des Hyperwürfel zu lesen. Dies geschieht durch die Verwendung des Pseudo-Aggregators IdentityAggregator (siehe unten).

Die folgenden Aggregatoren stehen zur Verfügung:

  • SumAggregator — die Summe der Feldwerte;
  • AverageAggregator — der durchschnittliche Feldwert;
  • MaxAggregator — der maximale Feldwert;
  • MinAggregator — der minimale Feldwert;
  • CountAggregator — die Anzahl der Datensätze;
  • ProfitFactorAggregator — das Verhältnis der Summe der positiven Feldwerte zur Summe der negativen Feldwerte;
  • IdentityAggregator (SerialNumberSelector entlang der X-Achse) — ein spezieller Selektortyp für das "transparente" Kopieren von Feldwerten in den Hyperwürfel, ohne Aggregation;
  • ProgressiveTotalAggregator (SerialNumberSelector entlang der X-Achse) — eine kumulative Summe für das Feld;

Die letzten beiden Aggregatoren unterscheiden sich von den anderen. Wenn IdentityAggregator ausgewählt ist, ist die Größe des Hyperwürfels immer gleich 2. Die Datensätze werden mit dem SerialNumberSelector entlang der X-Achse reflektiert, während jeder Zähler entlang der zweiten Achse (eigentlich ist es Vektor/Spalte) einem Selektor entspricht, mit dem das zu lesende Feld aus den Quellsätzen bestimmt wird. Wenn es also drei zusätzliche Selektoren gibt (zusätzlich zum SerialNumberSelector), gibt es drei Zählungen entlang der Y-Achse. Der Würfel hat jedoch noch zwei Dimensionen: die X- und Y-Achse. In der Regel wird der Würfel nach einem anderen Prinzip erzeugt: Jeder Selektor entspricht seiner eigenen Dimension, also bedeuten 3 Dimensionen 3 Achsen.

Der ProgressiveTotalAggregator behandelt die erste Dimension auf besondere Weise. Wie der Name schon sagt, ermöglicht der Aggregator die Berechnung der kumulierten Summe, während dies entlang der X-Achse geschieht. Wenn Sie beispielsweise das Gewinnfeld im Aggregatorparameter angeben, erhalten Sie die allgemeine Bilanzkurve. Wenn Sie Symbole (SymbolSelector) entlang der Y-Achse (im zweiten Selektor) zeichnen, gibt es für jedes der verfügbaren Symbole mehrere[N] Ausgleichskurven. Wenn der zweite Selektor MagicSelector ist, gibt es eine separate Saldenkurve [M] von verschiedenen Expert Advisors. Darüber hinaus können beide Parameter kombiniert werden: Setzen Sie SymbolSelector auf Y und MagicSelector auf der Z-Achse (oder umgekehrt): Sie erhalten die Saldenkurve [N*M], die jeweils eine andere Magicnummer und Symbolkombination aufweisen.

Nun ist die OLAP-Engine bereit. Wir haben einige der Beschreibungsteile weggelassen, um den Artikel kurz zu halten. So enthält der Artikel beispielsweise keine Beschreibung der Filter (Filter, die Klassen FilterRange), die in der Architektur bereitgestellt wurden. Darüber hinaus kann dieser Hyperwürfel die aggregierten Werte nicht nur einzeln darstellen (Methode getValue(const int &indices[]), sondern auch als Vektor mit der folgenden Methode zurückgeben:

  virtual bool getVector(const int dimension, const int &consts[], PairArray *&result, const SORT_BY sortby = SORT_BY_NONE)

Die Methodenausgabe ist die spezielle Klasse PairArray. Sie speichert ein Array von Strukturen mit den Paaren von [value;name]. Wenn wir zum Beispiel einen Würfel bauen, der den Gewinn durch Symbole widerspiegelt, dann entspricht jede Summe einem bestimmten Symbol - deshalb wird sein Name in einem Paar neben dem Wert angegeben. Wie aus dem Prototyp der Methode ersichtlich, ist es in der Lage, PairArray in verschiedenen Modi zu sortieren: aufsteigend oder absteigend, nach Werten oder nach Tags:

  enum SORT_BY // applicable only for 1-dimensional cubes
  {
    SORT_BY_NONE,             // none
    SORT_BY_VALUE_ASCENDING,  // value (ascending)
    SORT_BY_VALUE_DESCENDING, // value (descending)
    SORT_BY_LABEL_ASCENDING,  // label (ascending)
    SORT_BY_LABEL_DESCENDING  // label (descending)
  };

Die Sortierung wird nur bei eindimensionalen Hyperwürfeln unterstützt. Theoretisch könnte es für die beliebige Anzahl von Dimensionen implementiert werden, aber das ist eine ziemliche Arbeit. Interessierte können eine solche Sortierung implementieren.

Die vollständigen Quellcodes sind beigefügt.

OLAPDEMO Beispiel

Lassen Sie uns nun den Hyperwürfel testen. Lassen Sie uns einen nicht handelnden Expertenberater erstellen, der den Verlauf des Kontohandels analysieren kann. Nennen wir ihn OLAPDEMO. Binden Sie die Header-Datei ein, in der alle wichtigen OLAP-Klassen enthalten sind.

  #include <OLAPcube.mqh>

Obwohl der Hyperwürfel eine beliebige Anzahl von Dimensionen verarbeiten kann, lassen Sie uns aus Gründen der Einfachheit diese nun auf drei beschränken. Das bedeutet, dass der Benutzer bis zu 3 Selektor gleichzeitig verwenden kann. Definieren Sie die unterstützten Selektortypen mit den Elementen der speziellen Enumeration:

  enum SELECTORS
  {
    SELECTOR_NONE,       // none
    SELECTOR_TYPE,       // type
    SELECTOR_SYMBOL,     // symbol
    SELECTOR_SERIAL,     // ordinal
    SELECTOR_MAGIC,      // magic
    SELECTOR_PROFITABLE, // profitable
    /* custom selector */
    SELECTOR_DURATION,   // duration in days
    /* all the next require a field as parameter */
    SELECTOR_WEEKDAY,    // day-of-week(datetime field)
    SELECTOR_DAYHOUR,    // hour-of-day(datetime field)
    SELECTOR_HOURMINUTE, // minute-of-hour(datetime field)
    SELECTOR_SCALAR,     // scalar(field)
    SELECTOR_QUANTS      // quants(field)
  };

Die Enumeration wird verwendet, die Eingabeparameter, die die Selektoren festlegen, zu beschreiben:

  sinput string X = "————— X axis —————";
  input SELECTORS SelectorX = SELECTOR_SYMBOL;
  input TRADE_RECORD_FIELDS FieldX = FIELD_NONE /* field does matter only for some selectors */;
  
  sinput string Y = "————— Y axis —————";
  input SELECTORS SelectorY = SELECTOR_NONE;
  input TRADE_RECORD_FIELDS FieldY = FIELD_NONE;
  
  sinput string Z = "————— Z axis —————";
  input SELECTORS SelectorZ = SELECTOR_NONE;
  input TRADE_RECORD_FIELDS FieldZ = FIELD_NONE;

Jede Selektorgruppe enthält eine Eingabe zur Einstellung des optionalen Datensatzfeldes (einige der Selektoren benötigen die Felder, andere nicht).

Lassen Sie uns einen Filter angeben (obwohl mehrere Filter verwendet werden können). Der Filter wird standardmäßig deaktiviert.

  sinput string F = "————— Filter —————";
  input SELECTORS Filter1 = SELECTOR_NONE;
  input TRADE_RECORD_FIELDS Filter1Field = FIELD_NONE;
  input float Filter1value1 = 0;
  input float Filter1value2 = 0;

Die Idee des Filters: Berücksichtigen Sie nur die Datensätze, in denen das angegebene Feld Filter1Feld den spezifischen Wert Filter1Wert1 hat (Filter1Wert2 muss gleich sein, was für die Erstellung des Filterobjekts in diesem Beispiel erforderlich ist). Beachten Sie, dass der Wert für Symbol- oder Zauberzahlenfelder einen Index im entsprechenden Vokabular bezeichnet. Der Filter kann optional nicht einen Wert, sondern einen Wertebereich zwischen Filter1value1 und Filter1value2 beinhalten (wenn sie nicht gleich sind, da das Objekt FilterRange nur für zwei verschiedene Werte angelegt werden kann). Diese Implementierung wurde für die Demonstration der Filtermöglichkeit erstellt, kann aber für den zukünftigen praktischen Einsatz stark erweitert werden.

Lassen Sie uns eine weitere Enumeration für die Aggregatoren beschreiben:

  enum AGGREGATORS
  {
    AGGREGATOR_SUM,         // SUM
    AGGREGATOR_AVERAGE,     // AVERAGE
    AGGREGATOR_MAX,         // MAX
    AGGREGATOR_MIN,         // MIN
    AGGREGATOR_COUNT,       // COUNT
    AGGREGATOR_PROFITFACTOR, // PROFIT FACTOR
    AGGREGATOR_PROGRESSIVE,  // PROGRESSIVE TOTAL
    AGGREGATOR_IDENTITY      // IDENTITY
  };

Sie wird in Gruppen von Eingabeparameter verwendet, die die Arbeitsaggregatoren beschreiben:

  sinput string A = "————— Aggregator —————";
  input AGGREGATORS AggregatorType = AGGREGATOR_SUM;
  input TRADE_RECORD_FIELDS AggregatorField = FIELD_PROFIT_AMOUNT;

Alle diese Selektoren einschließlich jener, die in den optionalen Filtern verwendet werden, werden in OnInit initialisiert.

  int selectorCount;
  SELECTORS selectorArray[4];
  TRADE_RECORD_FIELDS selectorField[4];
  
  int OnInit()
  {
    selectorCount = (SelectorX != SELECTOR_NONE) + (SelectorY != SELECTOR_NONE) + (SelectorZ != SELECTOR_NONE);
    selectorArray[0] = SelectorX;
    selectorArray[1] = SelectorY;
    selectorArray[2] = SelectorZ;
    selectorArray[3] = Filter1;
    selectorField[0] = FieldX;
    selectorField[1] = FieldY;
    selectorField[2] = FieldZ;
    selectorField[3] = Filter1Field;
  
    EventSetTimer(1);
    return(INIT_SUCCEEDED);
  }

OLAP läuft nur ein Mal, auf Grund des Timers.

  void OnTimer()
  {
    process();
    EventKillTimer();
  }
  
  void process()
  {
    HistoryDataAdapter history;
    Analyst<TRADE_RECORD_FIELDS> *analyst;
    
    Selector<TRADE_RECORD_FIELDS> *selectors[];
    ArrayResize(selectors, selectorCount);
    
    for(int i = 0; i < selectorCount; i++)
    {
      selectors[i] = createSelector(i);
    }
    Filter<TRADE_RECORD_FIELDS> *filters[];
    if(Filter1 != SELECTOR_NONE)
    {
      ArrayResize(filters, 1);
      Selector<TRADE_RECORD_FIELDS> *filterSelector = createSelector(3);
      if(Filter1value1 != Filter1value2)
      {
        filters[0] = new FilterRange<TRADE_RECORD_FIELDS>(filterSelector, Filter1value1, Filter1value2);
      }
      else
      {
        filters[0] = new Filter<TRADE_RECORD_FIELDS>(filterSelector, Filter1value1);
      }
    }
    
    Aggregator<TRADE_RECORD_FIELDS> *aggregator;
    
    // MQL does not support a 'class info' metaclass.
    // Otherwise we could use an array of classes instead of the switch
    switch(AggregatorType)
    {
      case AGGREGATOR_SUM:
        aggregator = new SumAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_AVERAGE:
        aggregator = new AverageAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_MAX:
        aggregator = new MaxAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_MIN:
        aggregator = new MinAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_COUNT:
        aggregator = new CountAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_PROFITFACTOR:
        aggregator = new ProfitFactorAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_PROGRESSIVE:
        aggregator = new ProgressiveTotalAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_IDENTITY:
        aggregator = new IdentityAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
    }
    
    LogDisplay display;
    
    analyst = new Analyst<TRADE_RECORD_FIELDS>(history, aggregator, display);
    
    analyst.acquireData();
    
    Print("Symbol number: ", TradeRecord::getSymbolCount());
    for(int i = 0; i < TradeRecord::getSymbolCount(); i++)
    {
      Print(i, "] ", TradeRecord::getSymbol(i));
    }
  
    Print("Magic number: ", TradeRecord::getMagicCount());
    for(int i = 0; i < TradeRecord::getMagicCount(); i++)
    {
      Print(i, "] ", TradeRecord::getMagic(i));
    }
  
    Print("Filters: ", aggregator.getFilterTitles());
    
    Print("Selectors: ", selectorCount);
    
    analyst.build();
    analyst.display();
    
    delete analyst;
    delete aggregator;
    for(int i = 0; i < selectorCount; i++)
    {
      delete selectors[i];
    }
    for(int i = 0; i < ArraySize(filters); i++)
    {
      delete filters[i].getSelector();
      delete filters[i];
    }
  }

Die Hilfsfunktion createSelector ist wie folgt definiert.

  Selector<TRADE_RECORD_FIELDS> *createSelector(int i)
  {
    switch(selectorArray[i])
    {
      case SELECTOR_TYPE:
        return new TypeSelector();
      case SELECTOR_SYMBOL:
        return new SymbolSelector();
      case SELECTOR_SERIAL:
        return new SerialNumberSelector();
      case SELECTOR_MAGIC:
        return new MagicSelector();
      case SELECTOR_PROFITABLE:
        return new ProfitableSelector();
      case SELECTOR_DURATION:
        return new DaysRangeSelector(15); // up to 14 days
      case SELECTOR_WEEKDAY:
        return selectorField[i] != FIELD_NONE ? new WeekDaySelector(selectorField[i]) : NULL;
      case SELECTOR_DAYHOUR:
        return selectorField[i] != FIELD_NONE ? new DayHourSelector(selectorField[i]) : NULL;
      case SELECTOR_HOURMINUTE:
        return selectorField[i] != FIELD_NONE ? new DayHourSelector(selectorField[i]) : NULL;
      case SELECTOR_SCALAR:
        return selectorField[i] != FIELD_NONE ? new TradeSelector(selectorField[i]) : NULL;
      case SELECTOR_QUANTS:
        return selectorField[i] != FIELD_NONE ? new QuantizationSelector(selectorField[i]) : NULL;
    }
    return NULL;
  }

Alle Klassen mit Ausnahme von DaysRangeSelector werden aus der Header-Datei importiert, während DaysRangeSelector im OLAPDEMO Expert Advisor wie folgt beschrieben wird:

  class DaysRangeSelector: public DateTimeSelector<TRADE_RECORD_FIELDS>
  {
    public:
      DaysRangeSelector(const int n): DateTimeSelector<TRADE_RECORD_FIELDS>(FIELD_DURATION, n)
      {
        _typename = typename(this);
      }
      
      virtual bool select(const Record *r, int &index) const override
      {
        double d = r.get(selector);
        int days = (int)(d / (60 * 60 * 24));
        index = MathMin(days, granularity - 1);
        return true;
      }
      
      virtual string getLabel(const int index) const override
      {
        return index < granularity - 1 ? ((index < 10 ? " ": "") + (string)index + "D") : ((string)index + "D+");
      }
  };

Dies ist das Beispiel für eine Implementierung eines benutzerdefinierten Selektors. Er gruppiert Handelspositionen nach ihrer Lebensdauer auf dem Markt, in Tagen.

Wenn Sie den EA auf einem beliebigen Online-Konto ausführen und 2 Selektoren, SymbolSelector und WeekDaySelector auswählen, können Sie die folgenden Ergebnisse in Protokollen erhalten:

	Analyzing account history
	Symbol number: 5
	0] FDAX
	1] XAUUSD
	2] UKBrent
	3] NQ
	4] EURUSD
	Magic number: 1
	0] 0
	Filters: no
	Selectors: 2
	SumAggregator<TRADE_RECORD_FIELDS> FIELD_PROFIT_AMOUNT [35]
	X: SymbolSelector(FIELD_SYMBOL) [5]
	Y: WeekDaySelector(FIELD_DATETIME2) [7]
	     ...
	     0.000: FDAX Monday
	     0.000: XAUUSD Monday
	   -20.400: UKBrent Monday
	     0.000: NQ Monday
	     0.000: EURUSD Monday
	     0.000: FDAX Tuesday
	     0.000: XAUUSD Tuesday
	     0.000: UKBrent Tuesday
	     0.000: NQ Tuesday
	     0.000: EURUSD Tuesday
	    23.740: FDAX Wednesday
	     4.240: XAUUSD Wednesday
	     0.000: UKBrent Wednesday
	     0.000: NQ Wednesday
	     0.000: EURUSD Wednesday
	     0.000: FDAX Thursday
	     0.000: XAUUSD Thursday
	     0.000: UKBrent Thursday
	     0.000: NQ Thursday
	     0.000: EURUSD Thursday
	     0.000: FDAX Friday
	     0.000: XAUUSD Friday
	     0.000: UKBrent Friday
	    13.900: NQ Friday
	     1.140: EURUSD Friday
	     ...

Fünf Symbole wurden auf dem Konto gehandelt. Die Größe des Hyperwürfels: 35 Zellen. Alle Kombinationen von Symbolen und Wochentagen werden zusammen mit dem entsprechenden Gewinn-/Verlustbetrag angezeigt. Bitte beachten Sie, dass WeekDaySelector eine explizite Angabe des Feldes erfordert, da jede Position zwei Daten hat, das Eröffnungsdatum (FIELD_DATETIME1) und das Schließdatum (FIELD_DATETIME2). Hier haben wir FIELD_DATETIME2 ausgewählt.

Um nicht nur die aktuelle Kontohistorie, sondern auch beliebige Handelsberichte im HTML-Format sowie CSV-Dateien mit MQL5-Signalhistorie zu analysieren, wurden Methoden aus meinem Vorgängerartikel ( Extrahieren von strukturierten Daten aus HTML-Seiten mit Hilfe von CSS-Selektoren und Wie man den Handelsverlauf mehrerer Währungen basierend auf HTML- und CSV-Berichten visualisiert) in die OLAP-Bibliothek aufgenommen. Es wurden zusätzliche Layerklassen geschrieben, um sie in OLAP zu integrieren. Insbesondere enthält die Header-Datei HTMLcube.mqh die Klasse der Handelsdatensätze HTMLTradeRecord und den HTMLReportAdapter, der vom DataAdapter geerbt wird. Die Header-Datei CSVcube.mqh enthält dementsprechend die Recordklasse CSVTradeRecord und den CSVReportAdapter. Das Lesen von HTML wird von WebDataExtractor.mqh bereitgestellt, während CSV von CSVReader.mqh gelesen wird. Die Eingabeparameter für den Berichtsdownload und die allgemeinen Prinzipien der Arbeit mit den Berichten (einschließlich der Auswahl geeigneter Symbole bei Verwendung von Präfixen und Suffixen) werden im zweiten oben genannten Artikel ausführlich beschrieben.

Hier sind die Ergebnisse der Signalanalyse (eine CSV-Datei). Wir verwendeten einen Aggregator nach dem Gewinnfaktor und eine Aufschlüsselung nach Symbolen. Die Ergebnisse werden in absteigender Reihenfolge sortiert:

	Reading csv-file ***.history.csv
	219 records transferred to 217 trades
	Symbol number: 8
	0] GBPUSD
	1] EURUSD
	2] NZDUSD
	3] USDJPY
	4] USDCAD
	5] GBPAUD
	6] AUDUSD
	7] NZDJPY
	Magic number: 1
	0] 0
	Filters: no
	Selectors: 1
	ProfitFactorAggregator<TRADE_RECORD_FIELDS> FIELD_PROFIT_AMOUNT [8]
	X: SymbolSelector(FIELD_SYMBOL) [8]
	    [value]  [title]
	[0]     inf "NZDJPY"
	[1]     inf "AUDUSD"
	[2]     inf "GBPAUD"
	[3]   7.051 "USDCAD"
	[4]   4.716 "USDJPY"
	[5]   1.979 "EURUSD"
	[6]   1.802 "NZDUSD"
	[7]   1.359 "GBPUSD"

Der Wert inf (unendlich) wird vom Quellcode erzeugt, wenn es Gewinne aber keine Verluste gibt. Wie Sie sehen können, erfolgt der Vergleich von Realwerten und deren Sortierung so, dass "Unendlich" größer ist als alle anderen tatsächlichen Zahlen.

Natürlich ist es nicht sehr bequem, die Ergebnisse der Analyse des Handelsberichts in Protokollen anzuzeigen. Eine bessere Lösung wäre die Implementierung einer Display-Schnittstelle, die den Hyperwürfel in einer visuellen grafischen Form darstellen könnte. Trotz ihrer scheinbaren Einfachheit erfordert die Aufgabe vorbereitende Schritte und eine große Menge an Routinecodierung. Deshalb werden wir das im zweiten Teil des Artikels berücksichtigen.


Schlussfolgerungen

Der Artikel beschreibt einen bekannten Ansatz für die Online-Analyse von Big Data (OLAP), der auf die Handelshistorie angewendet wird. Mit Hilfe von MQL haben wir die Basisklassen implementiert, die die Generierung eines virtuellen Hyperwürfels auf Basis ausgewählter Variablen (Selektoren) sowie die Generierung verschiedener aggregierter Werte auf deren Basis ermöglichen. Dieser Mechanismus kann auch auf Prozessoptimierungsergebnisse angewendet werden, um Handelssignale nach ausgewählten Kriterien auszuwählen und in anderen Bereichen, in denen die große Datenmenge die Verwendung von Algorithmen zur Wissensextraktion für die Entscheidungsfindung erfordert.

Beigefügte Dateien:

  • Experten/OLAP/OLAPDEMO.mq5 — ein Demo-Expert Advisor;
  • Include/OLAP/OLAPcube.mqh — die grundlegende Header-Datei mit den OLAP-Klassen;
  • Include/OLAP/PairArray.mqh — das Array der Paare [value;name] mit Unterstützung für alle Sortiervarianten;
  • Include/OLAP/HTMLcube.mqh — kombiniert OLAP mit Daten, die aus HTML-Berichten geladen wurden;
  • Include/OLAP/CSVcube.mqh — kombiniert OLAP mit Daten, die aus CSV-Dateien geladen wurden;
  • Include/MT4orders.mqh — die Bibliothek MT4orders für die Arbeit mit Aufträgen in einem einzigen Stil sowohl für МТ4 als auch für МТ5;
  • Include/Marketeer/WebDataExtractor.mqh — der HTML-Parser;
  • Include/Marketeer/empty_strings.h — die Liste der leeren HTML-Tags;
  • Include/Marketeer/HTMLcolumns.mqh — Definition der Spaltenindizes in HTML-Berichten;
  • Include/Marketeer/CSVReader.mqh — der CSV-Parser;
  • Include/Marketeer/CSVcolumns.mqh — Definition von Spaltenindizes in CSV-Berichten;
  • Include/Marketeer/IndexMap.mqh — eine zusätzliche Header-Datei, die ein Array mit einem schlüssel- und indexbasierten kombinierten Zugriff implementiert;
  • Include/Marketeer/RubbArray.mqh — eine zusätzliche Header-Datei mit dem Array "rubber";
  • Include/Marketeer/TimeMT4.mqh — eine zusätzliche Header-Datei, die Datenverarbeitungsfunktionen im Stil des MetaTrader 4 implementiert;
  • Include/Marketeer/Converter.mqh — eine zusätzliche Header-Datei zur Konvertierung von Datentypen;
  • Include/Marketeer/GroupSettings.mqh — eine zusätzliche Header-Datei, die Gruppeneinstellungen von Eingabeparametern enthält.

Übersetzt aus dem Russischen von MetaQuotes Software Corp.
Originalartikel: https://www.mql5.com/ru/articles/6602

Beigefügte Dateien |
MQLOLAP1.zip (50.46 KB)
Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil V). Klassen und Kollektionen für Handelsereignisse, Nachrichten an das Programm senden Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil V). Klassen und Kollektionen für Handelsereignisse, Nachrichten an das Programm senden

In den vorherigen Artikeln haben wir begonnen, eine große plattformübergreifende Bibliothek zu erstellen, die die Entwicklung von Programmen für MetaTrader 5 und MetaTrader 4 Plattformen vereinfacht. Im vierten Teil haben wir die Verfolgung von Handelsereignissen auf dem Konto getestet. In diesem Artikel werden wir Klassen für Handelsereignisse entwickeln und diese in der Kollektion der Ereignisse platzieren. Von dort aus werden sie an das Basisobjekt der Enginebibliothek und die Steuerelement des Chartprogramms.

Hilfen zur Auswahl und Navigation in MQL5 und MQL4: Hinzufügen von Daten zum Chart Hilfen zur Auswahl und Navigation in MQL5 und MQL4: Hinzufügen von Daten zum Chart

In diesem Artikel werden wir die Funktionen des Dienstprogramms weiter ausbauen. Diesmal werden wir die Möglichkeit hinzufügen, Daten anzuzeigen, die unseren Handel vereinfachen. Insbesondere werden wir die Höchst- und Tiefstpreise des Vortages, das Rundungsniveau, die Höchst- und Tiefstpreise des Jahres, die Startzeit der Sitzung usw. hinzufügen.

Die Entwicklung von grafischen Oberflächen auf Basis von .Net Framework und C# (Teil 2): Weitere grafische Elemente Die Entwicklung von grafischen Oberflächen auf Basis von .Net Framework und C# (Teil 2): Weitere grafische Elemente

Der Artikel ist eine Fortsetzung der vorherigen Veröffentlichung "Die Entwicklung von grafischen Oberflächen für Expert Advisors und Indikatoren auf Basis von .Net Framework und C#". Es werden neue grafische Elemente zur Erstellung von grafischen Oberflächen eingeführt.

Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil VI): Ereignisse von Netting-Konten Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil VI): Ereignisse von Netting-Konten

In den vorherigen Artikeln haben wir begonnen, eine große plattformübergreifende Bibliothek zu erstellen, die die Entwicklung von Programmen für MetaTrader 5 und MetaTrader 4 Plattformen vereinfacht. Im fünften Teil der Artikelreihe haben wir Handelsereignisklassen und die Kollektion der Ereignisse angelegt, aus der die Ereignisse an das Basisobjekt der Motorenbibliothek und die Regelprogrammkarte gesendet werden. In diesem Teil werden wir die Bibliothek für die Arbeit an Netting-Konten weiterentwickeln.