トレードにおけるOLAPの適用(パート3):トレード戦略の開発の相場分析

Stanislav Korotky | 29 4月, 2020

この記事では、トレードに適用される OLAP (オンライン分析処理) テクノロジを引き続き取り扱います。 最初の2つの記事では、多次元データの蓄積と分析を可能にするクラスを作成するための一般的なテクニックの説明と、グラフィカルインターフェイスでの解析結果の視覚化を提供しました。 どちらの記事も、ストラテジーテスター、オンライントレードヒストリー、HTMLファイルとCSVファイル(MQL5トレードシグナルを含む)など、さまざまな方法で受け取ったトレードレポートの処理を扱いました。 ただし、OLAP は他の領域でも適用できます。 特に、OLAP は、クオートを分析し、トレード戦略を開発するための便利な手法です。

イントロダクション

前の記事で実装された内容の簡単な概要を次に示します (この記事を読んでいない場合は、最初の 2 つの記事から始めることを強くお勧めします)。 このコアは、以下を含む OLAPcube.mqh ファイルに含まれていました。

特定の HTML レポート関連フィールドが実装されている HTMLcube.mqh ファイルでは、HTML レポート HTMLTradeRecord と HTMLReportAdapter を生成するアダプターからのトレードのクラスが定義されています。

同様に、CSV レポートからのトレードの CSVTradeRecord クラスと、CSVReportAdapter 用のアダプターは、CSVcube.mqh ファイルに個別に実装されました。

最後に、MQL5プログラムとのOLAP統合を簡素化するために、OLAPcore.mqhファイルが書かれました。 デモ プロジェクトで使用する OLAP 関数全体の OLAPWrapper ラッパ クラスが含まれていました。

新しい OLAP 処理タスクは新しい領域を処理するため、既存のコードのリファクタリングを実行し、トレードヒストリーだけでなく、クオートやデータ ソースにも共通する部分を選択する必要があります。

Refactoring

新しいファイルが、基本タイプのみを持つ OLAPcube.mqh: OLAPCommon.mqh に基づいて作成されました。 まず、削除された部分には、データ フィールドの適用される意味を説明する列挙体 (SELECTORS や TRADE_RECORD_FIELDSなど) が含まれます。 また、トレーディングに関連するセレクタクラスとレコードクラスは除外されています。 もちろん、これらのパーツはすべては削除されませんでしたが、トレーディングヒストリーとレポートを操作するために作成された新しいファイル OLAPTrades.mqh に移動されました。

テンプレートとなり、OLAPEngine に名前が変更された前のラッパ クラス OLAPWrapper が OLAPCommon.mqh ファイルに移動されました。 タスクフィールドの列挙は、パラメータ化パラメータとして使用します (たとえば、TRADE_RECORD_FIELDS記事 1 と 2 のプロジェクトの適応に使用します。

OLAPTrades.mqh ファイルには、次のタイプが含まれています (記事 1 および 記事2 で説明しています)。

トレードヒストリーを分析するための標準セレクタとなっているDaysRangeSelectorセレクタの存在に注意してください。 以前のバージョンでは、カスタム セレクタの例として OLAPcore.mqh ファイルに配置されていました。

デフォルトのアダプター インスタンスは、ファイルのトレーリングストップに作成されます。

  HistoryDataAdapter<RECORD_CLASS> _defaultHistoryAdapter;

OLAP エンジン インスタンスと共に次の操作を行います。

  OLAPEngineTrade _defaultEngine;

このオブジェクトは、クライアントソースコードから使用するのに便利です。 準備完了オブジェクトを提示する方法は、他のアプリケーション領域 (ヘッダ ファイル) 、特に計画されたクオートアナライザーに適用されます。

HTMLcube.mqh および CSVcube.mqh ファイルはほとんど変更されません。 既存のトレーディングヒストリーとレポート分析関数はすべて保持されています。 デモンストレーション目的で新しいテストEAOLAPRPRT.mq5 が以下に添付されています。最初の記事の OLAPDEMO.mq5 の類似体です。

OLAPTrades.mqh を例として使用すると、他のデータ型に対する OLAP クラスの特殊な実装を作成できます。

新しい関数を追加することで、プロジェクトを複雑にします。 したがって、グラフィカル インターフェイスとの OLAP 統合のすべての側面はここでは考慮されません。 この記事では、視覚化を参照せずにデータ分析に焦点を当てます (さらに、異なる視覚化方法が存在する可能性があります)。 この記事を読んだ後、更新されたエンジンと記事2 の GUI 部分を組み合わせることができます。

改善

クオート分析の文脈では、論理的解剖とデータ蓄積の新しい方法が必要になる場合があります。 必須クラスは基本的な性質を持つため、OLAPCommon.mqh に追加されます。 したがって、OLAPTrades.mqh の前のキューブを含め、すべてのアプリケーション キューブで使用できます。

次の項目が追加されました。

月セレクタは、月ごとのデータグループ化を有効にします。 このセレクタは、以前の実装では何らかの形で省略されていました。

  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];
       }
  };

平日の曜日セレクタは平日のアナログですが、平日(1から5)でデータを分割します。 週末にトレードが行われない相場分析に便利なソリューションです:週末の値は常にゼロなので、ハイパーキューブセルを予約する必要はありません。

VarianceAggregator では、データ分散を計算できるため、AverageAgg の集計関数が補完されます。 新しいアグリゲータのアイデアは、Average True Range (ATR) の値と比較できますが、アグリゲータは、任意のデータ サンプル (たとえば、1 日の時間または曜日別に) および他のデータ ソース (トレードヒストリーの収益の差異など) について計算できます。

  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);
       }
  };

図1 アグリゲータークラスの図

図1 アグリゲータークラスの図

セレクタ量子化セレクタとシリアル番号セレクタは、より具体的なトレードセレクタではなく、ベースセレクタから派生します。 QuantizationSelector には、セレクタの粒度を設定できる新しいコンストラクタ パラメータがあります。 デフォルトではゼロに等しく、データは適切なフィールド値と完全に一致してグループ化されます(フィールドはセレクタで指定されます)。 たとえば、前回の記事ではロットサイズによる量子化を使用して、ロットサイズ別に分類された利益に関するレポートを取得しました。 キューブセルは、トレードヒストリーに含まれていた0.01、0.1などロットでしました。 指定したステップ (セルサイズ) でクオンタイズする方が便利な場合があります。 このステップは、new コンストラクタ パラメータを使用して指定できます。 新しく追加されたパーツには、以下のソースコードに + コメントが付いています。

  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];
       }
  };

また、既存のクラスに対して他の改良が加えられています。 Filter と FilterRange フィルタ クラスでは、値を追加するセルのインデックスだけでなく、フィールド値による比較がサポートされるようになりました。 セルインデックスが事前に知られているとは限らないので、ユーザーの観点からは便利です。 セレクタが -1 に等しいインデックスを返した場合、新しいモードが有効になります (新しく追加されたコード行は + コメントでマークされます)。

  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 + "]";
       }
  };

もちろん、-1をインデックスとして返すことができるセレクタが必要です。 FilterSelectorをコールします。

  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);
       }
  };

ご覧のとおり、セレクタは任意のレコードに対して true を返し、レコードを処理する必要があり、-1 がインデックスとして返されます。 この値に基づいて、フィルタは、ユーザーがインデックスではなくフィールド値によってデータをフィルタ処理するようにリクエストすることを認識します。 その使用例は以下に示します。

また、ログ表示では、多次元キューブを値で並べ替える関数もサポートされるようになりました。 以前は、マルチディメンション キューブを並べ替えることができませんでした。 多次元キューブの並べ替えは部分的にしか使用できません。つまり、辞書形式の順序で文字列でラベルを均一にフォーマットできるセレクターに対してのみ可能です。 特に、新しい平日のセレクタには、"1`Monday", "2`Tuesday", "3`Wednesday", "4`Thursday", "5`Friday"というラベルが表示されます。 最初の曜日番号は、適切な並べ替えを可能にします。 そうしないと、適切な実装に、ラベル比較関数が必要になります。 さらに、"sequential" アグリゲーターの一部、IdentityAggregator、ProgressiveTotalAggregators では、アグリゲータでは常にレコードのシーケンス番号が表示されるため、キューブ側の優先順位を設定する必要があります。

これらはソースコードの一部の変更に過ぎません。 ソースコードを比較することで、すべてのチェックができます。

OLAP をクオートアプリケーション領域に拡張する

OLAPCommon.mqh の基本クラスを基礎として使用し、OLAPTrades.mqh: OLAPQuotes.mqh のような引用分析クラスを持つファイルを作成してみましょう。 まず、次の型について説明します。

QUOTE_SELECTORS列挙体は、次のように定義されます。

  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)
  };

シェイプセレクタは、価格の動きの方向に応じて、強気、弱気、ニュートラルの足をタイプ別に区別します。

インデックス セレクタは、基本クラス (ファイル OLAPCommon.mqh) で定義されているシリアル番号セレクタ クラスに対応します。 トレード操作を扱う場合、トレードのシリアル番号でしました。 これで、足番号がクオートに使用します。

月セレクタは上述しました。 他のセレクタは、以前の記事から継承されます。

クオートのデータ フィールドは、次の列挙体で説明されます。

  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
  };

それぞれの目的は、名前とコメントから明確にする必要があります。

上記の 2 つの列挙体はマクロとして実装されます。

  #define SELECTORS QUOTE_SELECTORS
  #define ENUM_FIELDS QUOTE_RECORD_FIELDS

同様のマクロ定義である SELECTORS と ENUM_FIELDSは、すべての "適用" ヘッダ ファイルで使用できます。 今のところ、トレード操作のヒストリーと相場の 2 つのファイル (OLAPTrades.mqh、 OLAPQuotes.mqh) がありますが、そのようなファイルがもっと存在する可能性があります。 したがって、OLAP を使用するすべてのプロジェクトでは、一度に 1 つのアプリケーション領域のみを分析できるようになりました (たとえば、OLAPTrades.mqh または OLAPQuotes.mqh のいずれか 1 つは一度に両方ではありません)。 別の小さなリファクタリングを使用して、異なるキューブのクロス分析を可能にすることができます。 この記事では、複数のメタキューブの並列分析を必要とするタスクはまれに見えるため、この記事では説明しません。 この操作が必要な場合は、このようなリファクタリングを自分で実行できます。

クオートの親セレクタは、フィールドQUOTE_RECORD_FIELDSを持つ BaseSelector の特殊化です。

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

足タイプセレクタ(強気または弱気)は次のように実装されます。

  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];
       }
  };

タイプを示す予約値は、下方移動の場合は -1、レンジの場合は 0、上向きの動きには +1 の 3 つの値があります。 したがって、セルインデックスは 0 ~ 2 の範囲 (両端を含む) です。 以下の QuotesRecordクラスは、特定の足の種類に対応する関連する値をフィールドにインプットする方法を示します。

図2 セレクタクラスの図

図2 セレクタクラスの図

特定の足に関する情報を格納するレコードのクラスを次に示します。

  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";
       }
  };

すべての情報は MqlRates 構造体から受信されます。 クラス インスタンスの作成は、アダプタの実装でさらに詳しく説明します。

フィールドの適用は、同じクラス (integer, real, date)で定義されます。 すべてのレコードフィールドは技術的にはダブルタイプの配列に格納されているため、必要です。

  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
  };

フィールド特化のこのようなフラグの存在は、以下に示すユーザーインターフェイスでデータの入出力を調整することができます。

中間クラスを使用して、ユーザー設定フィールドのインプットを有効にできます。 その主な目的は、テンプレートを使用して基本的なもので指定されたカスタムクラスから fillCustomFields を呼び出すことです (したがって、CustomQuotesBaseRecord コンストラクタ呼び出しの時点で、カスタムオブジェクトはすでに作成され、標準フィールドが埋め込まれています)。

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

クオートアダプターで使用します。

  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;
       }
  };

このクラスは、古いものから新しいものまで、時系列で足を通過します。 つまり、インデックス作成 (FIELD_INDEX フィールド) は、時系列のオーダーではなく、通常の配列で実行されます。

最後に、OLAP クオートエンジンは次のようになります。

  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) {}
    
  };

すべての主要な関数は、最初の記事で説明された OLAPEngine 基本クラスで引き続き使用できます (名前は OLAPWrapper です)。 ここでは、クオート固有のセレクタを作成するだけです。

デフォルトのアダプタインスタンスと OLAP エンジン インスタンスは、既製のオブジェクトとして表示されます。

  QuotesDataAdapter<RECORD_CLASS> _defaultQuotesAdapter;
  OLAPEngineQuotes _defaultEngine;

OLAPTrades.mqh、OLAPQuotes.mqh の 2 つの分析アプリケーション領域用に作成されたクラスに基づいて、OLAP 関数は、最適化結果の処理や外部リソースから受信したデータの処理など、他の目的に拡張できます。

図3 OLAP コントロール クラスの図

図3 OLAP コントロール クラスの図

クオート分析のEA

作成されたクラスを使用して開始する準備は完了です。 非トレードEAのOLAPQTS.mq5を開発してみましょう。 その構造は、トレーディングレポートの分析に使用する OLAPRPRT.mq5 の構造と似ています。

カスタムフィールドの計算/インプットを示すことができるCustomQuotesRecordクラスがあります。 QuotesRecordから継承されます。 カスタムフィールドを使用して、トレード戦略の構築の基礎として使用できるクオートのパターンを決定してみましょう。 このようなフィールドはすべて、fillCustomFields メソッドにインプットされます。 後で詳しく説明します。

  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);
       }
  };

レコード クラス CustomQuotesRecord に関するアダプタとインスタンスの作成については、OLAPQuotes.mqh をインクルードする前に次のマクロを定義する必要があります。

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

このEAは、トレーディングヒストリー分析プロジェクトで使用するものと同様のインプットパラメータを使用して管理されます。 データは 3 つのメタキューブ次元に蓄積され、X、Y、Z 軸に沿ってセレクタを選択できます。 1 つの値または値の範囲でフィルタ処理することもできます。 最後に、ユーザーはアグリゲータータイプ (集計フィールドの指定を必要とするアグリゲーター、特定のフィールドを意味するアグリゲーター)、およびオプションでソートタイプを選択する必要があります。

  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

すべてのセレクタとそのフィールドは配列として実装され、エンジンに渡すことができます。

  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;
   }

ご覧のとおり、EAはエンジンとクオートアダプタのデフォルト インスタンスを使用します。 アプリケーションの詳細に従って、EAはインプットされたパラメータに対して 1 回データを処理する必要があります。 このため、ティックがない週末に操作を有効にするとともに、OnInit ハンドラでタイマーが開始されます。

OnTimer での処理の開始は次のとおりです。

  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);
   }

クオートを分析する場合は、日付によるフィルタが必要になります。 したがって、フィルタの値は、インプットパラメータで文字列の形式で設定されます。 フィルタが適用されるフィールドのタイプに応じて、文字列は数値または日付として解釈されます (YYYY.MM.DD)。 最初の記事では、常に数値をインプットする必要がありましたが、日付の場合にはエンドユーザーにとって不便です。

準備されたすべてのインプットパラメータは、OLAPエンジンの'プロセス'メソッドに渡されます。 ユーザーの介入なしにさらにタスクが行われ、その後、結果は LogDisplay インスタンスを使用してエキスパートログに表示されます。

クオートの OLAP 分析のテスト

上記の関数を使用して簡単な引用調査を行います。

EURUSD D1 チャートを開き、 OLAPQTSEAをアタッチします。 すべてのパラメータはデフォルト値のままにします。 つまり、X 軸と COUNT アグリゲータに沿って 'type' セレクタを使用します。 Filter1 パラメーターで "filter(フィールド)"を設定する必要があります。Filter1 パラメーターで、それぞれ設定 "filter(フィールド)"、フィルター1Field — 日時、フィルタ1値1とフィルタ1値2 - "2019.01.01"と"2020.01.01"を設定します。 したがって、計算範囲は 2019 年に制限されます。

EA実行結果は次のようになります。

  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

EAが12626足(EURUSD D1の利用可能なヒストリー全体)を分析しましたが、そのうちの259だけがフィルタ条件に一致したことをログから見ることができます。 そのうちの134は弱気で、125 は強気でした。

時間枠をH1に切り替えることで、1時間の足の評価を得ることができます:

  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

次に、スプレッドを分析してみましょう。 メタトレーダーの特徴の1つは、MqlRates構造体が最小スプレッドのみを格納することです。 トレード戦略をテストする場合、このようなアプローチは、誤って楽観的な利益クオートを与える可能性があるため、危険な場合があります。 より良い選択肢は、最小と最大のスプレッドの両方のヒストリーがあるでしょう。 もちろん、必要に応じてティックのヒストリーを使用できますが、バーモードの方がリソース効率が高くなります。 実際のスプレッドを時間単位で評価してみましょう。

2019年までに同じフィルタで同じEURUSD H1チャートを使用し、次のEA設定を追加してみましょう。 Selector X — "hour-of-day", aggregator — "AVERAGE", aggregator field — "spread". 結果は次のとおりです。

  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

平均スプレッド値は、1 日の各時間に対して指定されます。 しかし、最小スプレッドによって平均化されているので、本当の広がりではありません。 よりリアルな、M1時間枠に切り替えてみましょう。 したがって、利用可能なすべてのヒストリー詳細を分析します(ティックを使用せずに利用可能)。

  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

結果は現実に近いです:時間で平均最小スプレッドが2〜3倍増加しました。 分析をさらに厳密にするために、"MAX" アグリゲータを使用して平均値の代わりに高値を作成できます。 結果の値は最小値の中で最も高いものになりますが、1時間ごとに1分の足に基づいているため、短期トレード中のインプット条件と決済条件を完全にうまく記述することを忘れないでください。

  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

差を参照してください:最初に4ポイントのスプレッドがありましたが、真夜中に10や100になります。

スプレッドの分散を評価し、新しいアグリゲータがどのように機能するかを確認しましょう。 "偏差"を選択してを行いましょう。

  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

これらの値は、単一標準偏差を表し、スキャルピング戦略またはボラティリティインパルスに基づくロボットでフィルタを設定することが可能です。

では、足上の範囲や価格の動き、指定されたセルサイズとの量子化の操作とソートでフィールドの塗りつぶしを確認してみましょう。

このために、EURUSD D1 に切り替えて、2019 年までに同じフィルタを使用します。 また、次のパラメータを設定します。

次の結果を取得します。

  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"

予想通り、足(72)のほとんどはゼロレンジ、すなわち足での価格変化は100ポイントを超えませんでした。 ±100ポイントと±200ポイントの変更はさらに進みます。

ただし、クオートの分析における OLAP の可能性のデモンストレーションに過ぎません。 次のステップに進み、OLAPを使ってトレーディング戦略を作成します。

クオートの OLAP 分析に基づくトレード戦略の構築 パート1

クオートが日中および週内サイクルに関連付けられたパターンがあるかどうかを調べてみましょう。 優勢な価格の動きがある時間または週の日に対称でない場合は、オープンディールを使用してトレードを開くことができます。 この周期パターンを検出するには、時間セレクタと曜日セレクタを使用する必要があります。 セレクタは、それぞれ独自の軸で、1 つずつ、または同時に連続して使用できます。 2 番目のオプションは、一度に 2 つの要因 (サイクル) を考慮して、より正確なデータ サンプルを構築できるため、より適しています。 プログラムでは、どのセレクタが X 軸に設定され、どのセレクタが Y に設定されているかに差はありません。ただし、ユーザーに対する結果の表示に影響します。

セレクタの範囲は 24 (1 日の時間) と 5 (平日) であるため、キューブサイズは 120 です。 Z 軸に沿って "年の月" セレクタを選択することで、1 年以内に季節周期パターンを接続することもできます。 シンプルにするために、ここでは 2 次元キューブを使用します。

足内の価格変更は、FIELD_PRICE_RANGE_OCとFIELD_PRICE_RANGE_HLの2つのフィールドで表示されます。 最初のは、始値と終値の間のポイント差を提供し、2番目は、高値と安値の間の範囲を示します。 最初のトレードを潜在的なトレードの統計のソースとして使用しましょう。 どの統計が計算されるか、つまりどのアグリゲータを適用すべきかを決定する必要があります。

奇妙なことに、ProfitFactorAggのアグリゲーターは、ここで便利かもしれません。 これは以前の記事ですでに説明されています。 このアグリゲータは、指定されたフィールドの正と負の値を別々に合計し、その商を返します: 剰余を取り出した正の値と負の値を除算します。 したがって、一部のハイパーキューブセルでプラスの価格増分が優勢な場合、利益係数は1をはるかに上回ります。 負の値が優先される場合、利益率は1を大幅に下回ります。 言い換えれば、1と大きく異なる値はすべて、ロングトレードまたはショートトレードを開くための良好な条件を示します。 利益率が 1 を超える場合、買いトレードは利益を上げることができ、売りは 1 未満の利益係数でより収益性が高くなります。 実際の売り上げの利益係数は、計算値の逆になります。

EURUSD H1 で分析を実行してみましょう。 インプットパラメータを選択します。

120行の結果は興味深いものではありません。 ここでは、最も収益性の高い売買オプションを示す初期値と最終値を示します (ソートが有効なため、最初と最後に表示されます)。

  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"   

値ごとに、2 次元 X と Y (時間と曜日に使用する) のラベルが表示されます。

スプレッドを無視するため、受信した値は完全に正しいわけではありません。 ここでは、カスタム フィールドを使用して問題を解決できます。 たとえば、スプレッドの潜在的な効果を評価するには、最初のカスタムフィールドに足範囲からスプレッドを引いた値を保存します。 2 番目のフィールドでは、足の方向からスプレッドを引いた値が計算されます。

  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));
    
    // ..
   }

アグリゲータとしてカスタム フィールド 1 を選択します。 結果は次のとおりです。

  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"   

この値は、木曜日に行われたトレード操作から利益を得るべきであることを意味します:午前0時、1時、4時に買い、午後7時と11時に売ります。 (19と23)。 金曜日には、午前0時、3時、4時、9時に買いし、11、14、23で売ることをお勧めします。 しかし、金曜日の23時で売ることは、間もなく相場が終了し、潜在的な不利なギャップに陥る可能性があります(ところで、ギャップ分析はカスタムフィールドを使用してここで自動化することもできます)。 このプロジェクトでは、許容利益率のレベルが 2 以上に設定されます (それぞれ 0.5 以下を売りする場合)。 実際には、値は通常、理論的な結果よりも悪いので、一定の安全マージンを提供する必要があります。

また、プロフィットファクターは、足の範囲だけでなく、強気と弱気のローソク足の数によって計算されるべきです。 このために、アグリゲータとして足タイプ (フォーム) を選択します。 時には、利益額は、異常なサイズの1つまたは2つのローソク足で形成することができます。 ローソク足のサイズと利益率を異なる方向の足数で比較すると、このようなスパイクがより顕著になります。

一般的に言えば、日付フィールドによって下位セレクタで選択された同じ時間枠でデータを分析する必要は必ずしもありません。 今回は、H1時間枠で "時間" を使用しました。 データは、日付フィールドによって下位セレクタより低い、または等しい任意の時間枠で分析できます。 たとえば、M15 で同様の分析を実行し、"時間" セレクタを使用して時間別にグループ化を保持できます。 この方法では、15分の足の利益率を決定します。 ただし、現在の戦略では、1 時間以内にインプットの瞬間を追加で指定する必要があります。 各時間(すなわち、カウンタムーブメントがメイン足本体を形成した後)で最も可能性の高いローソク足の形成方法を分析することによって行うことができます。 OLAPQTS ソース コードのコメントでは、足のトレーリングストップの "デジタル化" の例を使用できます。

時間単位および日々の分析で安定した "買い" と "売り" 足を識別するより視覚的な方法は、ProgressiveTotalAggregator を使用することです。 この場合、X 軸 X に "序数"セレクタ(すべての足の連続分析)を設定し、Y と Z に "時間" セレクタと "日" セレクタを設定する必要があります。 各特定の1時間の足の実際のトレードバランス曲線を生成します。 しかし、このようなデータのロギングと分析は便利ではなく、接続されたグラフィカル表示に適します。 より実装がさらに複雑になり、ログを使用してみましょう。

OLAP分析を使用して見つかったサイクルに従ってトレードを実行するシングル足トレードEAを作成してみましょう。 主なパラメータは、スケジュールされたトレードを設定することができます。

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

文字列パラメータ BuyHours と SellHours は、売買トレードをそれぞれ開く時間のリストを受け入れます。 各リストの時間はカンマで区切られます。 平日は、ActiveDayOfWeek (月曜日の 1 から金曜日の 5 の値) で設定されます。 テスト段階では、特定の日がチェックされます。 ただし、将来的には、このEAは、すべての曜日のスケジュールをサポートする必要があります。 ActiveDayOfWeek が 0 に設定されている場合、EAは同じスケジュールを使用してすべての日でトレードします。 ただし、この場合は、"時間" のバリエーションを持つ予備的な OLAP 分析が必要ですが、Y に沿って "曜日" をリセットします。必要に応じて、この戦略を自分でテストできます。

設定は OnInit で読み取られ、チェックされます。

  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;
   }

OnTickハンドラでは、トレード時間のリストがチェックされ、現在の時間が見つかった場合は、特別な'mode'変数が+1または-1に設定されます。 時間が見つからない場合は、'mode' は 0 です。 オーダーがなく、'mode'がゼロに等しくない場合は、新しいポジションを開く必要があります。 スケジュールが示すのと同じ方向に開いているポジションがある場合、そのポジションは保持されます。 シグナルの方向が開いたポジションと反対の場合、ポジションは反転する必要があります。 一度に開くことができるポジションは 1 つだけです。

  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);
   }

取引は取引戦略によって設定されるため、バーのオープニングでのみ行われます。 追加関数 ArrayFind、CurrentOrderDirection、およびOrdersCloseAllを次に示します。 EAと同様に、すべての関数は、トレーディング API を使用して操作するために MetaTrader4Orders ライブラリを使用します。 さらに、アタッチされた MT4Bridge/MT4Time.mqhコードは日付を処理するために使用します。

  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);
         }
       }
     }
   }

完全なソースコードは以下に添付されています。 この記事でスキップされた事項の 1 つは、OLAP エンジンで使用するロジックに従って利益率を計算する理論です。 これより、理論値とテスト結果の実際的な利益係数を比較できます。 2 つの値は、通常は似ていますが、正確には一致しません。 もちろん、理論的利益係数は、EAが買い(BuyHours)または売り(SellHours)のいずれか一方向でのみトレードするように設定されている場合にのみ意味をなします。 そうでない場合、2 つのモードが重なり、理論 PF が 1 に設定されます。 また、売り取りトレードの理論上の利益利益率は、通常の利益係数の逆であるため、1未満の値で示されます。 例えば、理論売りPF 0.5は、テスターの実用PFと同じ2に等しい。 買い方向の場合、理論的および実用的なPFは似ています:1を超える値は利益を意味し、1未満の値は損失を意味します。

EURUSD H1 データを使用して、2019 年にシングル足EAをテストしてみましょう。 金曜日の見つかったトレード時間の値を設定します。

時間を指定するオーダーは重要ではありません。 ここでは、期待される収益性によって降順で指定されます。 テスト結果は次のとおりです。

図4 シングル足EAトレードのレポート 2019年の金曜日のスケジュール, EURUSD H1

図4 シングル足EAトレードのレポート 2019年の金曜日のスケジュール, EURUSD H1

この結果は良いです。 しかし、今年の最初の分析も行われたので、驚くべきことではありません。 テストの開始日を 2018 年の初めにシフトして、見つかったパターンのパフォーマンスを確認してみましょう。

図5 シングル足EAトレードのレポート 2018-2019年の間隔で2019年の金曜日のスケジュール, EURUSD H1

図5 シングル足EAトレードのレポート 2018-2019年の間隔で2019年の金曜日のスケジュール, EURUSD H1

結果はさらに悪いが、パターンは2018年半ばからうまく機能し、したがって、現在の未来に"をトレードするためにOLAP分析を使用して以前に見つけることができることがわかります。 しかし、最適な分析期間を検索し、見つかったパターンの期間を決定することはもう一つの大きなトピックです。 ある意味では、OLAP 分析にはEAと同じ最適化が必要です。 理論的には、異なる長さと異なる開始日の異なるヒストリー間隔でテスターで実行されるEAに OLAP を組み込むアプローチを実装することが可能です。それぞれの場合、フォワードテストが実行されます。 クラスターウォークフォワード技術ですが、MetaTraderでは完全にはサポートされていません(執筆時点ではフォワードテストの自動起動は可能ですが、開始日や期間のサイズ変更はできませんので、MQL5やシェルスクリプトなどの他のツールを使用して実装する必要があります)。

一般に、OLAP は、Expert Advisor などの従来の最適化など、他の方法を使用して、より徹底的な分析を行うための領域を特定するのに役立つ研究ツールと見なされるべきです。 さらに、OLAPエンジンをEAに組み込み、テスターとオンラインの両方でその場で使用する方法を見てみましょう。

では、他の数日間の現在のトレード戦略を確認してみましょう. 良い日も悪い日も、ここに意図的に示されています。

図6.a 2018-2019年火曜日のシングル足EAトレードのレポート、2019年の分析に基づいたEURUSD H1

図6.a 2018-2019年火曜日のシングル足EAトレードのレポート、2019年の分析に基づいたEURUSD H1

図6.b 2018-2019年水曜日のシングル足EAトレードのレポート、2019年の分析に基づいて、EURUSD H1

図6.b 2018-2019年水曜日のシングル足EAトレードのレポート、2019年の分析に基づいて、EURUSD H1

図6.c 2018-2019年木曜日のシングル足EAトレードのレポート、2019年の分析に基づいて、EURUSD H1

図6.c 2018-2019年木曜日のシングル足EAトレードのレポート、2019年の分析に基づいて、EURUSD H1

予想通り、異なる曜日のあいまいなトレード行動は、普遍的な解決策がないことを示しており、さらなる改善が必要です。

2015年から2019年まで、より長い期間にクオートを分析し、2019年にフォワードモードでトレードした場合、どのようなトレードスケジュールが見つかるかを見てみましょう。

  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"

ご覧のように、期間の増加は、個々の時間の収益性の低下につながります。 一般化は、ある時点でパターン検索に対して再生し始めます。 水曜日は最も収益性の高い日のようです。 しかし、この動作は順方向の期間にあまり安定していません。 たとえば、次の設定を考えます。

結果のレポートは次のとおりです。

図7 2015-2020年水曜日のシングル足EAトレードのレポート、2019年を除く分析に基づいたEURUSD H1

図7 2015-2020年水曜日のシングル足EAトレードのレポート、2019年を除く分析に基づいたEURUSD H1

この問題を解決するには、より多目的な手法が必要ですが、OLAP は複数の必須ツールの 1 つにすぎません。 さらに、より複雑な(多因子的)パターンを探す方が理にかなっています。 タイムサイクルだけでなく、前の足の方向性も考慮して別のトレード戦略を作成してみましょう。

クオートの OLAP 分析に基づくトレード戦略の構築 パート2

各足の方向は、前の足の方向にもある程度依存すると仮定できます。 この依存性は、前のセクションで検出された日中および週内の変動によって接続された同様の周期的な性格を有する可能性が最も高い。 つまり、OLAP分析で週の時間と曜日によって足のサイズや方向を蓄積することに加えて、前の足の特性を何らかの形で考慮する必要があります。 残りのカスタムフィールドを使用してみましょう。

3 番目のユーザー設定フィールドでは、隣接する 2 つの足の 非対称" 共分散が計算されます。 価格変動の積として計算される通常の共分散は、足内の範囲の値として計算され、方向(増加のプラスと減少のマイナス)を考慮に入れて、前の足と次の足が得られた共分散値で同等であるため、特別な予測値を持っていません。 しかし、トレードの決定は、前の足に基づいて行われますが、次の足でのみ効率的です。 つまり、前の足の大きな動きによる高い共分散は、そのような足がヒストリーの中にあるので、すでに行われています。 そのため、次の足の範囲のみを考慮に入れた "非対称" 共分散式と、前の足を乗算する積の符号を使用します。

このフィールドでは、トレンドと反転という2つの戦略をテストできます。 たとえば、このフィールドで利益率アグリゲータを使用する場合、1 より大きい値は、前の足方向でのトレードが有益であることを示します。1 未満の値は、反対方向が利益を上げることを示します。 以前の計算と同様に、極端な値(1より大きいか、1よりはるかに低い)は、それぞれトレンドまたは反転操作がより収益性の高いことを意味します。

4 番目のユーザー設定フィールドでは、隣接する足が同じ方向 (+1) または異なる方向 (-1) のどちらにあるかの符号を保存します。 したがって、アグリゲータを使用して隣接する反転足の数、およびトレンドおよび反転戦略のインプットの効率を決定することができます。

足は常に時系列で分析されるため(このオーダーはアダプタによって提供されます)、前の足のサイズを保存し、静的変数で計算に必要なスプレッドを保存できます。 もちろん、quotesアダプタの単一インスタンスが使用されている限り実行できます(デフォルトでは、そのインスタンスはヘッダファイルに作成されます)。 これは例として適しており、理解しやすいです。 ただし、一般的に、アダプターはカスタム レコード コンストラクタ (CustomQuotesBaseRecord など) に渡し、さらに fillCustomFields メソッドに、状態を保存して復元できる特定のコンテナ (たとえば、配列への参照として) に渡す必要があります。

  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;
       }
      
  };

OLAPQTS インプットの値は変更する必要があります。 主な変更点は、アグリゲーターフィールドでの "カスタム 3" の選択に関するものです。 X および Y によるセレクタ、アグリゲーター・タイプ (PF) およびソートのパラメータは変更されません。 また、日付フィルタも変更されます。

2015年から引用を分析する際にすでに見てきたように、より長い期間の選択は、周期性を決定することを目指すシステムに適しています - 月のセレクタに対応します。 この例では、週のセレクタの時間と曜日を使用して、2018 年のみを分析し、2019 年のフォワード テストを実行します。

  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"   

"カスタム3"フィールドに実装された戦略をテストするために、別のEANextBarを作成してみましょう。 EAを使用して、ストラテジーテスターで見つかったトレード機会をチェックすることができます。 一般的なEAの構造はシングル足に似ています:同じパラメータ、関数、コードフラグメントが使用します。 トレードロジックはより複雑で、添付されたソースファイルで表示できます。

月曜日のように、時間の最も魅力的な組み合わせを選択してみましょう(PF 2以上の0.5以下)。

範囲 2018.01.01-2019.05.01 のテストを実行します。

図8 2018年のOLAP分析後の01.01.2018-01.05.2019の間のNextBarEAトレードのレポート、EURUSD H1

図8 2018年のOLAP分析後の01.01.2018-01.05.2019の間のNextBarEAトレードのレポート、EURUSD H1

この戦略は2019年1月に引き続き成功を収め、その後負け連鎖が始まりました。 何とかパターンのライフタイムを見つけ、変更する方法を学ぶ必要があります。

クオートのOLAP分析に基づく適応的トレード

これまで、OLAP 分析に特別な非トレードEAOLAPQTS を使用し、個別に開発されたEAを使用して個別の仮説をテストしてきました。 より論理的で便利なソリューションは、EAに組み込まれたOLAPエンジンを持つことです。 このように、ロボットは自動的に特定の周期で相場を分析し、トレードスケジュールを調整することができます。 さらに、EAにメインパラメータを実装することで、上記のウォークフォワードテクニックをエミュレートできるメソッドを使用して最適化することができます。 EAは OLAPQRWF と呼ばれ、ローリング ウォークフォワードを使用したクオートの OLAP の略語です。

EAの主なインプット:

  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)

また、OLAP キューブの再計算頻度を指定する必要があります。

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

戦略に加えて、アグリゲータを計算するカスタムフィールドを選択できます。 フィールド 1 と 3 は、(ストラテジー 0 と 1 の場合はそれぞれ) 足の範囲を考慮して計算され、フィールド 2 と 4 は各方向の足の数のみを考慮に入れます。

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

このCustomQuotesRecordクラスは、変更されずに OLAPQTS から継承されます。 セレクタ、フィルタ、アグリゲータのコンフィグレーションで以前に使用されていたパラメータはすべて、名前を変更することなく、定数またはグローバル変数として設定されます(ストラテジーに応じて変更する必要がある場合)。

  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;

足は時間ではなく、FIELD_INDEXを使用して数量によってフィルタリングされることに注意してください。 _Filter1value1の実際の値は、足の合計数と指定された足ナンバールックバックの差として計算されます。 したがって、EAは常に直近のBarNumberLookBackを計算します。

EAは OnTick ハンドラからバーモードでトレードします。

  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;
     }
  
    //...
   }

分析の頻度に応じて、月または週が 'calcolap' 関数で OLAP を変更して実行するのを待ちます。

  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;
   }

このコード部分は既に使い慣れています。 変更は、インプットパラメータに従って集計フィールドの選択だけでなく、最初に分析された足のインデックスの設定に関するものです。

もう 1 つの重要な変更点は、分析の実行後に OLAP エンジンによって呼び出される特殊な表示オブジェクト (統計) の使用を意味します。

  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;

このオブジェクトは、取得した統計から最適なトレード時間を決定するので、予約された「トレード」メソッドを介して同じオブジェクトにトレードを委託するのが便利です。 したがって、次の項目が OnTick に追加されます。

  void OnTick()
  {
    // ..

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

次に、MyOLAPStats クラスについて詳しく考えてみましょう。 OLAP 分析結果は、'display' メソッド (ディスプレイのメイン仮想メソッド) と saveVector (補助) によって処理されます。

  #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");
         }
       }
      
      //...
  };

このクラスでは、2 次元配列 'index' について説明します。 スケジュールに関連してパフォーマンス値を格納できます。 'display' メソッドでは、この配列には OLAP キューブのベクトルが順番に取り込まれます。 補助 saveVector 関数は、特定のトレード日の 24 時間すべてに対して数値をコピーします。 値、時間番号、および稼働日番号は、'index' の 2 番目の次元に順次書き込まれます。 値は最初の (0) 要素に配置され、プロフィットファクターで配列を並べ替えることができます。 基本的に、ログ内の便利なビューが可能になります。

トレードモードは'インデックス'配列の値に基づいて選択されます。 したがって、トレードオーダーは、閾値を超えるPFを持つ曜日と曜日の適切な時間に送信されます。

    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);
       }
      // ..
     }

ここでは、最初のテストEAで使用されたコードを、1つのトレード戦略のみを示しました。 完全なソースコードは以下に添付されています。

2015 年から 2019 年の間の時間間隔で OLAPQRWF を最適化し、2019 年のフォワード テストを実行してみましょう。 最適化の考え方は、トレードのメタパラメータを見つけることです: OLAP分析の期間、OLAPキューブの再構築の頻度、戦略の選択とカスタム集計フィールドの選択。 各最適化実行では、EAは、_historicaldata_に基づいて OLAP キューブを構築し、_past_ の設定を使用して仮想 _future_ でトレードします。 この場合、なぜフォワードテストが必要なのでしょうか。 ここで、トレード効率は指定されたメタパラメータに直接依存し、そのため、サンプル外の間隔で選択した設定の適用性を確認することが重要です。

更新期間を除く分析に影響するすべてのパラメータを最適化しましょう(月単位で保存します)。

EAは、シャープ比とトレード数の積に等しい合成カスタム最適化値を計算します。 この値に基づいて、次のインプットパラメータを使用して最適な予測が生成されます。

2015 年から 2020 年の間に別のテストを実行し、順次期間の動作をマークしてみましょう。

図9 OLAPQRWFEAレポート 01.01.2015 から 01.01.2020 2018 年の OLAP 分析ウィンドウの最適化後 2018 年ユーロドル H1

図9 OLAPQRWFEAレポート 01.01.2015 から 01.01.2020 2018 年の OLAP 分析ウィンドウの最適化後 2018 年ユーロドル H1

収益性の高いスケジュールを自動的に決定するこのEAは、前年に見つかった集計ウィンドウサイズを使用して2019年に正常にトレードすると結論付けることができます。 もちろん、このシステムはさらなる研究と分析が必要です。 それでも、このツールは上手くいくことが確認できました。

結論

この記事では、オンライン データ処理用の OLAP ライブラリの関数を改善および拡張し、クオート領域を持つ特殊なアダプタクラスとタスクレコード クラスを通じてバンドルを実装しました。 記載されたプログラムを使用して、ヒストリーを分析し、収益性の高いトレードを提供するパターンを決定することが可能です。 最初の段階では、OLAP分析に慣れるときには、ソースデータのみを処理し、一般化された統計を提示する個々の非トレードEAを使用する方が便利です。 また、このようなEAでは、トレーディング戦略の基本要素 (仮説) を含むカスタムフィールドを計算するためのアルゴリズムを開発およびデバッグできます。 さらに、OLAP スタディステップでは、エンジンは新規または既存のトレーディングロボットと統合されます。 この場合、EAの最適化では、共通の操作パラメータだけでなく、OLAP に接続され、統計の収集に影響を与える新しいメタ パラメータも考慮する必要があります。

もちろん、OLAP ツールは、特に予測できない相場の状況では万能薬ではありません。 したがって、聖杯とは言えません。 それでも、組み込みの相場分析は間違いなく可能性を広げ、トレーダーは新しい戦略を検索して新しいEAを作成することができます。