トレードにおけるOLAPの適用(パート1):多次元データのオンライン分析

Stanislav Korotky | 1 7月, 2019

トレーダーは、多くの場合、膨大な量のデータを分析する必要があります。 多くの場合、数字、相場、インジケータ値、トレードレポートです。 数値が依存するパラメータと条件の数が多いため、それを部分的に考慮し、プロセス全体をさまざまな角度から表示します。 情報の全体量は仮想ハイパーキューブの一種で、各パラメータが独自のディメンションを定義します。 このようなハイパーキューブは、一般的なOLAP( オンライン分析処理)技術を使用して処理および分析することができます。

アプローチ名の "online(オンライン)" はインターネットを参照するのではなく、結果の迅速性を意味します。 操作原理はハイパーキューブ セルの予備的な計算を意味し、その後、キューブの断面を視覚的な形式ですばやく抽出して表示できます。 これは、MetaTraderの最適化プロセスと比較することができます。つまり、テスターは最初にトレード亜種を計算し(長い時間がかかる可能性があり、プロンプトではない)、インプットパラメータにリンクされた結果を特徴とするレポートを出力します。 ビルド1860以降、MetaTrader5プラットフォームは、さまざまな最適化基準を切り替えることで、表示された最適化結果の動的な変更をサポートします。 これは、OLAPの考えに近いです。 しかし、完全な分析には、ハイパーキューブの他の多くのスライスを選択する可能性が必要です。

ここでは、メタトレーダーにOLAPアプローチを適用し、MQLツールを用いた多次元解析の実装を試みます。 実装に進む前に、分析するデータを決定する必要があります。 これらには、トレードレポート、最適化結果、またはインジケータ値が含まれます。 この段階での選択は、あらゆるデータに適用可能なユニバーサルオブジェクト指向エンジンの開発を目指しているため、重要ではありません。 しかし、特定の結果にエンジンを適用する必要があります。 最も一般的なタスクの一つは、トレードレポートの分析です。 このタスクを検討します。

トレードレポート内では、シンボル別の利益のブレイクダウン、曜日、売買操作が役に立つ場合があります。 別の選択肢は、異なるトレードロボットのパフォーマンス結果を比較することです(すなわち、マジックナンバーごとに別々にです)。 次の論理的な問題は、EAに関連して曜日のシンボル、または他のグループ化を追加するなど、さまざまなディメンションを組み合わせることができるかどうかです。 このすべては、OLAP を使用して行うことができます。

アーキテクチャ

オブジェクト指向のアプローチによれば、大きなタスクはシンプルな論理的に関連する部分に分割する必要がありますが、各部分は受信データ、内部状態、および一部のルールセットに基づいて独自の役割を果たします。

最初に使用するクラスは、ソース データを含むレコードである 'Record' です。 このようなレコードは、1つのトレード操作または1つの最適化パスなどに関連するデータを格納することができます。

'Record' は、任意の数のフィールドを持つベクトルです。 抽象エンティティであるため、各フィールドの意味は重要ではありません。 特定のアプリケーションごとに、フィールドの目的を "知っている" 派生クラスを作成し、処理します。

別のクラス'DataAdapter'は、抽象ソース(トレード口座のヒストリー、CSVファイル、HTMLレポート、WebRequestを使用してウェブ上で取得されたデータなど)からレコードを読み取るために必要です。 この段階では、1 つの関数のみを実行します。 後で、実際のアプリケーションごとに派生クラスを作成できるようになります。 これらのクラスは、関連するソースからのレコードの配列を埋めます。

すべてのレコードは、ハイパーキューブ セルに何らかの方法で表示できます。 この段階では、やり方はわかりませんが、これがプロジェクトの考え方です。つまり、キューブセル間でレコードフィールドからのインプット値を分散し、選択した集計関数を使用して一般化された統計を計算します。

基本キューブ レベルでは、ディメンションの数、その名前、各ディメンションのサイズなどの主要なプロパティのみが提供されます。 このデータは MetaCube クラスで提供されます。

派生クラスは、セルに関連する統計情報をインプットします。 特定のアグリゲーターの最も一般的な例には、すべての値の合計またはすべてのレコードの同じフィールドの平均値が含まれます。 ただし、アグリゲータの種類はさらに異なります。

セル内の値の集計を有効にするには、各レコードがインデックスのセットを受け取り、キューブの特定のセルに一意にマップする必要があります。 このタスクは、特別な 「セレクタ」クラスによって実行されます。 このセレクタは、ハイパーキューブの片側(軸、座標)に対応します。

抽象セレクタ基本クラスは、有効な値のセットを定義し、各インプットを値のいずれかにマッピングするためのプログラミング インターフェイスを提供します。 たとえば、レコードを曜日で分割する目的がある場合、派生したセレクタ クラスは曜日の数を 0 から 6 に返す必要があります。 特定のセレクタの許容値の数によって、このキューブ ディメンションのサイズが定義されます。 これは、曜日、すなわち7です。

さらに、レコードの一部をフィルタリングすると便利な場合があります (分析から除外する場合)。 したがって、Filter クラスが必要です。 これはセレクタに似ていますが、許容値に追加の制限を設定します。 たとえば、曜日のセレクタに基づいてフィルタを作成できます。 このフィルタでは、計算から除外する必要がある日数、またはその中にインクルードする日数を指定することができます。

キューブが作成されると (つまり、すべてのセルの集計関数が計算された場合)、結果を視覚化して分析できます。 このため、特別な「Display(表示)」クラスを予約しましょう。

前述のすべてのクラスをユニット全体に結合するには、一種のコントロールセンターである Analyst クラスを作成します。

UML 表記法で次のようになります (任意の開発段階で確認できるアクション プランと見なすことができます)。

メタトレーダーにおけるオンライン分析処理

メタトレーダーにおけるオンライン分析処理

ここでは、一部のクラスを省略します。 ただし、ハイパーキューブ構築の一般的な基礎を反映し、ハイパーキューブ セルの計算に使用できる集計関数を示します。

基本クラスの実装

次に、上記のクラスの実装に進みます。 レコードクラスから始めましょう。

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

「data(データ)」配列(ベクトル)に任意の値を格納します。 ベクトルの長さはコンストラクタで設定されます。

異なるソースからのレコードは、DataAdapter を使用して読み取られます。

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

getNext メソッドは、NULLを返すまでループで呼び出す必要があります (つまり、レコードがそれ以上ありません)。 受信したすべてのレコードは、どこかに保存する必要があります (このタスクは後で説明します)。 reservedSizeメソッドを使用すると、最適化されたメモリ分散が可能になります (ソース内のレコードの数が事前にわかっている場合)。

各ハイパーキューブ ディメンションは、1 つ以上のレコード フィールドに基づいて計算されます。 各フィールドを列挙体の要素としてマークすると便利です。 たとえば、口座トレードヒストリーを分析する場合、次の列挙体を使用できます。

  //MetaTrader4とMetaTrader5ヘッジ
  enum TRADE_RECORD_FIELDS
  {
    FIELD_NONE,          //なし
    FIELD_NUMBER,        //シリアル番号
    FIELD_TICKET,        //チケット
    FIELD_SYMBOL,        //シンボル
    FIELD_TYPE,          //タイプ (OP_BUY/OP_SELL)
    FIELD_DATETIME1,     //オープン日時
    FIELD_DATETIME2,     //決済日時
    FIELD_DURATION,      //期間
    FIELD_MAGIC,         //マジックナンバー
    FIELD_LOT,           //ロット
    FIELD_PROFIT_AMOUNT, //利益金額
    FIELD_PROFIT_PERCENT,//利益パーセント
    FIELD_PROFIT_POINT,  //利益ポイント
    FIELD_COMMISSION,    //コミッション
    FIELD_SWAP,          //スワップ
    FIELD_CUSTOM1,       //カスタム 1
    FIELD_CUSTOM2        //カスタム 2
  };

直近の 2 つのフィールドは、非標準変数の計算に使用できます。

メタトレーダー最適化結果の分析には、以下の列挙を提案することができます。

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

実用的なアプリケーションケースごとに、個々の列挙体を用意する必要があります。 その後、セレクタ テンプレート クラスのパラメータとして使用できます。

  template<typename E>
  class Selector
  {
    protected:
      E selector;
      string _typename;
      
    public:
      Selector(const E field): selector(field)
      {
        _typename = typename(this);
      }
      
      //レコードから値を格納するセルのインデックスを返します。
      virtual bool select(const Record *r, int &index) const = 0;
      
      virtual int getRange() const = 0;
      virtual float getMin() const = 0;
      virtual float getMax() const = 0;
      
      virtual E getField() const
      {
        return selector;
      }
      
      virtual string getLabel(const int index) const = 0;
      
      virtual string getTitle() const
      {
        return _typename + "(" + EnumToString(selector) + ")";
      }
  };

セレクタ フィールドには、列挙体の要素である 1 つの値のみが格納されます。 たとえば、TRADE_RECORD_FIELDS を使用する場合、売買操作のセレクタは次のように作成できます。

  new Selector<TRADE_RECORD_FIELDS>(FIELD_TYPE);

_typename フィールドは補助です。 これは、すべての派生クラスで上書きされ、セレクタが識別され、結果を視覚化する場合に役立ちます。 このフィールドは、仮想 getTitle メソッドで使用します。

操作の大部分は、'select' メソッドのクラスによって実行されます。 ここでは、各インプットレコードは、現在のセレクタによって形成される座標軸に沿った特定のインデックス値としてマッピングされます。 インデックスは getMin メソッドと getMax メソッドによって返される値の間の範囲内にある必要がありますが、インデックスの合計数は getRange によって返される数と等しくなります。 セクタ定義領域でレコードを正しくマップできない場合、何らかの理由で 'select' メソッドは false を返します。 マッピングが正しく実行されている場合は、true が返されます。

getLabel メソッドは、特定のインデックスのわかりやすい説明を返します。 たとえば、売買操作の場合、インデックス 0 は "buy"を生成する必要がありますが、インデックス 1 では "sell"を生成する必要があります。

トレードヒストリーの特定のセレクタクラスとデータアダプタクラスの実装

トレードヒストリーを分析するので、TRADE_RECORD_FIELDS 列挙に基づいてセレクタの中間クラスを紹介します。

  class TradeSelector: public Selector<TRADE_RECORD_FIELDS>
  {
    public:
      TradeSelector(const TRADE_RECORD_FIELDS field): Selector(field)
      {
        _typename = typename(this);
      }
  
      virtual bool select(const Record *r, int &index) const
      {
        index = 0;
        return true;
      }
      
      virtual int getRange() const
      {
        return 1; //デフォルトではスカラーで、1 の値を返します。
      }
      
      virtual double getMin() const
      {
        return 0;
      }
      
      virtual double getMax() const
      {
        return (double)(getRange() - 1);
      }
      
      virtual string getLabel(const int index) const
      {
        return EnumToString(selector) + "[" + (string)index + "]";
      }
  };

デフォルトでは、すべてのレコードが同じセルにマップされます。 たとえば、このセレクタを使用すると、合計利益データを取得できます。

さて、このセレクタに基づいて、特定の派生型セレクタを決定できます。 工程タイプ (売買) によるレコードのグループ化にも使用します。

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

コンストラクタの FIELD_TYPE 要素を使用してクラスを定義しました。 getRange メソッドは 2 を返します。 getMin メソッドと getMax メソッドは、対応する定数を返します。 'select' メソッドには何をインクルードする必要があるでしょうか。

まず、どの情報を各レコードに格納するかを決定する必要があります。 レコードから派生し、トレードヒストリーを操作するために適応されたTradeRecordクラスを使用して行うことができます。

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

補助 fillByOrder メソッドは、現在のオーダーに基づいてほとんどのレコード フィールドをインプットする方法を示します。 もちろん、このオーダーはコード内の別の場所で事前に選択する必要があります。 ここでは、MetaTrader4トレード関数の表記を使用します。 MetaTrader5のサポートは、 MetaTrader4Ordersライブラリを含むことで実装されます(バージョンの1つは以下に添付されており、常に現在のバージョンをチェックしてダウンロードします)。 したがって、クロスプラットフォームコードを作成できます。

TRADE_RECORD_FIELDS_NUMBER フィールドの数は、マクロ定義としてハードコーディングすることも、TRADE_RECORD_FIELDS 列挙に基づいて動的に計算することもできます。 2 番目のアプローチは、特別なテンプレート化された EnumToArray 関数が使用する添付コードで実装されます。

fillByOrder メソッドから見られるように、FIELD_TYPE フィールドは OrderType の操作タイプでインプットされます。 これで、TypeSelector クラスに戻り、'select' メソッドを実装できます。

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

ここでは、インプットレコード (r) からフィールド値 (セレクタ) を読み取り、その値 (OP_BUY または OP_SELL のいずれか) をインデックス出力パラメータに割り当てます。 計算には成行オーダーのみが含まれるため、他のすべてのタイプに対して false が返されます。 後で他のセレクタタイプを検討します。

今度はトレードヒストリーのデータアダプタを開発する時間です。 これは、トレード記録レコードがアカウントの実際のトレードヒストリーに基づいて生成されるクラスです。

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

このアダプターは、ヒストリーで使用可能なすべてのオーダーを順番に通過し、成行オーダーごとに TradeRecord インスタンスを作成します。 このコードは簡略化された形式でここに示されています。 実際の使用中に、TradeRecord クラスではなく派生クラスのオブジェクトを作成する必要がある場合があります。つまり、TRADE_RECORD_FIELDS 列挙体の 2 つのカスタム フィールドを予約しました。 したがって、HistoryDataAdapter はテンプレート クラスであり、テンプレート パラメータは生成されたレコード オブジェクトの実際のクラスです。 Record クラスには、カスタム フィールドをインプットするための空の仮想メソッドが含まれている必要があります。

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

完全な実装アプローチを自分で分析できます。CustomTradeRecord クラスはコアで使用します。 fillCustomFields では、このクラス (トレードレコードの子) は、各ポジションの MFE (最大有利なエクスカーション) と MAE (最大不利なエクスカーション) を計算し、値を FIELD_CUSTOM1 および FIELD_CUSTOM2 フィールドに記録します。

アグリゲータとコントロール クラスの実装

アダプターを作成し、getNext メソッドを呼び出す場所が必要です。 次に、Analyst クラスである "コントロール センター"を扱います。 アダプターの起動に加えて、クラスは受信したレコードを内部配列に格納する必要があります。

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

このクラスはアダプターを作成しませんが、準備完了のアダプターパラメータとして受け取ります。 よく知られている設計原理であり、依存関係のインジェクションです。 これより、特定の DataAdapter 実装から Analyst を切り離できます。 つまり、Analyst クラスで変更を加えなくても、さまざまなアダプタ亜種を置き換えることができます。

Analyst クラスはレコードの内部配列を埋めることができるようになりましたが、メイン関数の実行方法、つまりデータの集計方法がまだわかりません。 このタスクは、アグリゲータによって実装されます。

アグリゲーターは、選択したレコードフィールドの定義済み変数 (統計) を計算できるクラスです。 アグリゲータの基本クラスは、多次元配列に基づくストレージであるMetaCubeです。

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

「ディメンション」配列は、ハイパーキューブ構造を記述します。 そのサイズは、使用するセレクタの数、つまりディメンションと同じです。 「dimension(ディメンション)」配列の各要素には、適切なセレクタの値の範囲によって決定される、このディメンションのキューブ サイズがあります。 たとえば、曜日別に利益を表示するには、オーダー (ポジション) の開始時刻または決済時刻に応じて、日番号をインデックスとして返すセレクタを作成する必要があります。 唯一のセレクタであるため、'dimensions' 配列には 1 つの要素があり、その値は 7 になります。 前述の TypeSelector など、別のセレクタを追加して、曜日と操作の種類で利益を表示すると、'dimensions' 配列には 7 と 2 の値を持つ 2 つの要素が含まれます。 また、ハイパーキューブに統計情報を含む 14 個のセルが含まれていることを意味します。

すべての値 (この例では 14) を持つ配列は'合計' に含まれています。 ハイパーキューブは多次元であるため、配列は 1 つのディメンションのみを持つと宣言されているように見える場合があります。 これは、ユーザーが追加する必要があるハイパーキューブ ディメンションが事前にわからないためです。 さらに、MQL は、すべてのディメンションが動的に分散される多次元配列をサポートしていません。 したがって、通常の "flat(フラット)" 配列 (ベクトル) が使用します。 特別なインデックス作成は、この配列内の複数のディメンションにセルを格納するために使用します。 次に、各ディメンションのオフセットの計算について考えてみましょう。

基本クラスは配列を割り当てませんし、初期化もしませんが、派生クラスによって実行されます。

すべてのアグリゲータには多くの共通関数が期待されているので、すべて 1 つの中間クラスにまとめましょう。

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

各アグリゲータは、特定のレコードフィールドを処理します。 このフィールドは、コンストラクタでインプットされる 'field' 変数のクラスで指定されます (以下を参照)。 たとえば、利益 (FIELD_PROFIT_AMOUNT) です。

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

この計算は、任意の数のセレクタ(セレクタCount)で形成される多次元空間で実行されます。 以前は、曜日別と操作タイプ別のブレイクダウンを持つ利益の計算を検討しましたが、 2 つのセレクタを必要とします。 参照の'セレクタ'配列に格納されます。 セレクタ オブジェクトはコンストラクタ パラメータとして渡されます。

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

ご存知のように、計算値を格納するための「total(合計)」配列は 1 次元です。 次の関数は、多次元セレクタ空間のインデックスを 1 次元配列内のオフセットに変換するために使用します。

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

インデックスを含む配列をインプットとして受け入れ、要素の連続した数を返します。 'offsets' 配列はここで使用します 。 初期化は重要なポイントの 1 つであり、setSelectorBounds メソッドで実行されます。

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

この目的は、すべてのセレクタの範囲を取得し、順番に乗算することです。つまり、したがって、各ハイパーキューブ ディメンションで座標を 1 ずつ増やすときに"ジャンプ"に要素の数を決定することができます。

集計変数の計算は、計算方法で実行されます。

      //セレクタの数と等しいディメンション数の配列を構築する
      virtual void calculate(const Record *&data[])
      {
        int k[];
        ArrayResize(k, selectorCount);
        int n = ArraySize(data);
        for(int i = 0; i < n; i++)
        {
          int j = 0;
          for(j = 0; j < selectorCount; j++)
          {
            int d;
            if(!selectors[j].select(data[i], d)) //レコードはセレクタを満たしているか?
            {
              break;                             //スキップしない場合
            }
            k[j] = d;
          }
          if(j == selectorCount)
          {
            update(mixIndex(k), data[i].get(field));
          }
        }
      }

このメソッドは、レコードの配列に対して呼び出されます。 ループ内の各レコードは、各セレクタに順番に渡されます。 すべてのセレクタ (各セレクタに独自のインデックスがある) で有効なインデックスに正常にマップされた場合、インデックスの完全なセットは k ローカル配列に保存されます。 すべてのセレクタがインデックスを決定している場合は、'update' メソッドが呼び出されます。 メソッドには、'total' 配列のオフセット (オフセットは前述の mixIndex を使用して計算されます) と、現在のレコードから指定された 'フィールド' (アグリゲータで設定) の値がインプットされます。 利益分布分析の例では、'フィールド' 変数は FIELD_PROFIT_AMOUNT と等しくなりますが、このフィールドの値は OrderProfit() 呼び出しによって提供されます。

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

更新メソッドはこのクラスでは抽象であり、継承で再定義する必要があります。

また、このアグリゲータは、計算結果にアクセスするための少なくとも 1 つのメソッドを提供する必要があります。 最もシンプルなのは、インデックスのセット全体に基づいて特定のセルの値を受け取るということです。

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

基本クラスアグリゲータは、ほぼすべてのラフなタスクを実行します。 多くの特定のアグリゲータを実装できるようになりました。

しかし、まず Analyst クラスに戻りましょう: アグリゲータへの参照を追加する必要があります。

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

acquireData メソッドでは、アグリゲータの setSelectBounds メソッドの追加呼び出しを使用してハイパーキューブ ディメンションを構成します。

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

主なタスク、すなわちハイパーキューブのすべての値の計算は、アグリゲータに実装されます(以前に'calculate'メソッドを検討しましたが、ここではレコードの配列が渡されます)。

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

Analyst クラスだけではありません。 以前は、特別な表示インターフェイスとして形式化することで、結果を表示できるようにする計画を立てました。 このインターフェイスは、同様の方法で Analyst に接続されます (コンストラクタへの参照を渡すことによって)。

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

「Display(表示)」の内容はシンプルです。

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

ハイパーキューブがデータソースとしてインプットされる抽象仮想メソッドがあります。 簡潔にするために、値のPrintオーダーに影響を与えるパラメータの一部はここでは省略します。 ビジュアライゼーションの詳細と必要な追加設定が派生クラスに表示されます。

分析クラスをテストするには、少なくとも 1 つの 'Display' インターフェイスの実装が必要です。 エキスパートジャーナルにメッセージを書き込むことで作成してみましょう。 LogDisplayと呼びばす。 このインターフェイスはハイパーキューブのすべての座標をループし、集計値を適切な座標と共にPrintします。

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

ログのより便利な書式設定の LogDisplay 実装はもう少し複雑になるので、「大まかに」説明します。 このクラスのフルバージョンは、添付されたソースコードで利用可能です。

もちろん、チャートほど効率的ではありませんが、2次元または3次元画像の作成は別の主題であり、考慮しません(オブジェクト、キャンバス、外部グラフィカルなど、異なる技術を使用することはできますが)Web 技術に基づくライブラリを含みます)。

したがって、Aggregatorべースクラスがあります。 その上で、update メソッドで集計変数の特定の計算を使用して、複数の派生クラスを取得できます。 特に、次のシンプルなコードを使用して、すべてのレコードから特定のセレクタによって抽出された値の合計を計算できます。

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

平均を計算するために必要なのは、わずかな統合だけです。

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

関連するすべてのクラスを考慮したので、その相互作用アルゴリズムを一般化してみましょう。

      analyst.acquireData();
      analyst.build();
      analyst.display();

特殊ケース: ダイナミックセレクタ

プログラムはほとんどできています。 以前は、説明を簡略化するために、その一部を省略していました。 さて、これを排除しましょう。

前述のすべてのセレクタは、値の一定の範囲を持っていました。 たとえば、成行オーダーは買いまたは売りである間、週に7日常にあります。 しかし、レンジは頻繁に起こりますが、事前に知られていない場合があります。

タスクシンボルまたはEAマジックナンバーを反映するハイパーキューブが必要な場合があります。 このタスクの解決策では、まず内部配列内のすべての固有のツールまたはマジックナンバーを収集し、次に関連するセレクタ範囲に配列サイズを使用します。

内部配列を管理するための「Vocabulary(ボキャブラリ)」クラスを作成してみましょう。 その使用方法を Symbol セレクタ クラスと組み合わせて分析します。

ボキャブラリの実装は簡単です(任意の好みのものに置き換えることができます)。

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

'index' 配列は、一意の値を格納するために予約されています。

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

'get' メソッドは、配列に特定の値が既に存在するかどうかを確認するために使用します。 このような値がある場合、メソッドは見つかったインデックスを返します。 値が配列に存在しない場合、メソッドは配列サイズを 1 ずつ増加し、マイナスシンボルを持って返します。 これより、配列に新しい値を追加するための次のメソッドをわずかな最適化が可能になります。

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

また、配列サイズとそこに格納されている値をインデックスで受信するためのメソッドを提供する必要があります。

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

この場合、タスクシンボルはオーダー(ポジション)のコンテキストで分析されるため、ボキャブラリを TradeRecord クラスに埋め込みます。

  class TradeRecord: public Record
  {
    private:
      ...
      static Vocabulary<string> symbols;
  
    protected:
      void fillByOrder(const double balance)
      {
        ...
        set(FIELD_SYMBOL, symbols.add(OrderSymbol())); //シンボルはボキャブラリのインデックスとして格納されます。
      }
  
    public:
      static int getSymbolCount()
      {
        return symbols.size();
      }
      
      static string getSymbol(const int index)
      {
        return symbols[index];
      }
      
      static int getSymbolIndex(const string s)
      {
        return symbols.get(s);
      }

このボキャブラリは、トレードヒストリー全体で共有されるため、静的変数として記述されます。

シンボルセレクタを実装できます。

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

このマジックナンバーセレクタも同様の方法で実装されています。

提供されるセレクタの一般的なリストには、次のものが含まれます (フィールドへの外部バインディングの必要性は括弧内に示されていますが、省略されている場合は、セレクタ クラス内で特定のフィールドへのバインドが既に提供されています)。

シリアル番号セレクタは、他のセレクタとは大きく異なります。 その範囲は、レコードの合計数と等しくなります。 これより、ハイパーキューブの生成が可能となり、このレコードは 1 つのディメンション (通常は最初のディメンション X) で順次カウントされ、指定されたフィールドは他のディメンションにコピーされます。 このフィールドはセレクタによって定義されます。つまり、特殊セレクタにはフィールド バインディングが既に含まれています。「スワップ」などの準備完了セレクタがないフィールドが必要な場合は、ユニバーサルトレードセレクタを使用できます。 つまり、SerialNumberSelector を使用すると、集約されたハイパーキューブ メタファー内のソース レコード データを読み取る可能性があります。 これは、擬似アグリゲータ IdentityAggreゲータを使用して行われます(下記参照)。

次のアグリゲータを使用できます。

直近の 2 つのアグリゲータは、残りのアグリゲータとは異なります。 IdentityAggregator を選択すると、ハイパーキューブ サイズは常に 2 になります。 このレコードは SerialNumberSelector を使用して X 軸に沿って反映され、2 番目の軸 (実際にはベクトル/列) に沿った各カウントは 1 つのセレクタに対応し、ソース レコードから読み取るフィールドが決定されます。 したがって、(シリアルナンバーセレクタに加えて)3つの追加セレクタがある場合、Y軸に沿って3つのカウントがあります。 ただし、このキューブには X 軸と Y 軸の 2 つのディメンションがあります。 通常、このキューブは異なる原理に従って生成されます。各セレクタは独自のディメンションに対応しているので、3 次元は 3 軸を意味します。

ProgressiveTotalAggregatorは、特別な方法で最初のディメンションを処理します。 その名前が示すように、アグリゲータは累積合計の計算を可能にし、X 軸に沿って実行します。 たとえば、アグリゲータ パラメータで利益フィールドを指定すると、一般的なバランス曲線が得されます。 Y 軸(2 番目のセレクタ)に沿ってシンボル(シンボルセレクタ)をプロットすると、使用可能なシンボルごとに複数の[N]バランスカーブが表示されます。 2番目のセレクタがMagicSelectorの場合、異なるEAの別々の[M]バランス曲線になります。 さらに、両方のパラメータを組み合わせることができます。Yに沿ってシンボルセレクタを設定し、Z軸に沿ってマジックセレクタを設定します(またはその逆):それぞれ異なるマジックナンバーとシンボルの組み合わせを持つ[N*M]バランスカーブを取得します。

これで OLAP エンジンの準備が整いました。 記事を簡潔に保つために、説明部分の一部を省略しました。 たとえば、この記事では、アーキテクチャで提供されたフィルタ (Filter、FilterRange クラス) の説明は提供されません。 さらに、このハイパーキューブは、集計値を 1 つずつ (メソッド getValue (const int &dices[])) だけでなく、次のメソッドを使用してベクトルとして返すことができます。

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

このメソッド出力は、特別な PairArray クラスです。 [value;name] のペアを持つ構造体の配列を格納します。 たとえば、シンボルによって利益を反映するキューブを構築する場合、各合計は特定のシンボルに対応するため、その名前は値の横のペアで示されます。 メソッドのプロトタイプから分かるように、ペア配列をさまざまなモード(昇順または降順、値またはタグで並べ替えることができます) を指定できます。

  enum SORT_BY //1 次元キューブにのみ適用可能
  {
    SORT_BY_NONE,             //なし
    SORT_BY_VALUE_ASCENDING,  //値 (昇順)
    SORT_BY_VALUE_DESCENDING, //値 (降順)
    SORT_BY_LABEL_ASCENDING,  //ラベル (昇順)
    SORT_BY_LABEL_DESCENDING  //ラベル (降順)
  };

並べ替えは、1 次元ハイパーキューブでのみサポートされます。 理論的には、任意の数のディメンションに対して実装できますが、これは日常的なタスクです。 興味のある方は、このような並べ替えを実装できます。

完全なソース コードが添付されています。

OLAPDEMOの例

次に、ハイパーキューブをテストしてみましょう。 口座トレードヒストリーを分析できる非トレードEAを作成してみましょう。 これを「OLAPDEMO」と名づけましょう。 すべての主要な OLAP クラスが含まれているヘッダ ファイルを含めます。

  #include <OLAPcube.mqh>

ハイパーキューブは任意の数のディメンションを処理できますが、わかりやすくするために、3に制限します。 つまり、ユーザーは同時に最大 3 つのセレクタを使用できます。 特殊列挙体の要素を使用して、サポートされているセレクタタイプを定義します。

  enum SELECTORS
  {
    SELECTOR_NONE,       //なし
    SELECTOR_TYPE,       //型
    SELECTOR_SYMBOL,     //シンボル
    SELECTOR_SERIAL,     //序数
    SELECTOR_MAGIC,      //マジック
    SELECTOR_PROFITABLE, //収益 性
    /* カスタムセレクタ */
    SELECTOR_DURATION,   //期間 (日数)
    /* 次のすべての項目はパラメータとして必要です */
    SELECTOR_WEEKDAY,    //曜日 (日時フィールド)
    SELECTOR_DAYHOUR,    //日時 (日時フィールド)
    SELECTOR_HOURMINUTE, //時間の分 (日時フィールド)
    SELECTOR_SCALAR,     //スカラー(フィールド)
    SELECTOR_QUANTS      //クオンツ(フィールド)
  };

列挙体を使用して、セレクタを設定するインプットパラメータを記述します。

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

各セレクタ グループには、オプションのレコード フィールドを設定するためのインプットが含まれています (一部のセレクタはフィールドを必要とし、他のセレクタは設定しません)。

1つのフィルタを指定します (複数のフィルタを使用できますが)。 このフィルタはデフォルトで無効になります。

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

フィルタの考え方: 指定された Filter1Field フィールドに特定の Filter1value1 値を持つレコードだけを考慮します。 (Filter1value2 は同じでなければならず、この例では Filter オブジェクトの作成に必要です)。 シンボルまたはマジックナンバーフィールドの値は、対応するボキャブラリのインデックスを示します。 このフィルタには、1 つの値ではなく、Filter1value1 と Filter1value2 の間の値の範囲をインクルードすることができます (FilterRange オブジェクトは 2 つの異なる値に対してのみ作成できるため、等しくない場合)。 この実装は、フィルタリングの可能性のデモンストレーションに作成されましたが、将来の実用的な使用に大幅に拡張することができます。

アグリゲータの別の列挙体について説明します。

  enum AGGREGATORS
  {
    AGGREGATOR_SUM,         //合計
    AGGREGATOR_AVERAGE,     //平均
    AGGREGATOR_MAX,         //最大
    AGGREGATOR_MIN,         //最小
    AGGREGATOR_COUNT,       //カウント
    AGGREGATOR_PROFITFACTOR, //利益率
    AGGREGATOR_PROGRESSIVE,  //プログレッシブ合計
    AGGREGATOR_IDENTITY      // IDENTITY
  };

タスクアグリゲータを記述するインプットパラメータのグループで使用します。

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

オプションのフィルタで使用するセレクタを含むすべてのセレクタは OnInit で初期化されます。

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

OLAP は、タイマーによって 1 回だけ実行されます。

  void OnTimer()
  {
    process();
    EventKillTimer();
  }
  
  void process()
  {
    HistoryDataAdapter history;
    Analyst<TRADE_RECORD_FIELDS> *analyst;
    
    Selector<TRADE_RECORD_FIELDS> *selectors[];
    ArrayResize(selectors, selectorCount);
    
    for(int i = 0; i < selectorCount; i++)
    {
      selectors[i] = createSelector(i);
    }
    Filter<TRADE_RECORD_FIELDS> *filters[];
    if(Filter1 != SELECTOR_NONE)
    {
      ArrayResize(filters, 1);
      Selector<TRADE_RECORD_FIELDS> *filterSelector = createSelector(3);
      if(Filter1value1 != Filter1value2)
      {
        filters[0] = new FilterRange<TRADE_RECORD_FIELDS>(filterSelector, Filter1value1, Filter1value2);
      }
      else
      {
        filters[0] = new Filter<TRADE_RECORD_FIELDS>(filterSelector, Filter1value1);
      }
    }
    
    Aggregator<TRADE_RECORD_FIELDS> *aggregator;
    
    //MQL は「クラス情報」メタクラスをサポートしていません。
    //それ以外の場合は、スイッチの代わりにクラスの配列を使用できます。
    switch(AggregatorType)
    {
      case AGGREGATOR_SUM:
        aggregator = new SumAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_AVERAGE:
        aggregator = new AverageAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_MAX:
        aggregator = new MaxAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_MIN:
        aggregator = new MinAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_COUNT:
        aggregator = new CountAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_PROFITFACTOR:
        aggregator = new ProfitFactorAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_PROGRESSIVE:
        aggregator = new ProgressiveTotalAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
      case AGGREGATOR_IDENTITY:
        aggregator = new IdentityAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
        break;
    }
    
    LogDisplay display;
    
    analyst = new Analyst<TRADE_RECORD_FIELDS>(history, aggregator, display);
    
    analyst.acquireData();
    
    Print("Symbol number: ", TradeRecord::getSymbolCount());
    for(int i = 0; i < TradeRecord::getSymbolCount(); i++)
    {
      Print(i, "] ", TradeRecord::getSymbol(i));
    }
  
    Print("Magic number: ", TradeRecord::getMagicCount());
    for(int i = 0; i < TradeRecord::getMagicCount(); i++)
    {
      Print(i, "] ", TradeRecord::getMagic(i));
    }
  
    Print("Filters: ", aggregator.getFilterTitles());
    
    Print("Selectors: ", selectorCount);
    
    analyst.build();
    analyst.display();
    
    delete analyst;
    delete aggregator;
    for(int i = 0; i < selectorCount; i++)
    {
      delete selectors[i];
    }
    for(int i = 0; i < ArraySize(filters); i++)
    {
      delete filters[i].getSelector();
      delete filters[i];
    }
  }

作成セレクターのサポート機能は、次のように定義されます。

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

DaysRangeセレクタを除くすべてのクラスはヘッダファイルからインポートされ、DaysRangeセレクタはOLAPDEMOEA内で次のように記述されます。

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

これは、カスタム セレクタの実装例です。 数日で、相場でのライフタイムによってトレードポジションをグループ分けします。

任意のオンラインアカウントでEAを実行し、2つのセレクタ、シンボルセレクタ、およびWeekDayセレクタを選択すると、ログで次の結果を受け取ることができます。

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

5つのシンボルが口座でトレードされました。 ハイパーキューブ サイズ: 35 セル。 銘柄と曜日のすべての組み合わせが、対応する損益額と共に表示されます。 各ポジションにはオープン日付 (FIELD_DATETIME1) とクローズ日 (FIELD_DATETIME2) の 2 つの日付があるため、WeekDay セレクタにはフィールドの明示的な指示が必要です。 ここでは、フィールド_DATETIME2を選択しました。

現在の口座ヒストリーだけでなく、HTML形式の任意のトレードレポート、MQL5シグナルヒストリーを持つCSVファイルを分析するために、前回の記事メソッド(CSSセレクタを使用してHTMLページから構造化データを抽出する)、およびHTMLとCSVレポートに基づいて多通貨トレードヒストリーを視覚化する方法)がOLAPライブラリに追加されました。 OLAP と統合するために、追加のレイヤー クラスが作成されました。 特に、ヘッダ ファイル HTMLcube.mqh には、トレード レコード クラス HTMLTradeRecord とデータアダプターから継承される HTMLReportAdapter があります。 ヘッダ・ファイル CSVcube.mqh には、応じてレコード・クラス CSVTradeRecord および CSVReportAdapter が含まれます。 HTML 読み取りは WebDataExtractor.mqh によって提供され、CSV は CSVReader.mqh によって読み取られます。 レポートのダウンロードに関するインプットパラメータと、レポートを操作するための一般的な原則 (プレフィックスとサフィックスが使用する場合に適切なシンボルの選択を含む) については、上記の 2 番目の記事で詳しく説明されています。

シグナル分析結果(CSVファイル)を次に示します。 アグリゲータを利益係数と銘柄別のブレイクダウンで使用しました。 結果は降順に並べ替えられて表示されます。

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

inf 値は、利益と損失がない場合にソース コードで生成されます。 ご覧のように、実際の値とその並べ替えの比較は、"infinity" が他の有限の数値よりも大きいように行われます。

もちろん、トレードレポートの分析結果をログで見ることはあまり便利ではありません。 より良い解決策は、ハイパーキューブを視覚的なグラフィカルな形式で表示できるディスプレイ インターフェイスの実装を持つことです。 その明らかなシンプルさそれにも関わらず、タスクは準備ステップと大量のルーチンコーディングを必要とします。 したがって、この記事の後半で検討します。


結論

この記事では、トレード操作のヒストリーに適用されるビッグデータ(OLAP)のオンライン分析のよく知られたアプローチを概説しました。 MQLを使用して、選択した変数(セレクタ)に基づいて仮想ハイパーキューブを生成し、そのベース上でさまざまな集計値を生成できるようにする基本的なクラスを実装しました。 このメカニズムは、プロセス最適化結果に適用することができ、選択された基準に従ってトレードシグナルを選択し、大量のデータ量が意思決定の抽出アルゴリズムの利用を必要とします。

添付ファイル: