Anwendung von OLAP im Handel (Teil 3): Kursanalyse für die Entwicklung von Handelsstrategien

Stanislav Korotky | 23 April, 2020

In diesem Artikel werden wir uns weiter mit der im Handel angewandten OLAP-Technologie (On-Line Analytical Processing) befassen. In den ersten beiden Artikeln wurden allgemeine Techniken zur Erstellung von Klassen beschrieben, die die Akkumulation und Analyse von mehrdimensionalen Daten sowie die Visualisierung von Analyseergebnissen in der grafischen Oberfläche ermöglichen. Beide Artikel befassten sich mit der Verarbeitung von Handelsberichten, die auf unterschiedliche Weise empfangen wurden: vom Strategietester, aus der Online-Handelshistorie und aus HTML- und CSV-Dateien (einschließlich MQL5-Handelssignalen). OLAP kann jedoch auch in anderen Bereichen angewendet werden. Insbesondere ist OLAP eine praktische Technik zur Analyse von Kursen und zur Entwicklung von Handelsstrategien.

Einführung

Hier ist eine kurze Zusammenfassung dessen, was in früheren Artikeln besprochen wurde (falls Sie diese nicht gelesen haben, wird dringend empfohlen, mit den ersten beiden Artikeln zu beginnen). Der Kern befand sich in der Datei OLAPcube.mqh:

In den HTMLcube.mqh-Dateien wurden spezifische HTML-Berichtsbezogene Felder implementiert, definiert in denen Klassen der Handelsgeschäfte aus dem HTML-Bericht HTMLTradeRecord und dem Adapter, der sie generiert.

In ähnlicher Weise wurden CSVTradeRecord-Klassen von Handelsgeschäften aus CSV-Berichten und ein Adapter für diese CSVReportAdapter separat in der Datei CSVcube.mqh implementiert.

Um die OLAP-Integration mit MQL5-Programmen zu vereinfachen, wurde schließlich die Datei OLAPcore.mqh geschrieben. Sie enthielt die Wrapper-Klasse OLAPWrapper für die gesamte in Demonstrationsprojekten verwendete OLAP-Funktionalität.

Da die neue OLAP-Verarbeitungsaufgabe einen neuen Bereich adressiert, müssen wir eine Neuordnung des vorhandenen Codes durchführen und die Teile davon auswählen, die nicht nur für die Handelshistorie, sondern auch für Kurse oder beliebige Datenquellen üblich sind.

Neuordnung

Es wurde eine neue Datei auf der Basis von OLAPcube.mqh erstellt: OLAPCommon.mqh mit nur den Grundtypen. Erstens enthalten die entfernten Teile Enumerationen, die die angewandte Bedeutung von Datenfeldern beschreiben, z.B. SELECTORS und TRADE_RECORD_FIELDS. Auch Selektorklasse und Datensatzklassen, die sich auf den Handel beziehen, wurden ausgeschlossen. Natürlich wurden all diese Teile nicht gelöscht, sondern in eine neue Datei OLAPTrades.mqh verschoben, die für die Arbeit mit der Handelshistorie und den Berichten erstellt wurde.

Die frühere Wrapper-Klasse OLAPWrapper, die zu einer Vorlage wurde und in OLAPEngine umbenannt wurde, wurde in die Datei OLAPCommon.mqh verschoben. Enumerationen von Arbeitsfeldern werden als Parametrisierungsparameter verwendet (z.B. wird TRADE_RECORD_FIELDS für die Anpassung von Projekten aus Artikel 1 und 2 verwendet, siehe Details unten).

Die Datei OLAPTrades.mqh enthält die folgenden Typen (beschrieben in Artikel 1 und 2):

Achten Sie auf das Vorhandensein des Selektors DaysRangeSelector, der sich zu einem Standard-Selektor für die Analyse der Handelsgeschichte entwickelt hat. In früheren Versionen befand er sich in der Datei OLAPcore.mqh als Beispiel für einen nutzerdefinierten Selektor.

Eine Standardinstanz des Adapters wird am Ende der Datei erstellt:

  HistoryDataAdapter<RECORD_CLASS> _defaultHistoryAdapter;

zusammen mit der Instanz der OLAP-Engine:

  OLAPEngineTrade _defaultEngine;

Diese Objekte lassen sich bequem aus dem Client-Quellcode heraus verwenden. Der Ansatz, fertige Objekte zu präsentieren, wird auch in anderen Anwendungsbereichen (Header-Dateien) angewendet, insbesondere für die geplante Kursanalyse.

Die Dateien HTMLcube.mqh und CSVcube.mqh bleiben nahezu unverändert. Alle bisher vorhandenen Funktionen zur Handelshistorie und zur Analyse von Berichten bleiben erhalten. Ein neuer Test Expert Advisor OLAPRPRT.mq5 ist zu Demonstrationszwecken unten angefügt; er ist ein Analogon von OLAPDEMO.mq5 aus dem ersten Artikel.

Am Beispiel von OLAPTrades.mqh können Sie leicht spezialisierte Implementierungen von OLAP-Klassen für andere Datentypen erstellen.

Wir werden das Projekt durch das Hinzufügen neuer Funktionen komplizieren. Daher werden hier nicht alle Aspekte der OLAP-Integration mit einer grafischen Oberfläche berücksichtigt. In diesem Artikel werden wir uns auf die Datenanalyse ohne Bezug auf die Visualisierung konzentrieren (außerdem kann es verschiedene Visualisierungsmethoden geben). Nachdem Sie diesen Artikel gelesen haben, können Sie die aktualisierte Engine mit dem GUI-Teil aus Artikel 2 kombinieren.

Verbesserungen

Im Zusammenhang mit der Kursanalyse benötigen wir möglicherweise neue Methoden der logischen Zerlegung und Datenakkumulation. Die erforderlichen Klassen werden zu OLAPCommon.mqh hinzugefügt, da sie grundlegender Natur sind. Daher werden sie für alle Anwendungswürfel verfügbar sein, einschließlich der früheren aus OLAPTrades.mqh.

Folgendes wurde hinzugefügt:

Der MonthSelector ermöglicht die Gruppierung der Daten nach Monaten. Dieser Selektor wurde bei früheren Implementierungen irgendwie weggelassen.

  template<typename E>
  class MonthSelector: public DateTimeSelector<E>
  {
    public:
      MonthSelector(const E f): DateTimeSelector(f, 12)
      {
        _typename = typename(this);
       }
      
      virtual bool select(const Record *r, int &index) const
      {
        double d = r.get(selector);
        datetime t = (datetime)d;
        index = TimeMonth(t) - 1;
        return true;
       }
      
      virtual string getLabel(const int index) const
      {
        static string months[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};
        return months[index];
       }
  };

WorkWeekDaySelector ist ein Analogon zu WeekDaySelector, teilt die Daten jedoch nach Wochentagen (von 1 bis 5) auf. Er ist eine bequeme Lösung für die Analyse von Märkten, in denen an Wochenenden nicht gehandelt wird: Die Werte der Wochenenden sind immer Null, so dass es nicht notwendig ist, dafür Zellen im Hyperwürfel zu reservieren.

VarianceAggregator ermöglicht die Berechnung der Datenvarianz und ergänzt somit den AverageAggregator. Die Idee des neuen Aggregators kann mit dem Wert der Average True Range (ATR) verglichen werden, jedoch kann der Aggregator für beliebige Datenstichproben (z.B. getrennt nach den Stunden des Tages oder nach den Wochentagen) sowie für andere Datenquellen (z.B. Varianz der Einnahmen in der Handelshistorie) berechnet werden.

  template<typename E>
  class VarianceAggregator: public Aggregator<E>
  {
    protected:
      int counters[];
      double sumx[];
      double sumx2[];
      
    public:
      VarianceAggregator(const E f, const Selector<E> *&s[], const Filter<E> *&t[]): Aggregator(f, s, t)
      {
        _typename = typename(this);
       }
      
      virtual void setSelectorBounds(const int length = 0) override
      {
        Aggregator<E>::setSelectorBounds();
        ArrayResize(counters, ArraySize(totals));
        ArrayResize(sumx, ArraySize(totals));
        ArrayResize(sumx2, ArraySize(totals));
        ArrayInitialize(counters, 0);
        ArrayInitialize(sumx, 0);
        ArrayInitialize(sumx2, 0);
       }
  
      virtual void update(const int index, const double value) override
      {
        counters[index]++;
        sumx[index] += value;
        sumx2[index] += value * value;
        
        const int n = counters[index];
        const double variance = (sumx2[index] - sumx[index] * sumx[index] / n) / MathMax(n - 1, 1);
        totals[index] = MathSqrt(variance);
       }
  };

Abb.1 Das Diagramm der Aggregatorklassen

Abb.1 Das Diagramm der Aggregatorklassen

Die Selektoren QuantizationSelector und SerialNumberSelector sind vom BaseSelector abgeleitet und nicht von einem spezifischeren TradeSelector. Der QuantizationSelector hat einen neuen Konstruktorparameter, mit dem die Granularität des Selektors eingestellt werden kann. Er ist standardmäßig Null, was bedeutet, dass die Daten nach exakter Übereinstimmung des entsprechenden Feldwertes gruppiert werden (das Feld wird im Selektor angegeben). Im vorigen Artikel haben wir beispielsweise die Quantisierung nach Losgröße verwendet, um den Bericht über die nach Losgrößen aufgeschlüsselten Gewinne zu erhalten. Die Würfelzellen waren Lose, wie z.B. 0,01, 0,1 usw., die in der Handelshistorie enthalten waren. Manchmal ist es günstiger, mit einem bestimmten Schritt (Zellgröße) zu quantifizieren. Dieser Schritt kann mit dem neuen Konstruktorparameter angegeben werden. Die neu hinzugefügten Teile sind im untenstehenden Quelltext mit dem Kommentar + gekennzeichnet.

  template<typename T>
  class QuantizationSelector: public BaseSelector<T>
  {
    protected:
      Vocabulary<double> quants;
      uint cell;                 // +
  
    public:
      QuantizationSelector(const T field, const uint granularity = 0 /* + */): BaseSelector<T>(field), cell(granularity)
      {
        _typename = typename(this);
       }
  
      virtual void prepare(const Record *r) override
      {
        double value = r.get(selector);
        if(cell != 0) value = MathSign(value) * MathFloor(MathAbs(value) / cell) * cell; // +
        quants.add(value);
       }
      
      virtual bool select(const Record *r, int &index) const override
      {
        double value = r.get(selector);
        if(cell != 0) value = MathSign(value) * MathFloor(MathAbs(value) / cell) * cell; // +
        index = quants.get(value);
        return (index >= 0);
       }
      
      virtual int getRange() const override
      {
        return quants.size();
       }
      
      virtual string getLabel(const int index) const override
      {
        return (string)(float)quants[index];
       }
  };

Darüber hinaus wurden weitere Verbesserungen an bestehenden Klassen vorgenommen. Jetzt unterstützen die Filterklassen 'Filter' und 'FilterRange' den Vergleich anhand des Feldwerts und nicht nur anhand des Index der Zelle, zu der der Wert hinzugefügt wird. Dies ist aus Nutzersicht praktisch, da der Zellindex nicht immer im Voraus bekannt ist. Der neue Modus wird aktiviert, wenn der Selektor einen Index gleich -1 zurückgibt (die neu hinzugefügten Codezeilen sind mit + Kommentaren gekennzeichnet):

  template<typename E>
  class Filter
  {
    protected:
      Selector<E> *selector;
      double filter;
      
    public:
      Filter(Selector<E> &s, const double value): selector(&s), filter(value)
      {
       }
      
      virtual bool matches(const Record *r) const
      {
        int index;
        if(selector.select(r, index))
        {
          if(index == -1)                                             // +
          {                                                           // +
            if(dynamic_cast<FilterSelector<E> *>(selector) != NULL)   // +
            {                                                         // +
              return r.get(selector.getField()) == filter;            // +
            }                                                         // +
          }                                                           // +
          else                                                        // +
          {                                                           // +
            if(index == (int)filter) return true;
          }                                                           // +
         }
        return false;
       }
      
      Selector<E> *getSelector() const
      {
        return selector;
       }
      
      virtual string getTitle() const
      {
        return selector.getTitle() + "[" + (string)filter + "]";
       }
  };

Natürlich brauchen wir einen Selektor, der als Index -1 zurückgeben kann. Dieser wird FilterSelector genannt.

  template<typename T>
  class FilterSelector: public BaseSelector<T>
  {
    public:
      FilterSelector(const T field): BaseSelector(field)
      {
        _typename = typename(this);
       }
  
      virtual bool select(const Record *r, int &index) const override
      {
        index = -1;
        return true;
       }
      
      virtual int getRange() const override
      {
        return 0;
       }
      
      virtual double getMin() const override
      {
        return 0;
       }
      
      virtual double getMax() const override
      {
        return 0;
       }
      
      virtual string getLabel(const int index) const override
      {
        return EnumToString(selector);
       }
  };

Wie Sie sehen können, gibt der Selektor für jeden Datensatz true zurück, was bedeutet, dass der Datensatz verarbeitet werden sollte, und -1 wird als Index zurückgegeben. Anhand dieses Wertes weiß der Filter, dass der Nutzer anfordert, Daten nicht nach dem Index, sondern nach einem Feldwert zu filtern. Das Beispiel seiner Verwendung wird weiter unten besprochen.

Außerdem unterstützt die Protokollanzeige jetzt die Sortierung eines mehrdimensionalen Würfels nach Werten. Zuvor konnten mehrdimensionale Würfel nicht sortiert werden. Die Sortierung eines mehrdimensionalen Würfels steht nur teilweise zur Verfügung, d.h. sie ist nur für diejenigen Selektoren möglich, die Beschriftungen durch Zeichenketten in einer lexikographischen Reihenfolge einheitlich formatieren können. Insbesondere der neue Wochentagsselektor stellt Kennzeichnungen zur Verfügung: "1`Monday", "2`Tuesday", "3`Wednesday", "4`Thursday", "5`Friday". Die Tagesnummer am Anfang ermöglicht eine korrekte Sortierung. Andernfalls wären für eine ordnungsgemäße die Implementierung einer Labelvergleichsfunktionen erforderlich. Darüber hinaus kann es für einige der "sequentiellen" Aggregatoren, IdentityAggregator, ProgressiveTotalAggregator, erforderlich sein, Prioritäten von Würfelseiten zu setzen, da in diesen Aggregatoren die X-Achse immer die laufende Nummer des Datensatzes anzeigt, die jedoch nicht das erste Kriterium bei der Sortierung sein sollte (oder sogar als letztes Kriterium verwendet werden sollte).

Dies sind nur einige der Änderungen im Quellcode. Sie können sie alle überprüfen, indem Sie die Quelltexte vergleichen.

OLAP auf den Anwendungsbereich Kurse erweitern

Lassen Sie uns die Basisklassen aus OLAPCommon.mqh als Grundlage verwenden und eine Datei mit Kursanalyseklassen ähnlich wie OLAPTrades.mqh erstellen: OLAPKurse.mqh. Beschreiben wir zunächst die folgenden Typen:

Die Enumeration QUOTE_SELECTORS ist wie folgt definiert:

  enum QUOTE_SELECTORS
  {
    SELECTOR_NONE,       // none
    SELECTOR_SHAPE,      // type
    SELECTOR_INDEX,      // ordinal number
    /* below datetime field assumed */
    SELECTOR_MONTH,      // month-of-year
    SELECTOR_WEEKDAY,    // day-of-week
    SELECTOR_DAYHOUR,    // hour-of-day
    SELECTOR_HOURMINUTE, // minute-of-hour
    /* the next require a field as parameter */
    SELECTOR_SCALAR,     // scalar(field)
    SELECTOR_QUANTS,     // quants(field)
    SELECTOR_FILTER      // filter(field)
  };

Der Form-Selektor unterscheidet die Balken nach Typen: auf-, abwärts und neutral, je nach Richtung der Preisbewegung.

Der Index-Selektor entspricht der Klasse SerialNumberSelector, die in Basisklassen definiert ist (Datei OLAPCommon.mqh). Wenn es sich um Handelsoperationen handelt, sind dies die Seriennummern der Deals. Jetzt werden für Kurse die Balkennummern verwendet.

Der Monatsselektor wurde oben bereits besprochen. Andere Selektoren sind von früheren Artikeln übernommen worden.

Die Datenfelder in den Kursofferten werden durch die folgende Enumeration beschrieben:

  enum QUOTE_RECORD_FIELDS
  {
    FIELD_NONE,          // none
    FIELD_INDEX,         // index (bar number)
    FIELD_SHAPE,         // type (bearish/flat/bullish)
    FIELD_DATETIME,      // datetime
    FIELD_PRICE_OPEN,    // open price
    FIELD_PRICE_HIGH,    // high price
    FIELD_PRICE_LOW,     // low price
    FIELD_PRICE_CLOSE,   // close price
    FIELD_PRICE_RANGE_OC,// price range (OC)
    FIELD_PRICE_RANGE_HL,// price range (HL)
    FIELD_SPREAD,        // spread
    FIELD_TICK_VOLUME,   // tick volume
    FIELD_REAL_VOLUME,   // real volume
    FIELD_CUSTOM1,       // custom 1
    FIELD_CUSTOM2,       // custom 2
    FIELD_CUSTOM3,       // custom 3
    FIELD_CUSTOM4,       // custom 4
    QUOTE_RECORD_FIELDS_LAST
  };

Die jeweilige Bedeutung sollte aus dem Namen und des Kommentars klar ersichtlich sein.

Zwei der oben genannten Enumerationen sind als Makros implementiert:

  #define SELECTORS QUOTE_SELECTORS
  #define ENUM_FIELDS QUOTE_RECORD_FIELDS

Beachten Sie, dass ähnliche Makrodefinitionen — SELECTORS und ENUM_FIELDS — in allen eingebundenen Header-Dateien verfügbar sind. Im Moment haben wir zwei Dateien (OLAPTrades.mqh, OLAPQuotes.mqh — für die Geschichte der Handelsoperationen und für Kurse), aber es kann noch mehr solcher Dateien geben. Daher ist es jetzt in jedem Projekt, das OLAP verwendet, möglich, jeweils nur ein Anwendungsgebiet zu analysieren (z.B. entweder OLAPTrades.mqh oder OLAPQuotes.mqh, aber nicht beide gleichzeitig). Ein weitere kleine Neuordnung kann erforderlich sein, um eine Kreuzanalyse verschiedener Cubes zu ermöglichen. Dieser Teil wird in diesem Artikel nicht behandelt, da Aufgaben, die eine parallele Analyse mehrerer Metawürfel erfordern, selten zu sein scheinen. Wenn Sie dies benötigen, können Sie eine solche Neuordnung selbst durchführen.

Der übergeordnete Selektor für Zitate ist eine Spezialisierung von BaseSelector mit den Feldern QUOTE_RECORD_FIELDS:

  class QuoteSelector: public BaseSelector<QUOTE_RECORD_FIELDS>
  {
    public:
      QuoteSelector(const QUOTE_RECORD_FIELDS field): BaseSelector(field)
      {
       }
  };

Der Selektor der Balken (auf- oder abwärts) ist wie folgt implementiert:

  class ShapeSelector: public QuoteSelector
  {
    public:
      ShapeSelector(): QuoteSelector(FIELD_SHAPE)
      {
        _typename = typename(this);
       }
  
      virtual bool select(const Record *r, int &index) const
      {
        index = (int)r.get(selector);
        index += 1; // shift from -1, 0, +1 to [0..2]
        return index >= getMin() && index <= getMax();
       }
      
      virtual int getRange() const
      {
        return 3; // 0 through 2
       }
      
      virtual string getLabel(const int index) const
      {
        const static string types[3] = {"bearish", "flat", "bullish"};
        return types[index];
       }
  };

Es gibt 3 reservierte Werte, die Typen angeben: -1 für die Abwärtsbewegung, 0 für eine Seitwärtsbewegung, +1 für die Aufwärtsbewegung. Daher liegen die Zellindizes im Bereich von 0 bis 2 (einschließlich). Die untenstehende Klasse QuotesRecord demonstriert das Füllen des Feldes mit dem entsprechenden Wert, der dem Typ des spezifischen Balkens entspricht.

Abb.2 Das Diagramm der Selektorklassen

Abb.2 Das Diagramm der Selektorklassen

Hier ist die Klasse des Datensatzes, der Informationen über einen bestimmten Balken speichert:

  class QuotesRecord: public Record
  {
    protected:
      static int counter; // number of bars
      
      void fillByQuotes(const MqlRates &rate)
      {
        set(FIELD_INDEX, counter++);
        set(FIELD_SHAPE, rate.close > rate.open ? +1 : (rate.close < rate.open ? -1 : 0));
        set(FIELD_DATETIME, (double)rate.time);
        set(FIELD_PRICE_OPEN, rate.open);
        set(FIELD_PRICE_HIGH, rate.high);
        set(FIELD_PRICE_LOW, rate.low);
        set(FIELD_PRICE_CLOSE, rate.close);
        set(FIELD_PRICE_RANGE_OC, (rate.close - rate.open) / _Point);
        set(FIELD_PRICE_RANGE_HL, (rate.high - rate.low) * MathSign(rate.close - rate.open) / _Point);
        set(FIELD_SPREAD, (double)rate.spread);
        set(FIELD_TICK_VOLUME, (double)rate.tick_volume);
        set(FIELD_REAL_VOLUME, (double)rate.real_volume);
       }
    
    public:
      QuotesRecord(): Record(QUOTE_RECORD_FIELDS_LAST)
      {
       }
      
      QuotesRecord(const MqlRates &rate): Record(QUOTE_RECORD_FIELDS_LAST)
      {
        fillByQuotes(rate);
       }
      
      static int getRecordCount()
      {
        return counter;
       }
  
      static void reset()
      {
        counter = 0;
       }
  
      virtual string legend(const int index) const override
      {
        if(index >= 0 && index < QUOTE_RECORD_FIELDS_LAST)
        {
          return EnumToString((QUOTE_RECORD_FIELDS)index);
         }
        return "unknown";
       }
  };

Alle Informationen werden aus der MqlRates-Struktur übernommen. Das Erzeugen von Klasseninstanzen wird in der Adapterimplementierung weiter gezeigt.

Die Anwendung von Feldern wird in der gleichen Klasse definiert (Integer, Real, Datum). Dies ist notwendig, da alle Datensatzfelder technisch in einem Array des Typs double gespeichert sind.

  class QuotesRecord: public Record
  {
    protected:
      const static char datatypes[QUOTE_RECORD_FIELDS_LAST];
  
    public:
      ...
      static char datatype(const int index)
      {
        return datatypes[index];
       }
  };
  
  const static char QuotesRecord::datatypes[QUOTE_RECORD_FIELDS_LAST] =
  {
    0,   // none
    'i', // index, serial number
    'i', // type (-1 down/0/+1 up)
    't', // datetime
    'd', // open price
    'd', // high price
    'd', // low price
    'd', // close price
    'd', // range OC
    'd', // range HL
    'i', // spread
    'i', // tick
    'i', // real
    'd',    // custom 1
    'd',    // custom 2
    'd',    // custom 3
    'd'     // custom 4
  };

Das Vorhandensein eines solchen Flags für die Feldspezialisierung ermöglicht die Anpassung der Datenein- und -ausgabe in der Nutzeroberfläche, was weiter unten demonstriert wird.

Eine Zwischenklasse ist verfügbar, um das Ausfüllen nutzerdefinierter Felder zu ermöglichen. Ihr Hauptzweck besteht darin, fillCustomFields aus der nutzerdefinierten Klasse aufzurufen, die von der Basisklasse mit Hilfe einer Vorlage spezifiziert wird (zum Zeitpunkt des Aufrufs des Konstruktors CustomQuotesBaseRecord ist unser nutzerdefiniertes Objekt also bereits erstellt und mit Standardfeldern gefüllt, die häufig zur Berechnung nutzerdefinierter Felder benötigt werden):

  template<typename T>
  class CustomQuotesBaseRecord: public T
  {
    public:
      CustomQuotesBaseRecord(const MqlRates &rate): T(rate)
      {
        fillCustomFields();
       }
  };

Sie wird im Adapter der Kurse verwendet:

  template<typename T>
  class QuotesDataAdapter: public DataAdapter
  {
    private:
      int size;
      int cursor;
      
    public:
      QuotesDataAdapter()
      {
        reset();
       }
  
      virtual void reset() override
      {
        size = MathMin(Bars(_Symbol, _Period), TerminalInfoInteger(TERMINAL_MAXBARS));
        cursor = size - 1;
        T::reset();
       }
      
      virtual int reservedSize()
      {
        return size;
       }
      
      virtual Record *getNext()
      {
        if(cursor >= 0)
        {
          MqlRates rate[1];
          if(CopyRates(_Symbol, _Period, cursor, 1, rate) > 0)
          {
            cursor--;
            return new CustomQuotesBaseRecord<T>(rate[0]);
           }
          
          Print(__FILE__, " ", __LINE__, " ", GetLastError());
          
          return NULL;
         }
        return NULL;
       }
  };

Die Klasse durchläuft die Balken in chronologischer Reihenfolge, von den älteren zu den neueren. Dies bedeutet, dass die Indizierung (Feld FIELD_INDEX) wie bei einem regulären Array und nicht in zeitlicher Reihenfolge erfolgt.

Schließlich sieht die OLAP-Kursengine wie folgt aus:

  class OLAPEngineQuotes: public OLAPEngine<QUOTE_SELECTORS,QUOTE_RECORD_FIELDS>
  {
    protected:
      virtual Selector<QUOTE_RECORD_FIELDS> *createSelector(const QUOTE_SELECTORS selector, const QUOTE_RECORD_FIELDS field) override
      {
        switch(selector)
        {
          case SELECTOR_SHAPE:
            return new ShapeSelector();
          case SELECTOR_INDEX:
            return new SerialNumberSelector<QUOTE_RECORD_FIELDS,QuotesRecord>(FIELD_INDEX);
          case SELECTOR_MONTH:
            return new MonthSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_WEEKDAY:
            return new WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_DAYHOUR:
            return new DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_HOURMINUTE:
            return new DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_SCALAR:
            return field != FIELD_NONE ? new BaseSelector<QUOTE_RECORD_FIELDS>(field) : NULL;
          case SELECTOR_QUANTS:
            return field != FIELD_NONE ? new QuantizationSelector<QUOTE_RECORD_FIELDS>(field, QuantGranularity) : NULL;
          case SELECTOR_FILTER:
            return field != FIELD_NONE ? new FilterSelector<QUOTE_RECORD_FIELDS>(field) : NULL;
         }
        return NULL;
       }
  
      virtual void initialize() override
      {
        Print("Bars read: ", QuotesRecord::getRecordCount());
       }
  
    public:
      OLAPEngineQuotes(): OLAPEngine() {}
      OLAPEngineQuotes(DataAdapter *ptr): OLAPEngine(ptr) {}
    
  };

Alle Hauptfunktionen befinden sich immer noch in der Basisklasse OLAPEngine, die im ersten Artikel beschrieben wurde (ihr Name war OLAPWrapper). Hier müssen wir nur noch kursspezifische Selektoren erstellen.

Der Standard-Adapter und die Instanzen von OLAPEngine werden als fertige Objekte präsentiert:

  QuotesDataAdapter<RECORD_CLASS> _defaultQuotesAdapter;
  OLAPEngineQuotes _defaultEngine;

Basierend auf den erstellten Klassen für zwei Analyse-Anwendungsbereiche (OLAPTrades.mqh, OLAPQuotes.mqh) kann die OLAP-Funktionsweise leicht auf andere Zwecke erweitert werden, wie z.B. die Verarbeitung von Optimierungsergebnissen oder von Daten, die von externen Ressourcen empfangen wurden.

Abb.3 Diagramm der OLAP-Kontrollklassen

Abb.3 Diagramm der OLAP-Kontrollklassen

Der Expert Advisor für OLAP-Analyse der Kurse

Alles ist bereit, um mit der Verwendung der erstellten Klassen zu beginnen. Lassen Sie uns einen nicht-handelnden Expert Advisor OLAPQTS.mq5 entwickeln. Seine Struktur wird ähnlich der von OLAPRPRT.mq5 sein, die für die Analyse von Handelsberichten verwendet wird.

Es gibt die Klasse CustomQuotesRecord, mit der die Berechnung/Ausfüllung von nutzerdefinierten Feldern demonstriert werden kann. Sie wird von QuotesRecord ererbt. Lassen Sie uns einige nutzerdefinierte Felder verwenden, um Muster der Kurse zu bestimmen, die als Grundlage für die Konstruktion von Handelsstrategien verwendet werden können. Alle diese Felder werden in der Methode fillCustomFields ausgefüllt. Sie werden später ausführlich beschrieben.

  class CustomQuotesRecord: public QuotesRecord
  {
    public:
      CustomQuotesRecord(): QuotesRecord() {}
      CustomQuotesRecord(const MqlRates &rate): QuotesRecord(rate)
      {
       }
      
      virtual void fillCustomFields() override
      {
  
        // ...
        
       }
      
      virtual string legend(const int index) const override
      {
        // ...
        return QuotesRecord::legend(index);
       }
  };

Damit der Adapter über unsere Datensatzklasse CustomQuotesRecord "Bescheid weiß" und seine Instanzen erstellen kann, sollte das folgende Makro vor dem Einbinden von OLAPQuotes.mqh definiert werden:

  // this line plugs our class into default adapter in OLAPQuotes.mqh
  #define RECORD_CLASS CustomQuotesRecord
  
  #include <OLAP/OLAPQuotes.mqh>

Der Expert Advisor wird durch Eingabeparameter verwaltet, die denen ähnlich sind, die im Projekt zur Analyse der Handelsgeschichte verwendet werden. Die Daten können in drei Dimensionen des Metawürfels akkumuliert werden, für die es möglich ist, Selektoren entlang der X-, Y- und Z-Achse zu wählen. Es ist auch möglich, nach einem Wert oder nach einem Wertebereich zu filtern. Und schließlich sollte der Nutzer den Aggregatortyp (einige Aggregatoren erfordern die Angabe des Aggregationsfeldes, andere implizieren ein bestimmtes Feld) und optional den Sortiertyp wählen.

  sinput string X = "————— X axis —————"; // · X ·
  input SELECTORS SelectorX = DEFAULT_SELECTOR_TYPE; // · SelectorX
  input ENUM_FIELDS FieldX = DEFAULT_SELECTOR_FIELD /* field does matter only for some selectors */; // · FieldX
  
  sinput string Y = "————— Y axis —————"; // · Y ·
  input SELECTORS SelectorY = SELECTOR_NONE; // · SelectorY
  input ENUM_FIELDS FieldY = FIELD_NONE; // · FieldY
  
  sinput string Z = "————— Z axis —————"; // · Z ·
  input SELECTORS SelectorZ = SELECTOR_NONE; // · SelectorZ
  input ENUM_FIELDS FieldZ = FIELD_NONE; // · FieldZ
  
  sinput string F = "————— Filter —————"; // · F ·
  input SELECTORS _Filter1 = SELECTOR_NONE; // · Filter1
  input ENUM_FIELDS _Filter1Field = FIELD_NONE; // · Filter1Field
  input string _Filter1value1 = ""; // · Filter1value1
  input string _Filter1value2 = ""; // · Filter1value2
  
  sinput string A = "————— Aggregator —————"; // · A ·
  input AGGREGATORS _AggregatorType = DEFAULT_AGGREGATOR_TYPE; // · AggregatorType
  input ENUM_FIELDS _AggregatorField = DEFAULT_AGGREGATOR_FIELD; // · AggregatorField
  input SORT_BY _SortBy = SORT_BY_NONE; // · SortBy

Alle Selektoren und ihre Felder sind als Arrays implementiert und können leicht an die Engine übergeben werden:

  SELECTORS _selectorArray[4];
  ENUM_FIELDS _selectorField[4];
  
  int OnInit()
  {
    _selectorArray[0] = SelectorX;
    _selectorArray[1] = SelectorY;
    _selectorArray[2] = SelectorZ;
    _selectorArray[3] = _Filter1;
    _selectorField[0] = FieldX;
    _selectorField[1] = FieldY;
    _selectorField[2] = FieldZ;
    _selectorField[3] = _Filter1Field;
    
    _defaultEngine.setAdapter(&_defaultQuotesAdapter);
  
    EventSetTimer(1);
    return INIT_SUCCEEDED;
   }

Wie wir sehen können, verwendet der EA die Standardinstanzen der Engine und des Kurs-Adapters. Je nach Anwendungsspezifikation sollte der EA die Daten für die eingegebenen Parameter einmal verarbeiten. Zu diesem Zweck sowie zur Ermöglichung des Betriebs an Wochenenden, an denen es keine Ticks gibt, startet im OnInit-Handler ein Timer.

Der Verarbeitungsstart im OnTimer ist wie folgt:

  LogDisplay _display(11, _Digits);
  
  void OnTimer()
  {
    EventKillTimer();
    
    double Filter1value1 = 0, Filter1value2 = 0;
    if(CustomQuotesRecord::datatype(_Filter1Field) == 't')
    {
      Filter1value1 = (double)StringToTime(_Filter1value1);
      Filter1value2 = (double)StringToTime(_Filter1value2);
     }
    else
    {
      Filter1value1 = StringToDouble(_Filter1value1);
      Filter1value2 = StringToDouble(_Filter1value2);
     }
    
    _defaultQuotesAdapter.reset();
    _defaultEngine.process(_selectorArray, _selectorField,
          _AggregatorType, _AggregatorField,
          _display,
          _SortBy,
          Filter1value1, Filter1value2);
   }

Bei der Analyse der Kurse benötigen wir einen Zeitfilter. Daher werden die Werte für die Filter in Eingabeparametern in Form von Zeichenketten festgelegt. Je nach dem Typ des Feldes, auf das der Filter angewendet wird, werden die Zeichenketten als Zahl oder als Datum interpretiert (im üblichen Format JJJJ.MM.TT). Im ersten Artikel mussten wir immer numerische Werte eingeben, was für den Endnutzer im Falle von Datumsangaben unbequem ist.

Alle vorbereiteten Eingabeparameter werden an die Methode 'process' der OLAP-Engine übergeben. Die weitere Arbeit erfolgt ohne Nutzereingriff, woraufhin die Ergebnisse mit Hilfe einer Instanz von LogDisplay im Expertenprotokoll angezeigt werden.

Testen der OLAP-Analyse von Kursdaten

Lassen Sie uns eine einfache Kursrecherche mit der oben beschriebenen Funktionsweise durchführen.

Öffnen Sie die EURUSD D1-Tabelle und fügen Sie den OLAPQTS EA hinzu. Lassen Sie alle Parameter mit Standardwerten. Das bedeutet: der Selektor 'type' entlang der X-Achse und den Aggregator COUNT. Die folgenden Filtereinstellungen sollten geändert werden: im Parameter Filter1 stellen Sie "filter(field)" ein, in Filter1Field — datetime, in Filter1Value1 und Filter1Value2 — "2019.01.01" bzw. "2020.01.01". Somit ist der Berechnungsbereich auf das Jahr 2019 begrenzt.

Das Ergebnis der EA-Ausführung wird wie folgt aussehen:

  OLAPQTS (EURUSD,D1)	Bars read: 12626
  OLAPQTS (EURUSD,D1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [3]
  OLAPQTS (EURUSD,D1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,D1)	Selectors: 1
  OLAPQTS (EURUSD,D1)	X: ShapeSelector(FIELD_SHAPE) [3]
  OLAPQTS (EURUSD,D1)	Processed records: 259
  OLAPQTS (EURUSD,D1)	  134.00000: bearish
  OLAPQTS (EURUSD,D1)	    0.00000: flat
  OLAPQTS (EURUSD,D1)	  125.00000: bullish

Aus dem Protokoll ist ersichtlich, dass der EA 12626 Balken (die gesamte verfügbare Historie von EURUSD D1) analysiert hat, aber nur 259 davon den Filterbedingungen entsprechen. 134 von ihnen waren abwärts (bearish), 125 — aufwärts (bullish).

Durch Umschalten des Zeitrahmens auf H1 können wir die Auswertung von Ein-Stunden-Balken erhalten:

  OLAPQTS (EURUSD,H1)	Bars read: 137574
  OLAPQTS (EURUSD,H1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [3]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 1
  OLAPQTS (EURUSD,H1)	X: ShapeSelector(FIELD_SHAPE) [3]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	 3051.00000: bearish
  OLAPQTS (EURUSD,H1)	   55.00000: flat
  OLAPQTS (EURUSD,H1)	 3090.00000: bullish

Versuchen wir als Nächstes, die Spreads zu analysieren. Eine der Eigenschaften von MetaTrader ist, dass die MqlRates-Strukturen nur den minimalen Spread speichern. Beim Testen von Handelsstrategien kann ein solcher Ansatz gefährlich sein, da dies zu falsch optimistischen Gewinnschätzungen führen kann. Eine bessere Option wäre es, die Historie sowohl der minimalen als auch der maximalen Spreads zu haben. Natürlich können Sie, falls erforderlich, die Historie der Ticks verwenden, aber der Balkenmodus ist ressourceneffizienter. Lassen Sie uns versuchen, die realen Spreads stundenweise zu bewerten.

Verwenden wir das gleiche EURUSD H1-Diagramm mit dem gleichen Filter bis 2019 und fügen die folgenden EA-Einstellungen hinzu. Selektor X — "hour-of-day", Aggregator — "AVERAGE", Aggregator-Feld — "spread". Hier sind die Ergebnisse:

  OLAPQTS (EURUSD,H1)	Bars read: 137574
  OLAPQTS (EURUSD,H1)	AverageAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 1
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	    4.71984: 00
  OLAPQTS (EURUSD,H1)	    3.19066: 01
  OLAPQTS (EURUSD,H1)	    3.72763: 02
  OLAPQTS (EURUSD,H1)	    4.19455: 03
  OLAPQTS (EURUSD,H1)	    4.38132: 04
  OLAPQTS (EURUSD,H1)	    4.28794: 05
  OLAPQTS (EURUSD,H1)	    3.93050: 06
  OLAPQTS (EURUSD,H1)	    4.01158: 07
  OLAPQTS (EURUSD,H1)	    4.39768: 08
  OLAPQTS (EURUSD,H1)	    4.68340: 09
  OLAPQTS (EURUSD,H1)	    4.68340: 10
  OLAPQTS (EURUSD,H1)	    4.64479: 11
  OLAPQTS (EURUSD,H1)	    4.57915: 12
  OLAPQTS (EURUSD,H1)	    4.62934: 13
  OLAPQTS (EURUSD,H1)	    4.64865: 14
  OLAPQTS (EURUSD,H1)	    4.61390: 15
  OLAPQTS (EURUSD,H1)	    4.62162: 16
  OLAPQTS (EURUSD,H1)	    4.50579: 17
  OLAPQTS (EURUSD,H1)	    4.56757: 18
  OLAPQTS (EURUSD,H1)	    4.61004: 19
  OLAPQTS (EURUSD,H1)	    4.59459: 20
  OLAPQTS (EURUSD,H1)	    4.67054: 21
  OLAPQTS (EURUSD,H1)	    4.50775: 22
  OLAPQTS (EURUSD,H1)	    3.57312: 23

Der durchschnittliche Wert des Spread wird für jede Stunde des Tages angegeben. Es handelt sich jedoch um eine Mittelwertbildung über die Mindestspanne und daher nicht um eine echte Spanne. Um ein realistischeres Bild zu erhalten, sollten wir zum M1-Zeitrahmen wechseln. Auf diese Weise werden wir alle verfügbaren historischen Details analysieren (verfügbar ohne Verwendung von Ticks).

  OLAPQTS (EURUSD,M1)	Bars read: 1000000
  OLAPQTS (EURUSD,M1)	AverageAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,M1)	Selectors: 1
  OLAPQTS (EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,M1)	Processed records: 371475
  OLAPQTS (EURUSD,M1)	   14.05653: 00
  OLAPQTS (EURUSD,M1)	    6.63397: 01
  OLAPQTS (EURUSD,M1)	    6.00707: 02
  OLAPQTS (EURUSD,M1)	    5.72516: 03
  OLAPQTS (EURUSD,M1)	    5.72575: 04
  OLAPQTS (EURUSD,M1)	    5.77588: 05
  OLAPQTS (EURUSD,M1)	    5.82541: 06
  OLAPQTS (EURUSD,M1)	    5.82560: 07
  OLAPQTS (EURUSD,M1)	    5.77979: 08
  OLAPQTS (EURUSD,M1)	    5.44876: 09
  OLAPQTS (EURUSD,M1)	    5.32619: 10
  OLAPQTS (EURUSD,M1)	    5.32966: 11
  OLAPQTS (EURUSD,M1)	    5.32096: 12
  OLAPQTS (EURUSD,M1)	    5.32117: 13
  OLAPQTS (EURUSD,M1)	    5.29633: 14
  OLAPQTS (EURUSD,M1)	    5.21140: 15
  OLAPQTS (EURUSD,M1)	    5.17084: 16
  OLAPQTS (EURUSD,M1)	    5.12794: 17
  OLAPQTS (EURUSD,M1)	    5.27576: 18
  OLAPQTS (EURUSD,M1)	    5.48078: 19
  OLAPQTS (EURUSD,M1)	    5.60175: 20
  OLAPQTS (EURUSD,M1)	    5.70999: 21
  OLAPQTS (EURUSD,M1)	    5.87404: 22
  OLAPQTS (EURUSD,M1)	    6.94555: 23

Das Ergebnis ist näher an der Realität: In einigen Stunden hat sich der durchschnittliche Mindestspread um das 2-3fache erhöht. Um die Analyse noch rigoroser zu machen, können wir mit dem Aggregator "MAX" statt des Durchschnitts den höchsten Wert ermitteln. Obwohl die sich ergebenden Werte der höchste der Mindestwerte sein werden, sollten Sie nicht vergessen, dass sie auf Ein-Minuten-Balken innerhalb jeder Stunde basieren und daher die Ein- und Ausstiegsbedingungen beim kurzfristigen Handel perfekt beschreiben.

  OLAPQTS (EURUSD,M1)	Bars read: 1000000
  OLAPQTS (EURUSD,M1)	MaxAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,M1)	Selectors: 1
  OLAPQTS (EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,M1)	Processed records: 371475
  OLAPQTS (EURUSD,M1)	  157.00000: 00
  OLAPQTS (EURUSD,M1)	   31.00000: 01
  OLAPQTS (EURUSD,M1)	   12.00000: 02
  OLAPQTS (EURUSD,M1)	   12.00000: 03
  OLAPQTS (EURUSD,M1)	   13.00000: 04
  OLAPQTS (EURUSD,M1)	   11.00000: 05
  OLAPQTS (EURUSD,M1)	   12.00000: 06
  OLAPQTS (EURUSD,M1)	   12.00000: 07
  OLAPQTS (EURUSD,M1)	   11.00000: 08
  OLAPQTS (EURUSD,M1)	   11.00000: 09
  OLAPQTS (EURUSD,M1)	   12.00000: 10
  OLAPQTS (EURUSD,M1)	   13.00000: 11
  OLAPQTS (EURUSD,M1)	   12.00000: 12
  OLAPQTS (EURUSD,M1)	   13.00000: 13
  OLAPQTS (EURUSD,M1)	   12.00000: 14
  OLAPQTS (EURUSD,M1)	   14.00000: 15
  OLAPQTS (EURUSD,M1)	   16.00000: 16
  OLAPQTS (EURUSD,M1)	   14.00000: 17
  OLAPQTS (EURUSD,M1)	   15.00000: 18
  OLAPQTS (EURUSD,M1)	   21.00000: 19
  OLAPQTS (EURUSD,M1)	   17.00000: 20
  OLAPQTS (EURUSD,M1)	   25.00000: 21
  OLAPQTS (EURUSD,M1)	   31.00000: 22
  OLAPQTS (EURUSD,M1)	   70.00000: 23

Sehen Sie den Unterschied: Am Anfang hatten wir Spreads von 4 Punkten; jetzt sind es um Mitternacht Zehnerwerte und sogar bis fast Hundert.

Bewerten wir die Varianz der Spreads und prüfen wir, wie der neue Aggregator funktioniert. Tun wir es, indem wir "DEVIATION" wählen.

  OLAPQTS (EURUSD,M1)	Bars read: 1000000
  OLAPQTS (EURUSD,M1)	VarianceAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,M1)	Selectors: 1
  OLAPQTS (EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,M1)	Processed records: 371475
  OLAPQTS (EURUSD,M1)	    9.13767: 00
  OLAPQTS (EURUSD,M1)	    3.12974: 01
  OLAPQTS (EURUSD,M1)	    2.72293: 02
  OLAPQTS (EURUSD,M1)	    2.70965: 03
  OLAPQTS (EURUSD,M1)	    2.68758: 04
  OLAPQTS (EURUSD,M1)	    2.64350: 05
  OLAPQTS (EURUSD,M1)	    2.64158: 06
  OLAPQTS (EURUSD,M1)	    2.64934: 07
  OLAPQTS (EURUSD,M1)	    2.62854: 08
  OLAPQTS (EURUSD,M1)	    2.72117: 09
  OLAPQTS (EURUSD,M1)	    2.80259: 10
  OLAPQTS (EURUSD,M1)	    2.79681: 11
  OLAPQTS (EURUSD,M1)	    2.80850: 12
  OLAPQTS (EURUSD,M1)	    2.81435: 13
  OLAPQTS (EURUSD,M1)	    2.83489: 14
  OLAPQTS (EURUSD,M1)	    2.90745: 15
  OLAPQTS (EURUSD,M1)	    2.95804: 16
  OLAPQTS (EURUSD,M1)	    2.96799: 17
  OLAPQTS (EURUSD,M1)	    2.88021: 18
  OLAPQTS (EURUSD,M1)	    2.76605: 19
  OLAPQTS (EURUSD,M1)	    2.72036: 20
  OLAPQTS (EURUSD,M1)	    2.85615: 21
  OLAPQTS (EURUSD,M1)	    2.94224: 22
  OLAPQTS (EURUSD,M1)	    4.60560: 23

Diese Werte stellen die einzelne Standardabweichung dar, auf deren Grundlage es möglich ist, Filter in Skalierungsstrategien oder Robotern zu konfigurieren, die auf Volatilitätsimpulsen basieren.

Überprüfen wir das Ausfüllen des Feldes mit der Spanne oder Preisbewegungen auf einem Balken, die Operation der Quantisierung mit der angegebenen Zellgröße und die Sortierung.

Wechseln Sie zu diesem Zweck zurück zu EURUSD D1 und verwenden Sie bis 2019 denselben Filter. Stellen Sie auch die folgenden Parameter ein:

Wir erhalten folgende Ergebnisse:

  OLAPQTS (EURUSD,D1)	Bars read: 12627
  OLAPQTS (EURUSD,D1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [20]
  OLAPQTS (EURUSD,D1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,D1)	Selectors: 1
  OLAPQTS (EURUSD,D1)	X: QuantizationSelector<QUOTE_RECORD_FIELDS>(FIELD_PRICE_RANGE_OC) [20]
  OLAPQTS (EURUSD,D1)	Processed records: 259
  OLAPQTS (EURUSD,D1)	      [value]   [title]
  OLAPQTS (EURUSD,D1) [ 0] 72.00000 "0.0"    
  OLAPQTS (EURUSD,D1) [ 1] 27.00000 "100.0"  
  OLAPQTS (EURUSD,D1) [ 2] 24.00000 "-100.0" 
  OLAPQTS (EURUSD,D1) [ 3] 24.00000 "-200.0" 
  OLAPQTS (EURUSD,D1) [ 4] 21.00000 "200.0"  
  OLAPQTS (EURUSD,D1) [ 5] 17.00000 "-300.0" 
  OLAPQTS (EURUSD,D1) [ 6] 16.00000 "300.0"  
  OLAPQTS (EURUSD,D1) [ 7] 12.00000 "-400.0" 
  OLAPQTS (EURUSD,D1) [ 8]  8.00000 "500.0"  
  OLAPQTS (EURUSD,D1) [ 9]  8.00000 "400.0"  
  OLAPQTS (EURUSD,D1) [10]  6.00000 "-700.0" 
  OLAPQTS (EURUSD,D1) [11]  6.00000 "-500.0" 
  OLAPQTS (EURUSD,D1) [12]  6.00000 "700.0"  
  OLAPQTS (EURUSD,D1) [13]  4.00000 "-600.0" 
  OLAPQTS (EURUSD,D1) [14]  2.00000 "600.0"  
  OLAPQTS (EURUSD,D1) [15]  2.00000 "1000.0" 
  OLAPQTS (EURUSD,D1) [16]  1.00000 "-800.0" 
  OLAPQTS (EURUSD,D1) [17]  1.00000 "-1100.0"
  OLAPQTS (EURUSD,D1) [18]  1.00000 "900.0"  
  OLAPQTS (EURUSD,D1) [19]  1.00000 "-1000.0"

Erwartungsgemäß fallen die meisten Balken (72) in den Bereich Null, d.h. die Kursveränderung dieser Balken betrug nicht mehr als 100 Punkte. Die Veränderungen ±100 und ±200 Punkte gehen weiter etc.

Es handelt sich jedoch nur um die Demonstration von OLAP-Möglichkeiten bei der Analyse von Kursen. Jetzt ist es an der Zeit, zum nächsten Schritt überzugehen und Handelsstrategien mit OLAP zu erstellen.

Erstellung von Handelsstrategien auf der Grundlage der Kursanalysen mit OLAP. Teil 1

Versuchen wir herauszufinden, ob die Kurse irgendwelche Muster aufweisen, die mit verbundenen Intraday- und Intraweekzyklen verbunden sind. Wenn die vorherrschenden Preisbewegungen zu bestimmten Stunden oder an bestimmten Tagen der Woche nicht symmetrisch sind, können wir dies zur Eröffnung von Positionen nutzen. Um diese zyklischen Muster zu erkennen, müssen wir die Selektoren Stunde des Tages und Tag der Woche verwenden. Die Selektoren können nacheinander einzeln oder gleichzeitig verwendet werden, jeder auf seiner eigenen Achse. Die zweite Option ist vorzuziehen, da sie es ermöglicht, genauere Datenstichproben zu erstellen und dabei zwei Faktoren (Zyklen) gleichzeitig zu berücksichtigen. Für das Programm gibt es keinen Unterschied, welcher Selektor auf der X-Achse und welcher auf der Y-Achse eingestellt ist. Dies beeinflusst jedoch die Anzeige der Ergebnisse für den Nutzer.

Die Bereiche dieser Selektoren sind 24 (Stunden eines Tages) und 5 (Wochentage), und daher beträgt die Würfelgröße 120. Es ist auch möglich, die saisonalen zyklischen Muster innerhalb eines Jahres miteinander zu verbinden, indem man den Selektor "Monat des Jahres" entlang der Z-Achse wählt. Der Einfachheit halber werden wir jetzt mit einem zweidimensionalen Würfel arbeiten.

Die Preisänderung innerhalb des Balkens wird in zwei Feldern dargestellt: FIELD_PRICE_RANGE_OC und FIELD_PRICE_RANGE_HL. Das erste Feld gibt die Punktdifferenz zwischen dem Eröffnungs- und dem Schlusskurs an, das zweite Feld zeigt den Bereich zwischen Hoch und Tief an. Lassen Sie uns die erste als Quelle für Statistiken für potenzielle Positionen verwenden. Es sollte jetzt entschieden werden, welche Statistiken berechnet werden, d.h. welcher Aggregator verwendet werden soll.

Merkwürdigerweise kann sich hier der Aggregator ProfitFactorAggregator als nützlich erweisen. Er wurde bereits in früheren Artikeln beschrieben. Dieser Aggregator summiert positive und negative Werte des angegebenen Feldes getrennt auf und liefert deren Quotienten: Er dividiert den positiven und den negativen Wert modulo genommen. Wenn also in einer Zelle des Hyperwürfels positive Preissteigerungen vorherrschen, wird der Gewinnfaktor deutlich über 1 liegen. Wenn negative Werte vorherrschen, wird der Gewinnfaktor deutlich unter 1 liegen. Mit anderen Worten, alle Werte, die stark von 1 abweichen, weisen auf gute Bedingungen für die Eröffnung eines Kauf- oder Verkaufspositionen hin. Wenn der Gewinnfaktor über 1 liegt, können Käufe profitabel sein, während Verkäufe mit einem Gewinnfaktor unter 1 profitabler sind. Der reale Gewinnfaktor beim Verkauf ist der Kehrwert für den berechneten Wert.

Lassen Sie uns die Analyse für EURUSD H1 durchführen. Wählen Sie folgende Eingabeparameter:

Die vollständige Ergebnisliste mit 120 Zeilen ist für uns nicht interessant. Hier sind die Anfangs- und Endwerte, die die profitabelsten Kauf- und Verkaufsoptionen zeigen (sie erscheinen aufgrund der aktivierten Sortierung ganz am Anfang und ganz am Ende).

  OLAPQTS (EURUSD,H1)	Bars read: 137597
  OLAPQTS (EURUSD,H1)	ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_PRICE_RANGE_OC [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 5.85417 "00; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  1] 5.79204 "00; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  2] 5.25194 "00; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  3] 4.10104 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  4] 4.00463 "01; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [  5] 2.93725 "01; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 2.50000 "00; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  7] 2.44557 "15; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  8] 2.43496 "04; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  9] 2.36278 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [ 10] 2.33917 "04; 4`Thursday" 
  ...
  OLAPQTS (EURUSD,H1) [110] 0.49096 "09; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [111] 0.48241 "13; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [112] 0.45891 "19; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [113] 0.45807 "19; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [114] 0.44993 "14; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [115] 0.44513 "23; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [116] 0.42693 "23; 1`Monday"   
  OLAPQTS (EURUSD,H1) [117] 0.37026 "10; 1`Monday"   
  OLAPQTS (EURUSD,H1) [118] 0.34662 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [119] 0.19705 "23; 5`Friday"   

Bitte beachten Sie, dass für jeden Wert die Etiketten der beiden Dimensionen X und Y (die für die Stunde und den Wochentag verwendet werden) angezeigt werden.

Die empfangenen Werte sind nicht ganz korrekt, da sie den Spread ignorieren. Hier können nutzerdefinierte Felder verwendet werden, um das Problem zu lösen. Um beispielsweise die potenzielle Auswirkung von Spreads zu beurteilen, speichern wir im ersten nutzerdefinierten Feld den Balkenbereich minus Spread. Für das zweite Feld wird die Balkenrichtung minus Spread berechnet.

  virtual void fillCustomFields() override
  {
    const double newBarRange = get(FIELD_PRICE_RANGE_OC);
    const double spread = get(FIELD_SPREAD);

    set(FIELD_CUSTOM1, MathSign(newBarRange) * (MathAbs(newBarRange) - spread));
    set(FIELD_CUSTOM2, MathSign(newBarRange) * MathSign(MathAbs(newBarRange) - spread));
    
    // ...
   }

Wählen wir Nutzerfeld 1 als Aggregator. Hier ist das Ergebnis:

  OLAPQTS (EURUSD,H1)	Bars read: 137598
  OLAPQTS (EURUSD,H1)	ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM1 [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 6.34239 "00; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  1] 5.63981 "00; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  2] 5.15044 "00; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  3] 4.41176 "01; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [  4] 4.18052 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  5] 3.04167 "01; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 2.60000 "00; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  7] 2.53118 "15; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  8] 2.50118 "04; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  9] 2.47716 "04; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [ 10] 2.46208 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [ 11] 2.20858 "03; 5`Friday"   
  OLAPQTS (EURUSD,H1) [ 12] 2.11964 "03; 1`Monday"   
  OLAPQTS (EURUSD,H1) [ 13] 2.11123 "19; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [ 14] 2.10998 "01; 1`Monday"   
  OLAPQTS (EURUSD,H1) [ 15] 2.07638 "10; 4`Thursday"
  OLAPQTS (EURUSD,H1) [ 16] 1.95498 "09; 5`Friday"    
  ...
  OLAPQTS (EURUSD,H1) [105] 0.59029 "11; 5`Friday"   
  OLAPQTS (EURUSD,H1) [106] 0.55008 "14; 5`Friday"   
  OLAPQTS (EURUSD,H1) [107] 0.54643 "13; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [108] 0.50484 "09; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [109] 0.50000 "22; 1`Monday"   
  OLAPQTS (EURUSD,H1) [110] 0.49744 "06; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [111] 0.46686 "13; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [112] 0.44753 "19; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [113] 0.44499 "19; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [114] 0.43838 "14; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [115] 0.41290 "23; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [116] 0.39770 "23; 1`Monday"   
  OLAPQTS (EURUSD,H1) [117] 0.35586 "10; 1`Monday"   
  OLAPQTS (EURUSD,H1) [118] 0.34721 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [119] 0.18769 "23; 5`Friday"   

Die Werte bedeuten, dass aus den am Donnerstag durchgeführten Handel Gewinn erzielt werden sollte: Kauf um 0, 1 und 4 Uhr morgens und Verkauf um 19 und 23 Uhr. Am Freitag wird empfohlen, morgens um 0, 3, 4, 9 Uhr zu kaufen und um 11, 14 und 23 Uhr zu verkaufen. Ein Verkauf am Freitag um 23 Uhr kann jedoch aufgrund des baldigen Sitzungsschlusses und einer potentiell ungünstigen Lücke riskant sein (übrigens kann die Lückenanalyse auch hier mit Hilfe von nutzerdefinierten Feldern leicht automatisiert werden). In diesem Projekt wird das akzeptable Niveau des Gewinnfaktors auf 2 oder mehr gesetzt (für den Verkauf jeweils 0,5 oder weniger). In der Praxis sind die Werte in der Regel schlechter als das theoretische Ergebnis, daher sollte eine gewisse Sicherheitsmarge vorgesehen werden.

Außerdem sollte der Gewinnfaktor nicht nur durch den Balkenbereich, sondern auch durch die Anzahl der Auf- und Abwärtskerzen berechnet werden. Wählen Sie zu diesem Zweck den Balkentyp (Form) als Aggregator. Manchmal kann ein Gewinnbetrag aus einem oder zwei Balken von außergewöhnlicher Größe gebildet werden. Solche Spitzen werden deutlicher, wenn wir den Gewinnfaktor nach der Größe der Kerzenleuchter und den Gewinnfaktor nach der Anzahl der Balken in verschiedenen Richtungen vergleichen.

Im Allgemeinen müssen wir nicht unbedingt Daten über denselben Zeitraum analysieren, der im unteren Selektor durch das Datumsfeld ausgewählt ist. Dieses Mal haben wir "hour-of-day" für den H1-Zeitrahmen verwendet. Daten können in einem beliebigen Zeitrahmen analysiert werden, der kleiner oder gleich dem unteren Selektor nach Datumsfeld ist. Beispielsweise können wir eine ähnliche Analyse für M15 durchführen und die Gruppierung nach Stunden beibehalten, indem wir den Selektor "hour-of-day" verwenden. Auf diese Weise werden wir den Gewinnfaktor für 15-Minuten-Balken bestimmen. Für die aktuelle Strategie müssten wir jedoch zusätzlich den Eröffnungszeitpunkt innerhalb einer Stunde angeben. Dies kann durch eine Analyse der wahrscheinlichsten Balkenrichtung zu jeder Stunde erfolgen (d.h. nach welchen Gegenbewegungen der Hauptbalkenkörper gebildet wird). Ein Beispiel für die "Digitalisierung" von Balkenenden ist in den Kommentaren im OLAPQTS-Quellcode zu finden.

Eine visuellere Methode zur Identifizierung stabiler "Kauf-" und "Verkaufs"-Balken in der stündlichen und tageweisen Analyse ist die Verwendung von ProgressiveTotalAggregator. In diesem Fall sollte der "Ordnungszahl"-Selektor (fortlaufende Analyse aller Balken) für die X-Achse gesetzt werden, und die Selektoren "hour-of-day"- und "day-of-weeke" sollten für Y und Z gesetzt werden, sowie das vorherige Aggregationsfeld "custom 1" verwendet werden. Dies würde die tatsächlichen Handelsbilanzkurven für jeden spezifischen Ein-Stunden-Balken ergeben. Die Protokollierung und Analyse solcher Daten ist jedoch nicht bequem und daher ist diese Methode mit einer angeschlossenen grafischen Anzeige besser geeignet. Dies würde die Implementierung noch komplizierter machen, deshalb sollten wir Protokolle verwenden.

Lassen Sie uns den Expert Advisor für den Handel mit Einzelbalken erstellen, der in Übereinstimmung mit den Zyklen handelt, die mit Hilfe der OLAP-Analyse gefunden wurden. Die Hauptparameter ermöglichen die Konfiguration des geplanten Handels:

  input string BuyHours = "";
  input string SellHours = "";
  input uint ActiveDayOfWeek = 0;

Die String-Parameter BuyHours und SellHours akzeptieren Stundenlisten, in denen gekauft und verkauft werden soll. Die Stunden in jeder Liste werden durch Komma getrennt. Der Wochentag wird in ActiveDayOfWeek festgelegt (Werte von 1 für Montag bis 5 für Freitag). In der Testphase wird ein bestimmter Tag überprüft. Künftig sollte der Fachberater jedoch einen Zeitplan mit allen Wochentagen unterstützen. Wenn ActiveDayOfWeek auf 0 gesetzt ist, wird der EA an allen Tagen nach dem gleichen Zeitplan handeln. Dies erfordert jedoch eine vorläufige OLAP-Analyse mit der Variation von "Stunde des Tages", während "Wochentag" entlang Y zurückgesetzt wird. Wenn Sie möchten, können Sie diese Strategie selbst testen.

Die Einstellungen werden in OnInit gelesen und überprüft:

  int buyHours[], sellHours[];
  
  int parseHours(const string &data, int &result[])
  {
    string str[];
    const int n = StringSplit(data, ',', str);
    ArrayResize(result, n);
    for(int i = 0; i < n; i++)
    {
      result[i] = (int)StringToInteger(str[i]);
     }
    return n;
   }
  
  int OnInit()
  {
    const int trend = parseHours(BuyHours, buyHours);
    const int reverse = parseHours(SellHours, sellHours);
    
    return trend > 0 || reverse > 0 ? INIT_SUCCEEDED : INIT_PARAMETERS_INCORRECT;
   }

In OnTick() werden die Listen der Handelszeiten überprüft und die spezielle 'mode'-Variable wird auf +1 oder -1 gesetzt, wenn die aktuelle Stunde in einer von ihnen gefunden wird. Wenn die Stunde nicht gefunden wird, ist 'mode' gleich 0, was bedeutet, dass alle bestehenden Positionen geschlossen werden sollten, ohne neue Positionen zu eröffnen. Wenn es keine Aufträge gibt und 'mode' ungleich Null ist, sollte eine neue Position eröffnet werden. Wenn es eine offene Position in der gleichen Richtung gibt, wie der Zeitplan vorschlägt, bleibt die Position erhalten. Wenn die Signalrichtung entgegengesetzt zur offenen Position ist, sollte die Position umgekehrt werden. Es kann immer nur eine Position zur gleichen Zeit geöffnet sein.

  template<typename T>
  int ArrayFind(const T &array[], const T value)
  {
    const int n = ArraySize(array);
    for(int i = 0; i < n; i++)
    {
      if(array[i] == value) return i;
     }
    return -1;
   }
  
  void OnTick()
  {
    MqlTick tick;
    if(!SymbolInfoTick(_Symbol, tick)) return;
    
    const int h = TimeHour(TimeCurrent());
  
    int mode = 0;
    
    if(ArrayFind(buyHours, h) > -1)
    {
      mode = +1;
     }
    else
    if(ArrayFind(sellHours, h) > -1)
    {
      mode = -1;
     }
  
    if(ActiveDayOfWeek != 0 && ActiveDayOfWeek != _TimeDayOfWeek()) mode = 0; // skip all days except specified
  
    // pick up existing orders (if any)
    const int direction = CurrentOrderDirection();
    
    if(mode == 0)
    {
      if(direction != 0)
      {
        OrdersCloseAll();
       }
      return;
     }
    
    if(direction != 0) // there exist open orders
    {
      if(mode == direction) // keep direction
      {
        return; // existing trade goes on
       }
      OrdersCloseAll();
     }
    
    
    const int type = mode > 0 ? OP_BUY : OP_SELL;
    
    const double p = type == OP_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
      
    OrderSend(_Symbol, type, Lot, p, 100, 0, 0);
   }

Gehandelt wird nur bei Balkeneröffnung, da dies durch die Handelsstrategie festgelegt ist. Zusätzliche Funktionen ArrayFind, CurrentOrderDirection und OrdersCloseAll sind unten dargestellt. Alle diese Funktionen sowie die EA verwenden die MT4Orders Bibliothek für eine einfachere Bedienung mit der Handels-API. Darüber hinaus wird der beigefügte Code MT4Bridge/MT4Time.mqh für die Arbeit mit Daten verwendet.

  int CurrentOrderDirection(const string symbol = NULL)
  {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
      if(OrderSelect(i, SELECT_BY_POS))
      {
        if(OrderType() <= OP_SELL && (symbol == NULL || symbol == OrderSymbol()))
        {
          return OrderType() == OP_BUY ? +1 : -1;
         }
       }
     }
    return 0;
   }
  
  void OrdersCloseAll(const string symbol = NULL, const int type = -1) // OP_BUY or OP_SELL
  {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
      if(OrderSelect(i, SELECT_BY_POS))
      {
        if(OrderType() <= OP_SELL && (type == -1 || OrderType() == type) && (symbol == NULL || symbol == OrderSymbol()))
        {
          OrderClose(OrderTicket(), OrderLots(), OrderType() == OP_BUY ? SymbolInfoDouble(OrderSymbol(), SYMBOL_BID) : SymbolInfoDouble(OrderSymbol(), SYMBOL_ASK), 100);
         }
       }
     }
   }

Der vollständige Quellcode ist unten angehängt. Eines der Dinge, die in diesem Artikel übersprungen wurden, ist die theoretische Berechnung des Gewinnfaktors nach der im OLAP-Engine verwendeten Logik. Dies ermöglicht den Vergleich des theoretischen Werts mit dem praktischen Gewinnfaktor aus den Testergebnissen. Diese beiden Werte sind normalerweise ähnlich, stimmen aber nicht genau überein. Natürlich macht der theoretische Gewinnfaktor nur dann Sinn, wenn der EA so eingestellt ist, dass er nur in eine Richtung gehandelt wird, entweder Kaufen (BuyHours) oder Verkaufen (SellHours). Andernfalls überschneiden sich die beiden Modi und der theoretische PF tendiert zu 1. Auch der theoretische gewinnbringende Gewinnfaktor für Verkaufsgeschäfte wird durch Werte kleiner als 1 angegeben, da er die Umkehrung des normalen Gewinnfaktors ist. Zum Beispiel ist der theoretische Verkaufs-PF von 0,5 derselbe wie der praktische PF im Tester gleich 2. Für die Kaufrichtung sind der theoretische und der praktische PF ähnlich: Werte über 1 bedeuten Gewinn, Werte unter 1 Verlust.

Testen wir den SingleBar-EA im Jahr 2019 unter Verwendung von EURUSD H1-Daten. Legen Sie die gefundenen Handelsstundenwerte für Freitag fest:

Die Reihenfolge, in der die Stunden angegeben werden, ist nicht wichtig. Hier werden sie in absteigender Reihenfolge nach der erwarteten Rentabilität angegeben. Das Testergebnis lautet wie folgt:

Abb.4 Der Handelsbericht des SingleBar-EAs mit dem gefundenen Zeitplan von Freitagen für das Jahr 2019, EURUSD H1

Abb.4 Der Handelsbericht des SingleBar-EAs mit dem gefundenen Zeitplan von Freitagen für das Jahr 2019, EURUSD H1

Die Ergebnisse sind gut. Aber das ist nicht überraschend, denn auch für dieses Jahr wurde eine erste Analyse durchgeführt. Verschieben wir das Datum des Testbeginns auf Anfang 2018, um die Leistung der gefundenen Muster zu sehen.

Abb.5 Der Handelsbericht des SingleBar-EAs mit dem gefundenen Zeitplan von Freitagen des Jahres 2019 im Intervall von 2018-2019, EURUSD H1

Abb.5 Der Handelsbericht des SingleBar-EAs mit dem gefundenen Zeitplan von Freitagen des Jahres 2019 im Intervall von 2018-2019, EURUSD H1

Obwohl die Ergebnisse schlechter ausfallen, kann man sehen, dass die Muster seit Mitte 2018 gut funktioniert haben und daher mit Hilfe der OLAP-Analyse früher gefunden werden konnten, um sie "in der Gegenwart" zu handeln. Die Suche nach einem optimalen Analysezeitraum und die Bestimmung der Dauer der gefundenen Muster ist jedoch ein weiteres großes Thema. In gewisser Weise erfordert die OLAP-Analyse die gleiche Optimierung wie die Expert Advisors. Theoretisch ist es möglich, einen Ansatz zu implementieren, bei dem OLAP in eine EA eingebaut wird, die im Tester in verschiedenen Historienintervallen mit unterschiedlicher Länge und unterschiedlichen Startdaten ausgeführt wird; für jedes dieser Intervalle wird dann ein Vorwärtstest durchgeführt. Hierbei handelt es sich um die Cluster Walk-Forward-Technologie, die jedoch in MetaTrader nicht vollständig unterstützt wird (zum Zeitpunkt des Verfassens dieses Artikels ist nur der automatische Start von Vorwärtstest möglich, aber keine Verschiebung der Startdaten oder Größenänderung der Periode, und daher muss man sie selbst mit MQL5 oder anderen Tools, wie z.B. Shell-Skripten, implementieren).

Im Allgemeinen sollte OLAP als ein Forschungswerkzeug gesehen werden, das dabei hilft, Bereiche für eine gründlichere Analyse mit anderen Mitteln zu identifizieren, wie z.B. die traditionelle Optimierung des Expert Advisors und andere. Weiterhin werden wir sehen, wie die OLAP-Engine in einen Expert Advisor eingebaut und sofort sowohl im Tester als auch life verwendet werden kann.

Lassen Sie uns die aktuelle Handelsstrategie für ein paar weitere Tage überprüfen. Es werden hier absichtlich sowohl gute als auch schlechte Tage werden gezeigt.

Abb.6.a Der Handelsbericht des SingleBar-EAs dienstags, 2018-2019, basierend auf der Analyse von 2019, EURUSD H1

Abb.6.a Der Handelsbericht des SingleBar-EAs dienstags, 2018-2019, basierend auf der Analyse von 2019, EURUSD H1

Abb.6.b Der Handelsbericht des SingleBar-EAs mittwochs, 2018-2019, basierend auf der Analyse von 2019, EURUSD H1

Abb.6.b Der Handelsbericht des SingleBar-EAs mittwochs, 2018-2019, basierend auf der Analyse von 2019, EURUSD H1

Abb.6.c Der Handelsbericht des SingleBar-EAs donnerstags, 2018-2019, basierend auf der Analyse von 2019, EURUSD H1

Abb.6.c Der Handelsbericht des SingleBar-EAs donnerstags, 2018-2019, basierend auf der Analyse von 2019, EURUSD H1

Wie erwartet, zeigt das unklare Handelsverhalten an verschiedenen Wochentagen, dass es keine allgemeingültigen Lösungen gibt, und dass diese Lösung weiter verbessert werden muss.

Lassen Sie uns sehen, welche Handelspläne gefunden werden könnten, wenn wir die Kurse in einem längeren Zeitraum analysieren würden, zum Beispiel von 2015 bis 2019, und dann 2019 im Vorwärts-Modus handeln würden.

  OLAPQTS (EURUSD,H1)	Bars read: 137606
  OLAPQTS (EURUSD,H1)	ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM3 [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1420070400.0 ... 1546300800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 24832
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 2.04053 "01; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  1] 1.78702 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  2] 1.75055 "15; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  3] 1.71793 "00; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  4] 1.69210 "00; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  5] 1.64361 "04; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 1.63956 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  7] 1.62157 "05; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  8] 1.53032 "00; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  9] 1.49733 "16; 1`Monday"   
  OLAPQTS (EURUSD,H1) [ 10] 1.48539 "01; 5`Friday"   
  ...
  OLAPQTS (EURUSD,H1) [109] 0.74241 "16; 5`Friday"   
  OLAPQTS (EURUSD,H1) [110] 0.70346 "13; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [111] 0.68990 "23; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [112] 0.66238 "23; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [113] 0.66176 "14; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [114] 0.62968 "13; 1`Monday"   
  OLAPQTS (EURUSD,H1) [115] 0.62585 "23; 5`Friday"   
  OLAPQTS (EURUSD,H1) [116] 0.60150 "14; 5`Friday"   
  OLAPQTS (EURUSD,H1) [117] 0.55621 "11; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [118] 0.54919 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [119] 0.49804 "11; 3`Wednesday"

Wie Sie sehen können, führt eine Zunahme in der Periode zu einer Abnahme der Rentabilität jeder einzelnen Stunde. Irgendwann beginnt die Verallgemeinerung gegen die Mustersuche zu spielen. Der Mittwoch scheint der rentabelste Tag zu sein. Allerdings ist das Verhalten in der Vorwärtsperiode nicht sehr stabil. Betrachten Sie zum Beispiel die folgenden Einstellungen:

Der daraus resultierende Bericht lautet wie folgt:

Abb.7. Der Handelsbericht des SingleBar-EAs mittwochs, 2015-2020, basierend auf der Analyse von 2019, EURUSD H1

Abb.7. Der Handelsbericht des SingleBar-EAs mittwochs, 2015-2020, basierend auf der Analyse von 2019, EURUSD H1

Zur Lösung dieses Problems ist eine vielseitigere Technik erforderlich, während OLAP nur eines von mehreren erforderlichen Werkzeugen ist. Außerdem ist es sinnvoll, nach komplexeren (multifaktoriellen) Mustern zu suchen. Lassen Sie uns versuchen, eine andere Handelsstrategie zu entwickeln, die nicht nur den Zeitzyklus, sondern auch die vorherige Balkenrichtung berücksichtigt.

Erstellung von Handelsstrategien auf der Grundlage der Kursanalysen mit OLAP. Teil 2

Es kann davon ausgegangen werden, dass die Richtung jedes Balkens bis zu einem gewissen Grad von der Richtung des vorherigen Balkens abhängen kann. Diese Abhängigkeit hat höchstwahrscheinlich einen ähnlichen zyklischen Charakter in Verbindung mit den Fluktuationen innerhalb eines Tages oder einer Woche, die im vorigen Abschnitt festgestellt wurden. Mit anderen Worten, zusätzlich zur Akkumulation der Balkengrößen und -richtungen nach Stunden und Tagen der Woche in einer OLAP-Analyse ist es auch notwendig, die Eigenschaften des vorherigen Balkens irgendwie zu berücksichtigen. Lassen Sie uns dazu die übrigen nutzerdefinierten Felder verwenden.

Im dritten nutzerdefinierten Feld wird die "asymmetrische" Kovarianz von zwei benachbarten Balken berechnet. Die gewöhnliche Kovarianz, die als Produkt der Preisbewegungsbereiche innerhalb der Balken unter Berücksichtigung der Richtung (plus für Anstieg und minus für Rückgang) berechnet wird, hat keinen besonderen Vorhersagewert, da der vorherige und der nächste Balken im erhaltenen Kovarianzwert äquivalent sind. Handelsentscheidungen sind jedoch nur für den nächsten Barren effizient, obwohl sie auf der Grundlage des vorherigen Barrens getroffen werden. Mit anderen Worten, eine hohe Kovarianz aufgrund der großen Bewegungen des vorherigen Balkens war bereits geschehen, da ein solcher Balken in der Vergangenheit liegt. Deshalb werden wir versuchen, die "asymmetrische" Kovarianzformel zu verwenden, bei der nur der Bereich des nächsten Balkens berücksichtigt wird, zusammen mit dem Vorzeichen des Produkts aus der Multiplikation mit dem vorherigen Balken.

In diesem Feld können zwei Strategien getestet werden: Trend und Umkehrung. Wenn wir z.B. den Profitfaktor-Aggregator in diesem Feld verwenden, dann zeigen Werte größer als 1 an, dass der Handel in der Richtung des vorherigen Balkens profitabel ist; Werte kleiner als 1 zeigen an, dass die entgegengesetzte Richtung profitabel ist. Wie in früheren Berechnungen bedeuten Extremwerte (viel größer als 1 oder viel kleiner als 1), dass Trend- bzw. Umkehroperationen profitabler sind.

Im vierten nutzerdefinierten Feld speichern wir das Vorzeichen dafür, ob benachbarte Balken in der gleichen Richtung (+1) oder in verschiedenen Richtungen (-1) verlaufen. Auf diese Weise können wir sehr wohl die Anzahl benachbarter Umkehrbalken mit Hilfe von Aggregatoren bestimmen, ebenso wie die Effizienz der Eingaben für die Trend- und Umkehrstrategien.

Da die Balken immer in chronologischer Reihenfolge analysiert werden (diese Reihenfolge wird durch den Adapter bereitgestellt), können wir die für Berechnungen in statischen Variablen erforderliche vorherige Balkengröße und Spreizung speichern. Dies ist natürlich möglich, solange eine einzige Instanz des Kursadapters verwendet wird (seine Instanz wird standardmäßig in der Header-Datei erstellt). Dies ist für unser Beispiel geeignet und einfacher zu verstehen. Im Allgemeinen sollte der Adapter jedoch an den nutzerdefinierten Datensatzkonstruktor (wie z.B. CustomQuotesBaseRecord) und weiter an die Methode fillCustomFields einen bestimmten Container übergeben, der das Speichern und Wiederherstellen des Zustands ermöglichen würde, z.B. als Verweis auf ein Array: fillCustomFields(double &bundle[]).

  class CustomQuotesRecord: public QuotesRecord
  {
    private:
      static double previousBarRange;
      static double previousSpread;
      
    public:
      // ...
      
      virtual void fillCustomFields() override
      {
        const double newBarRange = get(FIELD_PRICE_RANGE_OC);
        const double spread = get(FIELD_SPREAD);
  
        // ...
  
        if(MathAbs(previousBarRange) > previousSpread)
        {
          double mult = newBarRange * previousBarRange;
          double value = MathSign(mult) * MathAbs(newBarRange);
  
          // this is an attempt to approximate average losses due to spreads
          value += MathSignNonZero(value) * -1 * MathMax(spread, previousSpread);
          
          set(FIELD_CUSTOM3, value);
          set(FIELD_CUSTOM4, MathSign(mult));
         }
        else
        {
          set(FIELD_CUSTOM3, 0);
          set(FIELD_CUSTOM4, 0);
         }
  
        previousBarRange = newBarRange;
        previousSpread = spread;
       }
      
  };

Die Werte der OLAPQTS-Eingaben sollten geändert werden. Die wichtigste Änderung betrifft die Auswahl von "custom 3" im AggregatorFeld. Die folgenden Parameter bleiben unverändert: Selektoren nach X und Y, Aggregatortyp (PF) und Sortierung. Auch der Datumsfilter wird geändert.

Wie wir bereits bei der Analyse der Notierungen ab 2015 gesehen haben, ist die Wahl eines längeren Zeitraums für die Systeme, die auf die Bestimmung der Zyklizität abzielen, besser geeignet — sie würde dem Selektor für den Monat des Jahres entsprechen. In unserem Beispiel, in dem wir Stunden- und Wochentagselektoren verwenden, werden wir nur das Jahr 2018 analysieren und dann einen Vorwärtstest für das Jahr 2019 durchführen.

  OLAPQTS (EURUSD,H1)	Bars read: 137642
  OLAPQTS (EURUSD,H1)	Aggregator: ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM3 [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1514764800.0 ... 1546300800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 6203
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 2.65010 "23; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  1] 2.37966 "03; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  2] 2.33875 "04; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  3] 1.96317 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  4] 1.91188 "18; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [  5] 1.89293 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 1.87159 "12; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  7] 1.78903 "15; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  8] 1.74461 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  9] 1.73821 "13; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [ 10] 1.73244 "14; 2`Tuesday"
  ...  
  OLAPQTS (EURUSD,H1) [110] 0.57331 "22; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [111] 0.51515 "07; 5`Friday"   
  OLAPQTS (EURUSD,H1) [112] 0.50202 "05; 5`Friday"   
  OLAPQTS (EURUSD,H1) [113] 0.48557 "04; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [114] 0.46313 "23; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [115] 0.44182 "00; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [116] 0.40907 "13; 1`Monday"   
  OLAPQTS (EURUSD,H1) [117] 0.38230 "10; 1`Monday"   
  OLAPQTS (EURUSD,H1) [118] 0.36296 "22; 5`Friday"   
  OLAPQTS (EURUSD,H1) [119] 0.29462 "17; 5`Friday"   

Lassen Sie uns einen weiteren Expert Advisor, NextBar, erstellen, um die im Feld "custom 3" implementierte Strategie zu testen. Mit Hilfe des EA können wir die gefundenen Handelsmöglichkeiten im Strategietester überprüfen. Die allgemeine Struktur des Expert Advisors ist ähnlich wie bei SingleBar: Es werden dieselben Parameter, Funktionen und Codefragmente verwendet. Die Handelslogik ist komplizierter, Sie können sie in der angehängten Quelldatei einsehen.

Lassen Sie uns die attraktivsten Stundenkombinationen auswählen (mit dem PF 2 und höher oder 0.5 und niedriger), zum Beispiel für Montag:

Testen wir im Zeitraum 2018.01.01-2019.05.01:

Abb.8 Der Handelsbericht des NextBar-EAs im Intervall 01.01.2018-01.05.2019 nach OLAP-Analyse für 2018, EURUSD H1

Abb.8 Der Handelsbericht des NextBar-EAs im Intervall 01.01.2018-01.05.2019 nach OLAP-Analyse für 2018, EURUSD H1

Die Strategie funktionierte auch im Januar 2019 noch erfolgreich, danach begann eine Niederlagenserie. Wir müssen irgendwie die Lebensdauer der Muster herausfinden und lernen, wie wir sie unterwegs ändern können.

Adaptiver Handel auf der Grundlage der OLAP-Analyse der Kurse

Bisher haben wir ein spezielles, nicht handelsübliches EA OLAPQTS für die OLAP-Analyse verwendet, während wir separate Hypothesen mit individuell entwickelten EAs getestet haben. Eine logischere und bequemere Lösung wäre es, eine OLAP-Engine in einen Expert Advisor einzubauen. Auf diese Weise wäre der Roboter in der Lage, automatisch Kurse mit einer gegebenen Periodizität zu analysieren und den Handelszeitplan anzupassen. Darüber hinaus können wir durch die Implementierung der Hauptparameter in den EA diese mit einer Methode optimieren, die die oben erwähnte Walk-Forward-Technik emulieren kann. Der EA heißt OLAPQRWF, was eine Abkürzung für OLAP of Quotes with Rolling Walk-Forward ist.

Die wichtigsten Eingaben des Expert Advisors:

  input int BarNumberLookBack = 2880; // BarNumberLookBack (week: 120 H1, month: 480 H1, year: 5760 H1)
  input double Threshold = 2.0; // Threshold (PF >= Threshold && PF <= 1/Threshold)
  input int Strategy = 0; // Strategy (0 - single bar, 1 - adjacent bars)

Darüber hinaus müssen wir die Häufigkeit angeben, mit der der OLAP-Würfel neu berechnet wird.

  enum UPDATEPERIOD
  {
    monthly,
    weekly
  };
  
  input UPDATEPERIOD Update = monthly;

Zusätzlich zu den Strategien können wir auch nutzerdefinierte Felder auswählen, nach denen der Aggregator berechnet wird. Die Felder 1 und 3 werden unter Berücksichtigung des Balkenbereichs berechnet (jeweils für die Strategien 0 und 1), während die Felder 2 und 4 nur die Anzahl der Balken in jeder Richtung berücksichtigen.

  enum CUSTOMFIELD
  {
    range,
    count
  };
  
  input CUSTOMFIELD CustomField = range;

Die Klasse CustomQuotesRecord wird unverändert von OLAPQTS abgeleitet. Alle zuvor zur Konfiguration von Selektoren, Filtern und Aggregatoren verwendeten Parameter werden als Konstanten oder als globale Variablen (falls sie je nach Strategie geändert werden sollen) festgelegt, ohne dass ihre Namen geändert werden.

  const SELECTORS SelectorX = SELECTOR_DAYHOUR;
  const ENUM_FIELDS FieldX = FIELD_DATETIME;
  
  const SELECTORS SelectorY = SELECTOR_WEEKDAY;
  const ENUM_FIELDS FieldY = FIELD_DATETIME;
  
  const SELECTORS SelectorZ = SELECTOR_NONE;
  const ENUM_FIELDS FieldZ = FIELD_NONE;
  
  const SELECTORS _Filter1 = SELECTOR_FILTER;
  const ENUM_FIELDS _Filter1Field = FIELD_INDEX;
        int _Filter1value1 = -1; // to be filled with index of first bar to process
  const int _Filter1value2 = -1;
  
  const AGGREGATORS _AggregatorType = AGGREGATOR_PROFITFACTOR;
        ENUM_FIELDS _AggregatorField = FIELD_CUSTOM1;
  const SORT_BY _SortBy = SORT_BY_NONE;

Bitte beachten Sie, dass die Balken mit FIELD_INDEX nicht nach Zeit, sondern nach Menge gefiltert werden. Der tatsächliche Wert für _Filter1value1 wird als Differenz zwischen der Gesamtzahl der Takte und der angegebenen BarNumberLookBack berechnet. Auf diese Weise wird der EA immer mit den letzten Balken BarNumberLookBack rechnen.

Der EA wird im Balkenmodus in OnTick handeln.

  bool freshStart = true;
  
  void OnTick()
  {
    if(!isNewBar()) return;
    
    if(Bars(_Symbol, _Period) < BarNumberLookBack) return;
    
    const int m0 = TimeMonth(iTime(_Symbol, _Period, 0));
    const int w0 = _TimeDayOfWeek();
    const int m1 = TimeMonth(iTime(_Symbol, _Period, 1));
    const int w1 = _TimeDayOfWeek();
    
    static bool success = false;
    
    if((Update == monthly && m0 != m1)
    || (Update == weekly && w0 < w1)
    || freshStart)
    {
      success = calcolap();
      freshStart = !success;
     }
  
    //...
   }

Warten Sie je nach Analysehäufigkeit, bis sich der Monat oder die Woche ändert, und führen Sie OLAP in der Funktion 'calcolap' aus.

  bool calcolap()
  {
    _Filter1value1 = Bars(_Symbol, _Period) - BarNumberLookBack;
    _AggregatorField = Strategy == 0 ? (ENUM_FIELDS)(FIELD_CUSTOM1 + CustomField) : (ENUM_FIELDS)(FIELD_CUSTOM3 + CustomField);
  
    _defaultQuotesAdapter.reset();
    const int processed =
    _defaultEngine.process(_selectorArray, _selectorField,
          _AggregatorType, _AggregatorField,
          stats,                              // custom display object
          _SortBy,
          _Filter1value1, _Filter1value2);
    
    return processed == BarNumberLookBack;
   }

Dieser Codeteil ist bereits bekannt. Einige Modifikationen betreffen die Auswahl des Aggregationsfeldes nach Eingabeparametern sowie die Einstellung des Index des ersten analysierten Balkens.

Eine weitere wichtige Änderung betrifft die Verwendung des speziellen Anzeigeobjekts (Statistik), das von der OLAP-Engine nach Durchführung der Analyse aufgerufen wird.

  class MyOLAPStats: public Display
  {
    // ...
    public:
      virtual void display(MetaCube *cube, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override
      {
        // ...
       }
  
      void trade(const double threshold, const double lots, const int strategy = 0)
      {
        // ...
       }
  };
  
  MyOLAPStats stats;

Da dieses Objekt die besten Handelszeiten aus den erhaltenen Statistiken ermitteln wird, ist es bequem, den Handel mit demselben Objekt über die reservierte Methode 'trade' zu beauftragen. Daher wird OnTick wie folgt ergänzt:

  void OnTick()
  {
    // ...

    if(success)
    {
      stats.trade(Threshold, Lot, Strategy);
     }
    else
    {
      OrdersCloseAll();
     }
   }

Betrachten wir nun die Klasse MyOLAPStats im Detail. Die Ergebnisse der OLAP-Analyse werden durch die Methoden 'display' (die wichtigste virtuelle Methode der Anzeige) und saveVector (Hilfsmethode) verarbeitet.

  #define N_HOURS   24
  #define N_DAYS     5
  #define AXIS_HOURS 0
  #define AXIS_DAYS  1
  
  class MyOLAPStats: public Display
  {
    private:
      bool filled;
      double index[][3]; // value, hour, day
      int cursor;
  
    protected:
      bool saveVector(MetaCube *cube, const int &consts[], const SORT_BY sortby = SORT_BY_NONE)
      {
        PairArray *result = NULL;
        cube.getVector(0, consts, result, sortby);
        if(CheckPointer(result) == POINTER_DYNAMIC)
        {
          const int n = ArraySize(result.array);
          
          if(n == N_HOURS)
          {
            for(int i = 0; i < n; i++)
            {
              index[cursor][0] = result.array[i].value;
              index[cursor][1] = i;
              index[cursor][2] = consts[AXIS_DAYS];
              cursor++;
             }
           }
          
          delete result;
          return n == N_HOURS;
         }
        return false;
       }
  
    public:
      virtual void display(MetaCube *cube, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override
      {
        int consts[];
        const int n = cube.getDimension();
        ArrayResize(consts, n);
        ArrayInitialize(consts, 0);
  
        filled = false;
        
        ArrayResize(index, N_HOURS * N_DAYS);
        ArrayInitialize(index, 1);
        cursor = 0;
  
        if(n == 2)
        {
          const int i = AXIS_DAYS;
          int m = cube.getDimensionRange(i); // should be 5 work days
          for(int j = 0; j < m; j++)
          {
            consts[i] = j;
            
            if(!saveVector(cube, consts, sortby)) // 24 hours (values) per current day
            {
              Print("Bad data format");
              return;
             }
            
            consts[i] = 0;
           }
          filled = true;
          ArraySort(index);
          ArrayPrint(index);
         }
        else
        {
          Print("Incorrect cube structure");
         }
       }
      
      //...
  };

In dieser Klasse wird ein zweidimensionaler Array 'index' deklariert. Er ermöglicht die Speicherung von Leistungswerten in Bezug auf den Zeitplan. Bei der Methode 'display' wird dieses Array sequentiell mit Vektoren aus einem OLAP-Würfel gefüllt. Die Hilfsfunktion saveVector kopiert Zahlen für alle 24 Stunden eines bestimmten Handelstages. Der Wert, die Stundenzahl und die Nummer des Arbeitstages werden sequentiell in die zweite Dimension von 'index' geschrieben. Die Werte befinden sich im ersten (0) Element, was das Sortieren des Arrays nach Gewinnfaktor ermöglicht. Dies ermöglicht im Grunde genommen eine bequeme Ansicht im Protokoll.

Der Handelsmodus wird auf der Grundlage der Werte des Arrays 'index' ausgewählt. Dementsprechend werden Handelsaufträge für die entsprechende Tageszeit und den entsprechenden Wochentag gesendet, deren PF über dem Schwellenwert liegt.

    void trade(const double threshold, const double lots, const int strategy = 0)
    {
      const int h = TimeHour(lastBar);
      const int w = _TimeDayOfWeek() - 1;
    
      int mode = 0;
      
      for(int i = 0; i < N_HOURS * N_DAYS; i++)
      {
        if(index[i][1] == h && index[i][2] == w)
        {
          if(index[i][0] >= threshold)
          {
            mode = +1;
            Print("+ Rule ", i);
            break;
           }
          
          if(index[i][0] <= 1.0 / threshold)
          {
            mode = -1;
            Print("- Rule ", i);
            break;
           }
         }
       }
      
      // pick up existing orders (if any)
      const int direction = CurrentOrderDirection();
      
      if(mode == 0)
      {
        if(direction != 0)
        {
          OrdersCloseAll();
         }
        return;
       }
      
      if(strategy == 0)
      {
        if(direction != 0) // there exist open orders
        {
          if(mode == direction) // keep direction
          {
            return; // existing trade goes on
           }
          OrdersCloseAll();
         }
        
        const int type = mode > 0 ? OP_BUY : OP_SELL;
        
        const double p = type == OP_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
        const double sl = StopLoss > 0 ? (type == OP_BUY ? p - StopLoss * _Point : p + StopLoss * _Point) : 0;
          
        OrderSend(_Symbol, type, Lot, p, 100, sl, 0);
       }
      // ...
     }

Hier habe ich nur eine Handelsstrategie gezeigt, deren Code im ersten Test Expert Advisor verwendet wurde. Der vollständige Quellcode ist unten angehängt.

Lassen Sie uns OLAPQRWF in der Zeitspanne zwischen 2015 und 2019 optimieren und dann einen Vorwärtstest für 2019 durchführen. Bitte beachten Sie, dass die Idee der Optimierung darin besteht, Meta-Parameter für den Handel zu finden: die Dauer der OLAP-Analyse, die Häufigkeit des Neuaufbaus von OLAP-Würfeln, die Auswahl der Strategie und des nutzerdefinierten Aggregationsfeldes. In jedem Optimierungslauf baut der EA einen OLAP-Würfel auf der Grundlage _historical data_ auf und handelt in seiner virtuellen _future_ unter Verwendung von Einstellungen aus _past_. Warum brauchen wir in diesem Fall einen Vorwärtstest? Hier hängt die Handelseffizienz direkt von den vorgegebenen Meta-Parametern ab, deshalb ist es wichtig, die Anwendbarkeit der gewählten Einstellungen im Out-of-Sample-Intervall zu überprüfen.

Lassen Sie uns alle Parameter, die die Analyse beeinflussen, mit Ausnahme der Aktualisierungsperiode optimieren (monatlich aufbewahren):

Der EA berechnet einen synthetischen nutzerdefinierten Optimierungswert, der gleich dem Produkt aus der Sharpe-Ratio und der Anzahl der Positionen ist. Auf der Grundlage dieses Wertes wird die beste Prognose mit den folgenden Eingabeparametern erstellt:

Lassen Sie uns einen separaten Test von 2015 bis 2020 durchführen und das Verhalten in der Vorwärtsperiode markieren.

Abb.9 Der Bericht des EAs OLAPQRWF vom 01.01.2015 bis 01.01.2020 nach der Optimierung des OLAP-Analysefensters für 2018 einschließlich, EURUSD H1

Abb.9 Der Bericht des EAs OLAPQRWF vom 01.01.2015 bis 01.01.2020 nach der Optimierung des OLAP-Analysefensters für 2018 einschließlich, EURUSD H1

Daraus lässt sich schließen, dass ein Expert Advisor, der automatisch einen profitablen Zeitplan festlegt, im Jahr 2019 unter Verwendung der in den Vorjahren ermittelten Aggregationsfenstergröße erfolgreich handelt. Natürlich erfordert dieses System weitere Untersuchungen und Analysen. Dennoch wird bestätigt, dass das Tool funktioniert.

Schlussfolgerung

In diesem Artikel haben wir die Funktionsweise der OLAP-Bibliothek (für die Online-Datenverarbeitung) verbessert und erweitert und ihr Bündel durch einen speziellen Adapter und funktionierende Datensatzklassen mit dem Kursbereich implementiert. Mit den beschriebenen Programmen ist es möglich, die Historie zu analysieren und Muster zu bestimmen, die einen profitablen Handel ermöglichen. In der ersten Phase, wenn man sich mit der OLAP-Analyse vertraut macht, ist es bequemer, individuelle, nicht handelnde Expert Advisors einzusetzen, die nur Quelldaten verarbeiten und verallgemeinerte Statistiken präsentieren. Außerdem ermöglichen solche EAs die Entwicklung und das Debugging von Algorithmen zur Berechnung nutzerdefinierter Felder, die die Grundelemente von Handelsstrategien (Hypothesen) enthalten. In weiteren OLAP-Studienschritten wird die Engine in neue oder bestehende Handelsroboter integriert. In diesem Fall sollte eine EA-Optimierung nicht nur allgemeine Betriebsparameter berücksichtigen, sondern auch neue Metaparameter, die mit OLAP verbunden sind und die Erfassung von Statistiken beeinflussen.

Natürlich sind OLAP-Tools kein Allheilmittel, insbesondere für unvorhersehbare Marktsituationen. Daher kann auch das nicht zu einem "gebrauchsfertigen"-Gral führen. Nichtsdestotrotz erweitert die integrierte Kursanalyse zweifellos die Möglichkeiten und ermöglicht es Händlern, nach neuen Strategien zu suchen und neue Expert Advisor zu schaffen.