English Русский 中文 Español Deutsch 日本語
Implementado OLAP na negociação (Parte 1): Noções básicas da análise de dados multidimensionais

Implementado OLAP na negociação (Parte 1): Noções básicas da análise de dados multidimensionais

MetaTrader 5Negociação | 4 junho 2019, 09:14
2 391 0
Stanislav Korotky
Stanislav Korotky

Os traders geralmente precisam analisar quantidades significativas de dados que, normalmente, são números, isto é: cotações, leituras de indicadores, resultados de relatórios de negociação. Devido ao grande número de parâmetros e condições de que esses números dependem, é melhor lidar com eles segundo o princípio de dividir e conquistar, isto é, em partes, e examiná-los de vários ângulos. De certo modo, toda a informação forma um hipercubo virtual, no qual cada parâmetro define sua dimensão de maneira perpendicular ao resto. Para processar e analisar tais hipercubos, existe OLAP (em inglês, Online Analytical Processing).

A palavra 'online' no título se refere à prontidão para obter resultados. O princípio de ação consiste no cálculo preliminar das células do hipercubo, para, depois, rapidamente extrair e ver qualquer seção transversal do cubo. Por exemplo, isso pode ser comparado com o processo de otimização no MetaTrader: primeiro, o testador calcula as variantes de negociação (o que pode levar muito tempo) e, em seguida, é obtido um relatório que mostra as leituras de indicadores em relação aos parâmetros de entrada. A partir do build 1860, o MetaTrader 5 permite que você altere dinamicamente os resultados de otimização visualizados, alternando vários critérios de otimização. Isso nos aproxima da ideia do OLAP. Mas, para uma análise completa, seria bom poder selecionar rapidamente muitas outras seções do hipercubo.

Hoje tentaremos implementar a abordagem do OLAP no MetaTrader e implementar análise multidimensional usando ferramentas MQL. Antes de começar, é necessário determinar quais dados analisaremos (por exemplo, relatórios de negociação, resultados de otimização, leituras de indicadores). Em princípio, nossa escolha nesta etapa não é tão importante, porque a estrutura a ser desenvolvida deve ser um mecanismo universal orientado a objetos aplicável a qualquer dado. No entanto, precisaremos executá-lo em algo concreto, e como uma das tarefas mais populares é a análise de relatórios de negociação, vamos nos debruçar sobre ela.

Para um relatório de negociação, pode ser interessante distinguir os lucros (por símbolos, por dias da semana, por compras e vendas) ou comparar as leituras de vários robôs de negociação (separadamente para cada número mágico). Neste ponto, surge a pergunta: será possível combinar essas seções (símbolos por dias da semana em relação a EAs) ou adicionar algum outro tipo de agrupamento? Sim, tudo isso pode ser feito usando o OLAP.

Arquitetura

A abordagem orientada a objetos implica que é necessário realizar uma decomposição da tarefa num conjunto de partes logicamente relacionadas simples, em que cada parte executa sua própria função com base nos dados recebidos, no estado interno e em alguns conjuntos de regras.

A primeira classe requerida é um registro com os dados de origem — Record. Tal registro pode armazenar informações, por exemplo, sobre uma única operação de negociação ou sobre um única execução da otimização.

Um registro é um vetor com um número arbitrário de campos. Como se trata de uma entidade abstrata, neste caso, não importa o que significa cada campo. Para cada uso específico, é criada uma classe derivada que conhece o propósito dos campos e os processa apropriadamente.

Para ler registros da fonte abstrata (pode ser de um histórico de conta de negociação, de um arquivo CSV, de um relatório HTML oude dados da Internet obtidos usando o WebRequest), é necessária outra classe — o adaptador de dados (DataAdapter). Neste nível, ele é capaz de executar apenas uma função — sequencialmente iterar registros e fornecer acesso a eles. Posteriormente, para cada uso do mundo real, podemos criar classes derivadas que preencham arrays de registros a partir das fontes respectivas.

Todos os registros devem, de alguma forma, ser exibidos nas células do hipercubo. Ainda não sabemos como fazer isso, mas essa é a essência de todo o projeto — distribuir os valores de entrada dos campos de registros segundo as células do cubo e calcular - para eles - estatísticas generalizadas usando as funções de agregação selecionadas.

Num nível básico, um cubo fornece apenas propriedades básicas, como o número, os nomes e o tamanho de cada dimensão. Tudo isso está na classe MetaCube.

Classes derivadas preenchem as células com estatísticas específicas. Como exemplos simples de agregadores específicos, pode-se citar a soma de todos os valores ou a média do mesmo campo em todos os registros, mas, de fato, existem muitos mais tipos de agregadores.

Para que os valores sejam agregados nas células, cada registro deve receber um conjunto de índices que o exibem de maneira idêntica numa determinada célula do cubo. Nós delegamos essa subtarefa a uma classe especial — seletor (Selector). O seletor corresponde a um lado (eixo, coordenada) do hipercubo.

A classe base abstrata do seletor fornece uma interface para definir um conjunto de valores válidos e para exibir cada entrada num desses valores. Por exemplo, se necessário dividir registros por dias da semana, a classe derivada do seletor deve retornar o número do dia da semana — um número de 0 a 6. O número de valores válidos para um determinado seletor define o tamanho do cubo para essa dimensão. No caso de um dia da semana, isso é obviamente 7.

Além disso, algumas vezes é útil filtrar alguns registros, portanto, precisamos de uma classe de filtro (Filter). Ela é muito semelhante ao seletor, mas impõe restrições adicionais aos valores aceitáveis. Por exemplo, continuando com o exemplo do seletor por dias da semana, poderíamos criar um filtro - baseado nele - em que podemos indicar quais dias excluir ou incluir nos cálculos.

Uma vez construído o cubo (ou seja, já calculadas as funções agregadas de todas as células), é preciso visualizar e analisar o resultado. Para isso, reservamos uma classe especial — Display.

Finalmente, para conectar todas as classes mencionadas de maneira coerente, criamos um centro de controle — a classe Analyst

Todas juntas em notação UML ficam assim (um tipo de plano de ação que pode ser verificado em qualquer estágio de desenvolvimento).

Online Analytical Processing em MetaTrader

Online Analytical Processing em MetaTrader

Aqui são omitidas algumas classes, mas, em geral, de acordo com este esquema, já é possível formar uma idéia (com base na qual é suposto construir um hipercubo) e saber quais funções de agregação estão disponíveis para cálculo nas células.

Implementação de classes base

Comecemos a implementar as classes descritas acima. Primeiro, a classe Record.

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

Ela simplesmente armazena valores arbitrários no array (no vetor) data. O comprimento do vetor é definido no construtor.

Lemos os registros de diferentes fontes usando DataAdapter.

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

O método getNext deve ser chamado num loop até retornar NULL (o que significa que não há mais entradas). Todos os registros recebidos devem ser salvos em algum lugar (lidaremos com isso um pouco abaixo). O método reservedSize permite otimizar a alocação de memória (se o número de registros na origem for conhecido antecipadamente).

Cada dimensão do hipercubo é calculada com base num ou mais campos de registros. É conveniente designar cada campo como um elemento de uma enumeração. Por exemplo, ao analisar um histórico de conta de negociação, pode-se propor a listagem a seguir.

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

Os dois últimos campos são fornecidos para o cálculo de indicadores não padronizados.

Se analisarmos os resultados da otimização do MetaTrader, poderíamos usar tal enumeração.

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

Para cada aplicação, é necessário desenvolver uma listagem própria. Ela pode logo ser usada como um parâmetro da classe genérica Seletor.

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

O campo selector armazena um valor — o elemento de enumeração. Por exemplo, se for usado TRADE_RECORD_FIELDS, pode-se criar um seletor para operações de compra/venda como este:

  new Selector<TRADE_RECORD_FIELDS>(FIELD_TYPE);

O campo _typename é auxiliar. Ele é sobrescrito em todas as classes derivadas para identificar seletores, o que é útil ao visualizar os resultados. O campo é usado no método virtual getTitle.

O trabalho principal é executado pela classe no método select. É aqui que cada registro de entrada é mostrado num valor de índice específico ao longo do eixo de coordenadas formado pelo seletor atual. O índice deve estar no intervalo entre os valores retornados pelos métodos getMin e getMax, enquanto o número total de índices deve ser igual ao número retornado pelo método getRange. Se, por algum motivo, esse registro não puder ser mostrado corretamente no escopo do seletor, o método select retornará false. Se a exibição for bem-sucedida, retornará true.

O método getLabel retorna uma descrição clara de um índice específico. Por exemplo, para operações de compra/venda, o índice 0 deve gerar "buy", enquanto o índice 1 — "sell".

Implementando classes de seletores específicos e adaptador de dados para o histórico de negociação

Como vamos nos concentrar na análise do histórico de negociação, introduzimos uma classe intermediária de seletores com base na enumeração 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; // this is a scalar by default, returns 1 value
      }
      
      virtual double getMin() const
      {
        return 0;
      }
      
      virtual double getMax() const
      {
        return (double)(getRange() - 1);
      }
      
      virtual string getLabel(const int index) const
      {
        return EnumToString(selector) + "[" + (string)index + "]";
      }
  };

Por padrão, ela exibe todas as entradas na mesma célula. Com esse seletor, pode-se, por exemplo, obter o lucro total.

Agora, com base nesse seletor, é fácil determinar tipos de derivativos específicos de seletores, em particular para dividir registros por tipo de operação (compra/venda).

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

Definimos uma classe usando o elemento FIELD_TYPE no construtor. O método getRange retorna 2, pois aqui temos apenas dois valores possíveis: OP_BUY ou OP_SELL. Os métodos getMin e getMax retornam as constantes correspondentes. O que deve estar no método select?

Para responder a essa pergunta, é preciso decidir quais informações serão armazenadas em cada registro. Fazemos isso com a ajuda da classe TradeRecord, derivada de Record e adaptada para trabalhar com histórico de negociação.

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

O método auxiliar fillByOrder mostra como a maioria dos campos de registro pode ser preenchida com base na ordem atual. Naturalmente, a ordem deve ser pré-selecionada em outro lugar no código. Aqui usamos a notação das funções de negociação do MetaTrader 4 e fornecemos suporte para o MetaTrader 5 incluindo a biblioteca MT4Orders (uma das versões é anexada ao final do artigo, sempre verifique e baixe a versão atual) — assim, conseguiremos um código de plataforma cruzada.

O número de campos TRADE_RECORD_FIELDS_NUMBER pode ser inserido no código como uma definição de macro ou ser calculado dinamicamente com base na capacidade da enumeração TRADE_RECORD_FIELDS. No código fonte anexado, é usada a segunda abordagem, para isso, é usada uma função de modelo especial EnumToArray.

Como se pode ver no código do método fillByOrder, o campo FIELD_TYPE é preenchido com o tipo de operação a partir de OrderType. Agora podemos retornar para a classe TypeSelector e implementar seu método select.

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

Aqui, lemos o valor do campo (selector) do registro (r) enviado para a entrada e atribuímos seu valor (que pode ser OP_BUY ou OP_SELL) ao parâmetro de saída index. São levadas em conta apenas ordens a mercado, por isso, para todos os outros tipos é retornado false. Nós vamos considerar outros tipos de seletores mais tarde.

É hora de desenvolver um adaptador de dados para o histórico de negociação. Esta é uma classe na qual os registros TradeRecord são gerados com base no histórico real da conta de negociação.

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

O adaptador passa sequencialmente por todas as ordens no histórico e cria uma instância TradeRecord para cada ordem a mercado. O código dado aqui é um pouco simplificado. Na prática, talvez seja necessário criar objetos que não sejam da classe TradeRecord, mas, sim, de alguma derivada dela, especialmente porque na enumeração TRADE_RECORD_FIELDS reservamos dois campos personalizados. A esse respeito, a classe HistoryDataAdapter é genérica, enquanto o parâmetro do modelo é a classe atual dos objetos-registros gerados. Para preencher campos personalizados na classe Record, é fornecido um método virtual vazio:

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

A implementação completa da abordagem pode ser estudada independentemente: o kernel usa a classe CustomTradeRecord (herdada de TradeRecord), que no método fillCustomFields calcula MFE (Maximum Favorable Excursion, máximo lucro flutuante) e MAE (Maximum Adverse Excursion, máxima perda flutuante) como uma porcentagem para cada posição e grava-os respectivamente nos campos FIELD_CUSTOM1 e FIELD_CUSTOM2.

Implementando agregadores e uma classe de controle

Naturalmente, é preciso criar o adaptador em algum lugar e chamar seu método getNext. Assim, chegamos ao 'centro de controle' — classe Analyst. Além de iniciar o adaptador, ele deve armazenar os registros recebidos no array interno.

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

A classe não cria o próprio adaptador, mas, sim, o aceita como um parâmetro do construtor. Este é um princípio de design chamado injeção de dependência (em inglês, dependency injection). Ele permite separar o Analyst da implementação específica do DataAdapter. Em outras palavras, podemos substituir livremente várias variantes de adaptadores sem a necessidade de modificações na classe Analyst.

A classe Analyst agora é capaz de preencher a matriz interna de registros, mas ainda não sabe como executar a função principal — agregar dados. Ela não fará isso sozinha, mas, sim, delegará a tarefa ao agregador.

Lembre-se de que agregadores são classes que podem calcular leituras predefinidas (estatísticas) para campos selecionados de registros. A classe base dos agregadores será o MetaCube — repositório baseado num array multidimensional.

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

A matriz dimensions descreve a estrutura do hipercubo. Seu tamanho é igual ao número de seletores usados (medições). Cada elemento da matriz dimensions contém o tamanho do cubo em dada dimensão, o que é determinado pelo intervalo de valores do seletor correspondente. Por exemplo, se queremos ver os lucros por dia da semana, precisamos criar um seletor que retorne o número do dia como um índice de 0 a 6, de acordo com a hora de abertura/fechamento da ordem (posição). Como esse é o único seletor, a matriz dimensions terá 1 elemento e seu valor será 7. Se quisermos adicionar outro seletor, por exemplo, TypeSelector, descrito anteriormente, para ver os lucros por dia da semana e por tipo de operação, a matriz dimensions conterá 2 elementos com os valores 7 e 2. Isso também significa que haverá 14 células com estatísticas no hipercubo.

Diretamente, uma matriz com todos os valores (no exemplo considerado — 14 valores) está contida nos totals. Como o hipercubo é multidimensional, à primeira vista parece estranho que o array seja declarado com uma dimensão. Isso é feito porque não sabemos de antemão as dimensões do hipercubo que o usuário deseja adicionar. Além disso, a MQL não suporta arrays multidimensionais em que todas as dimensões sejam distribuídas dinamicamente. Portanto, é usado o usual array 'planar' (vetor), além disso, para armazenar células nele em várias dimensões, é necessário aplicar uma indexação astuta. Em seguida, mostramos como os deslocamentos são calculados para cada dimensão.

A classe base não aloca e não inicializa arrays — tudo isso é deixado para as classes derivadas.

Como todos os agregadores terão muitos recursos comuns, nós os compactaremos numa única classe intermediária.

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

Cada agregador processa um campo específico de registros que é especificado na classe, na variável field, que é preenchida no construtor (veja Por exemplo, isso pode ser um lucro (FIELD_PROFIT_AMOUNT).

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

Os cálculos são realizados num espaço multidimensional formado por um número arbitrário de seletores (selectorCount). Anteriormente, consideramos um exemplo de cálculo de lucros por dia da semana e por tipo de operação, o que requeria dois seletores. Eles são armazenados no array de links de selectors. Diretamente, os objetos dos seletores são passados novamente como parâmetros do construtor.

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

Como você se lembra, o array para armazenar os valores totals calculados é unidimensional. Para converter os índices do espaço multidimensional dos seletores no deslocamento no array unidimensional, é usada a função a seguir.

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

Ela pega um array com índices como entrada e retorna o número do elemento. Aqui é usado um array offsets, que por este ponto já deve ser preenchida. Sua inicialização - um dos pontos-chave - é realizada no método 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
        }
      }

Seu objetivo consiste em obter os intervalos e multiplicação sequencial de todos os seletores, uma vez que isso determina o número de elementos através dos quais é necessário "saltar" ao ter coordenadas crescentes de um em cada dimensão do hipercubo.

O cálculo dos indicadores agregados é realizado no método de calculate, diretamente.

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

Esse método é chamado para um array de registros. Cada registro no loop é passado alternadamente para cada seletor e, se ele for exibido com sucesso nos índices válidos em todos os seletores (cada seletor tem seu próprio índice), o conjunto completo de índices será armazenado no array local k. Se todos os seletores tiverem índices definidos, será chamado o método de atualização. Ele aceita um deslocamento no array totals como entrada (o deslocamento é calculado pela função mixIndex mostrada anteriormente) e o valor do campo especificado field (especificado no agregador) do registro atual. Se continuarmos o exemplo com a análise da distribuição de lucros, a variável field será igual a FIELD_PROFIT_AMOUNT, e os valores deste campo serão retirados da chamada OrderProfit().

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

O método update é abstrato nesta classe e deve ser redefinido nos herdeiros.

Finalmente, o agregador deve fornecer pelo menos um método para acessar os resultados dos cálculos. O mais simples deles é obter o valor de uma célula específica usando um conjunto de índices completo.

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

A classe base Aggregator faz quase todo o trabalho bruto. Agora podemos implementar muitos agregadores específicos com um mínimo de esforço.

Mas, primeiro, vamos retornar brevemente à classe Analyst e adicioná-la ao agregador, que também será passado pelo parâmetro do construtor.

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

No método acquireData, ajustamos as dimensões do hipercubo usando a chamada adicional para o método setSelectorBounds do agregador.

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

A tarefa principal, isto é, o cálculo de todos os valores do hipercubo, como fornecido, é delegada ao agregador (já consideramos o método de cálculo acima, e aqui a matriz de registros é passada para ele).

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

Mas esta não é toda a classe Analyst. Anteriormente, planejamos dar a capacidade de exibir os resultados, criando-a como uma interface de exibição especial. A interface se conecta ao Analyst de maneira semelhante (passando a referência para o construtor):

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

A composição Display é bem simples:

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

Ele contém um método virtual abstrato que aceita um hipercubo como uma fonte de dados de entrada. Aqui, por brevidade, alguns parâmetros que afetam a ordem dos valores de impressão são omitidos. As nuances da visualização e as configurações adicionais necessárias aparecerão nas classes derivadas.

Para testar o desempenho de classes analíticas, precisamos ter pelo menos uma implementação da interface Display. Vamos criá-la enviando mensagens para o log do EA e chamá-la de LogDisplay. Ele percorrerá o hipercubo em todas as coordenadas e imprimirá os valores agregados junto com as coordenadas correspondentes, aproximadamente da seguinte maneira.

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

Eu escrevi 'aproximadamente', porque, na verdade, para uma formatação mais legível de logs, a implementação do LogDisplay é um pouco mais complicada, mas não é essencial. A versão completa da classe está disponível nos códigos fonte.

Naturalmente, seja como for, isso não é tão claro como um gráfico, mas a construção de imagens bidimensionais ou tridimensionais é uma história totalmente separada, por isso, deixamos por algum tempo nos bastidores, especialmente porque se podem aplicar várias tecnologias: objetos, telas, bibliotecas gráficas externas, incluindo aqueles construídos em tecnologias web.

Assim, temos uma classe base Aggregator. É fácil obter - com base nele - várias classes derivadas com cálculos específicos de indicadores agregados no método update. Em particular, para calcular a soma dos valores extraídos por um determinado seletor a partir de todos os registros, basta escrever:

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

Para calcular a média, é preciso apenas de uma complicação menor:

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

Tendo considerado, finalmente, todas as classes envolvidas, vamos generalizar seu o algoritmo de interação:

  • Criamos um objeto HistoryDataAdapter;
  • Criamos vários seletores específicos (cada seletor é anexado a pelo menos um campo, por exemplo, ao tipo de uma operação de negociação) e salvamos num array;
  • Criamos um objeto de um agregador específico, por exemplo, SumAggregator, passando-lhe um array de seletores e a designação de campo de acordo com a qual a agregação deve ser realizada;
  • Criamos um objeto LogDisplay;
  • Criamos um objeto Analyst usando objetos do adaptador, do agregador e da exibição;
  • Chamamos sequencialmente:
      analyst.acquireData();
      analyst.build();
      analyst.display();
  • No final, não esquecemos de apagar objetos.

Caso especial: seletores dinâmicos

Nosso programa está quase pronto. Mas há uma pequena omissão, que foi feita intencionalmente para simplificar a apresentação, e agora é hora de eliminá-la.

Todos os seletores com quem nos encontramos até agora têm um intervalo constante de valores. Por exemplo, há sempre 7 dias por semana, enquanto as ordens de mercado são comprar ou vender. No entanto, o intervalo pode não ser conhecido antecipadamente e isso é bastante comum.

O desejo de construir um hipercubo no contexto de símbolos de trabalho ou números mágicos de EA é bem justificado. Para resolver esse problema, precisamos primeiro coletar todos os símbolos únicos ou mágicos em alguma matriz interna e, em seguida, usar seu tamanho como o intervalo do seletor correspondente.

Criamos a classe Vocabulary para gerenciar esse tipo de arrays internos e mostramos como usá-la, por exemplo, em conjunto com a classe SymbolSelector.

Nossa implementação do dicionário é bastante simples (você pode substituí-la por outra).

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

Reservamos o array index para armazenar valores exclusivos.

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

O método get permite verificar se no array já existe um valor específico. Se houver, o método retornará o índice encontrado. Caso contrário, o método retornará o tamanho atual do array aumentado em 1 com um sinal de menos. Isso permite que otimizar um pouco o método a seguir para adicionar um novo valor ao array.

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

Também devemos fornecer métodos para obter o tamanho do array e os valores armazenados nele por índice.

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

Como os símbolos de trabalho em nosso caso são analisados no contexto de ordens (posições), vamos incorporar o dicionário na classe TradeRecord.

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

O dicionário é descrito como uma variável estática, pois ele é comum para todo o histórico de negociação.

Agora podemos implementar o SymbolSelector.

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

O seletor para números mágicos é organizado de maneira semelhante.

A lista geral de seletores fornecidos inclui (entre parênteses é indicada a necessidade de ligação externa ao campo, e se for omitida, isso significa que dentro da classe do seletor já existe uma ligação a um campo específico):

  • TradeSelector (qualquer campo) — escalar, um valor (a generalização de todos os registros no caso de agregadores "reais" ou o valor do campo de um registro específico no caso de IdentityAggregator (consulte a seguir));
  • TypeSelector — compra ou venda dependendo de OrderType();
  • WeekDaySelector (campo do tipo datetime) — dia da semana, por exemplo, em OrderOpenTime() ou OrderCloseTime();
  • DayHourSelector (campo do tipo datetime) — hora dentro do dia;
  • HourMinuteSelector (campo do tipo datetime) — minuto dentro da hora;
  • SymbolSelector — símbolo de trabalho, índice no dicionário de OrderSymbol() exclusivos;
  • SerialNumberSelector — número de sequência do registro (ordem);
  • MagicSelector — número mágico, índice no dicionário de OrderMagicNumber() exclusivos;
  • ProfitableSelector — true = lucro, false = perda, do campo OrderProfit();
  • QuantizationSelector (campo do tipo double) — dicionário de valores arbitrários do tipo double (por exemplo, tamanhos de lotes);
  • DaysRangeSelector — exemplo de seletor de usuário em dois campos do tipo datetime (OrderCloseTime() e OrderOpenTime()), construído com base na classe DateTimeSelector — ancestral comum de todos os seletores para o campo do tipo datetime; ao contrário de outros tipos de seletores definidos no kernel, este tipo de seletor é implementado num EA de demonstração (consultar a seguir).

O seletor SerialNumberSelector é significativamente diferente de outros. Seu alcance é igual ao número total de registros. Isso permite criar um hipercubo no qual os próprios registros são contados sequencialmente de acordo com uma das dimensões (geralmente a primeira, ou seja, X), e os campos especificados são copiados de acordo com a segunda. Os campos são definidos por seletores: em seletores especializados, já existe uma ligação ao campo, e, se necessário um campo para o qual não há seletor pronto, como swap, pode ser usado o TradeSelector universal. Em outras palavras, usando SerialNumberSelector, é possível ler os dados iniciais dos registros dentro da metáfora de um hipercubo de agregação. O pseudo-agregador IdentityAggregator se destina a esse propósito (consulte a seguir).

Entre os agregadores estão disponíveis:

  • SumAggregator — soma dos valores do campo;
  • AverageAggregator — valor médio do campo;
  • MaxAggregator — valor máximo do campo;
  • MinAggregator — valor mínimo do campo;
  • CountAggregator — número de registros;
  • ProfitFactorAggregator — razão entre a soma dos valores dos campos positivos e a soma dos valores dos campos negativos;
  • IdentityAggregator (SerialNumberSelector по оси X) — tipo especial de seletor para copiar de forma transparente os valores dos campos para um hipercubo, sem agregação;
  • ProgressiveTotalAggregator (SerialNumberSelector по оси X) — total cumulativo para o campo;

Os dois últimos agregadores são diferentes dos restantes. Quando selecionado IdentityAggregator, o tamanho do hipercubo é sempre 2. No primeiro eixo X, deve-se iterar os registros usando um SerialNumberSelector e, no segundo eixo, cada contagem (na verdade, um vetor/coluna) corresponde a um seletor, por meio do qual é determinado o campo a ser lido a partir dos registros de origem. Assim, se houver 3 seletores adicionais (além de SerialNumberSelector), então, ao longo do eixo Y, haverá 3 contagens. No entanto, a dimensão do cubo ainda é 2: os eixos X e Y. Lembre-se de que no modo geral o hipercubo é construído sobre um princípio diferente: cada seletor corresponde à sua dimensão, portanto, se houver 3 deles, haverá 3 eixos.

O agregador ProgressiveTotalAggregator também interpreta a primeira dimensão de maneira especial. Como o próprio nome indica, ele permite calcular um total cumulativo e faz isso ao longo do eixo X. Por exemplo, se, no parâmetro deste agregador, especificarmos o campo lucro, obteremos a curva de saldo geral. Se no segundo seletor (ao longo do eixo Y) especificamos uma divisão por símbolos (SymbolSelector), obtemos várias [N] curvas de balanço — cada uma para seu próprio símbolo. Se o MagicSelector for o segundo seletor, obteremos curvas de saldo [M] separadas de diferentes EAs. Mas é possível combinar as duas divisões escolhendo SymbolSelector por Y e MagicSelector por Z (ou vice-versa), assim, obtemos [N*M] das curvas de saldo separadas pelo sinal do símbolo de trabalho e pelo código do EA.

Em princípio, o mecanismo OLAP está pronto para ser usado. Para reduzir a apresentação, omitimos algumas das nuances. Por exemplo, o artigo não descreve os filtros (Filter, FilterRange classes) que foram apresentados na arquitetura. Além disso, nosso hipercubo pode fornecer valores agregados não apenas um por um (o método getValue(const int &indices[])), mas também como um vetor — para isso é implementado o método:

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

O parâmetro de saída aqui é a classe especial PairArray. Ele contém um array de estruturas com pares [valor;nome]. Por exemplo, se construirmos um cubo com lucro por símbolos, cada soma corresponderá a um símbolo específico e, portanto, seu nome será indicado no par ao lado do valor. Como pode ser visto no protótipo do método, o kernel é capaz de classificar o PairArray em diferentes modos — ascendente ou descendente, por valores ou por rótulos:

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

A classificação é suportada apenas no hipercubo unidimensional. Hipoteticamente, ele pode ser implementado para uma dimensão arbitrária, mas isso é um trabalho bastante rotineiro. Quem estiver interessado pode fazer isso.

Códigos de fonte completos estão anexados.

Exemplo OLAPDEMO

Tetemos implementar o hipercubo. Para fazer isso, criaremos um EA não comercial capaz de processar analiticamente o histórico de trades da conta. Vamos chamá-lo de OLAPDEMO. Incluímos um arquivo de cabeçalho contendo todas as classes principais do OLAP.

  #include <OLAPcube.mqh>

Embora o hipercubo possa processar um número arbitrário de dimensões, por simplicidade, limitamos o número a três. Isso significa que o usuário pode selecionar até 3 seletores ao mesmo tempo. Denotamos os tipos de seletores suportados por elementos de enumeração especiais:

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

Usamos a enumeração para descrever as variáveis de entrada que configuram os seletores:

  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;

No grupo cada seletor possui uma variável de entrada para especificar um campo de registro opcional (alguns seletores requerem campos, outros não).

Especificamos um filtro (embora, de fato, possa haver muitos), e ele estará desativado por padrão.

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

O objetivo do filtro é levar em conta apenas os registros nos quais o campo Filter1Field especificado possui um valor Filter1value1 específico (Filter1value2 deve ser o mesmo, pois somente nessas condições o exemplo cria um objeto Filter). Tenha em mente que, para campos como símbolo ou número mágico, o valor indica um índice no dicionário correspondente. Opcionalmente, o filtro pode incluir um intervalo de valores entre Filter1value1 e Filter1value2 (se eles não forem iguais é porque somente neste caso é criado um objeto FilterRange). Esta implementação é projetada para mostrar a própria possibilidade de filtragem e deixa um contexto mais alargado para melhorar o uso prático a interface do usuário.

Para se referir os agregadores, descreveremos outra listagem:

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

Nós vamos usá-la no grupo de variáveis de entrada que descrevem o agregador de trabalho:

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

Todos os seletores, incluindo aqueles usados no filtro opcional, são inicializados em 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 é executado apenas uma vez, de acordo com o temporizador.

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

A função auxiliar createSelector é definida da seguinte forma.

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

Todas as classes, exceto DaysRangeSelector, são importadas do arquivo de cabeçalho, e DaysRangeSelector é descrito no próprio EA OLAPDEMO, como a seguir.

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

Este é um exemplo de implementação de um seletor de usuário. Ele agrupa posições de negociação por duração no mercado, em dias.

Ao executar o EA numa conta e selecionar 2 seletores — SymbolSelector e WeekDaySelector, pode-se obter resultados no log como estes:

	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
	     ...

Neste caso, na conta foram negociados 5 símbolos diferentes. Tamanho do hipercubo — 35 células. Todas as combinações de símbolos e dias da semana são listadas junto com a quantia correspondente de lucros/perdas. Note que WeekDaySelector requer uma indicação explícita do campo, uma vez que cada posição tem duas datas — a abertura (FIELD_DATETIME1) e o fechamento (FIELD_DATETIME2). Aqui escolhemos FIELD_DATETIME2.

Para analisar não apenas o histórico da conta atual, mas também relatórios arbitrários de negociação em formato HTML, bem como arquivos CSV com o histórico de sinais MQL5, as classes de meus artigos anteriores foram adicionadas à biblioteca OLAP Extrair dados estruturados de páginas HTML usando seletores CSS e Visualização do histórico de negociação multimoeda em relatórios em HTML e CSV. Para sua integração com OLAP, foram escritas classes de camada. Em particular, o arquivo de cabeçalho HTMLcube.mqh contém a classe de registros de negociação HTMLTradeRecord e o adaptador HTMLReportAdapter herdado de DataAdapter. O arquivo de cabeçalho CSVcube.mqh é a classe de registro CSVTradeRecord e o adaptador CSVReportAdapter, respectivamente. A leitura de HTML é fornecida pelo WebDataExtractor.mqh e a leitura do CSV, pelo CSVReader.mqh.csv Parâmetros de entrada para download de relatórios e princípios gerais (incluindo a seleção de caracteres de trabalho adequados usando prefixos e sufixos se o relatório de outra pessoa contiver símbolos ausentes na sua conta) são descritos detalhadamente no segundo artigo mencionado.

Por exemplo, aqui está a análise de um dos sinais (carregados como um arquivo CSV) com a ajuda de um agregador pelo fator de lucratividade dividido por símbolos, classificado por índice descendente:

	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"

O valor inf é gerado no código fonte quando há lucro e não há perda. Como você pode ver, a comparação de valores reais e sua ordenação é feita de tal maneira que o "infinito" é maior do que qualquer outro número finito.

É claro que visualizar os resultados da análise de relatórios reais de negociação nos registros não é muito conveniente. Seria muito melhor ter uma implementação da interface Display que mostrasse o hipercubo numa forma visual. Esta tarefa, apesar da sua aparente simplicidade, requer preparação e uma grande quantidade de codificação de rotina. A este respeito, vamos considerá-la numa segunda parte.


Fim do artigo

O artigo descreve uma abordagem conhecida para a análise operacional de big data (OLAP) aplicada ao histórico de operações de negociação. Com a ajuda da MQL, implementamos classes fundamentais que nos permitem construir um hipercubo virtual no espaço de recursos selecionados (seletores) e calcular, em sua seção, vários indicadores agregados. Esse mecanismo também pode ser implementado para refinar e decifrar os resultados de otimização, para selecionar sinais de negociação por critério e, em outras áreas em que uma grande quantidade de dados requer o uso de algoritmos de extração de conhecimento, para tomar decisões.

Arquivos anexados:

  • Experts/OLAP/OLAPDEMO.mq5 — EA de demonstração;
  • Include/OLAP/OLAPcube.mqh — arquivo de cabeçalho principal com classes OLAP;
  • Include/OLAP/PairArray.mqh — classe do array de pares [valor;nome] com suporte para todas as variantes de classificação;
  • Include/OLAP/HTMLcube.mqh — interface OLAP com carregamento de dados de relatórios HTML;
  • Include/OLAP/CSVcube.mqh — interface OLAP com o carregamento de dados de relatórios CSV;
  • Include/MT4orders.mqh — biblioteca MT4orders para trabalhar com ordens usando o mesmo estilo em МТ4 e em МТ5;
  • Include/Marketeer/WebDataExtractor.mqh — analisador HTML;
  • Include/Marketeer/empty_strings.h — lista de tags HTML vazias;
  • Include/Marketeer/HTMLcolumns.mqh — definições de índices de colunas em relatórios HTML;
  • Include/Marketeer/CSVReader.mqh — analisador CSV;
  • Include/Marketeer/CSVcolumns.mqh — definições de índice de colunas em relatórios CSV;
  • Include/Marketeer/IndexMap.mqh — arquivo de cabeçalho auxiliar com implementação de array com acesso combinado por chave e índice;
  • Include/Marketeer/RubbArray.mqh — arquivo de cabeçalho auxiliar com array de borracha;
  • Include/Marketeer/TimeMT4.mqh — arquivo de cabeçalho auxiliar com implementação de funções para trabalhar com datas usando o estilo de MT4;
  • Include/Marketeer/Converter.mqh — arquivo de cabeçalho auxiliar com união para converter tipos de dados;
  • Include/Marketeer/GroupSettings.mqh — arquivo de cabeçalho auxiliar para configurar o grupo de parâmetros de entrada;

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/6602

Arquivos anexados |
MQLOLAP1.zip (50.46 KB)
Criando interfaces gráficas baseadas no .Net Framework e C# (Parte 2): elementos gráficos adicionais Criando interfaces gráficas baseadas no .Net Framework e C# (Parte 2): elementos gráficos adicionais
O artigo é uma continuação lógica da publicação anterior "Criando interfaces gráficas para EAs e indicadores baseados no .Net Framework e C#" e introduz os leitores a novos elementos gráficos para criar interfaces gráficas.
Estudo de técnicas de análise de velas (parte IV): Atualizações e adições ao Pattern Analyzer Estudo de técnicas de análise de velas (parte IV): Atualizações e adições ao Pattern Analyzer
O artigo apresenta uma nova versão do aplicativo Pattern Analyzer. Esta versão fornece correções de bugs e novos recursos, bem como a interface de usuário revisada. Os comentários e sugestões do artigo anterior foram levados em conta no desenvolvimento da nova versão. A aplicação resultante é descrita neste artigo.
Implementado OLAP na negociação (Parte 2): Visualizando resultados da análise interativa de dados multidimensionais Implementado OLAP na negociação (Parte 2): Visualizando resultados da análise interativa de dados multidimensionais
O artigo discute diversos aspectos da criação de interfaces gráficas interativas de programas MQL projetados para processamento analítico online (OLAP) do histórico de contas e de relatórios de negociação. Para obter um resultado visual, são usadas janelas maximizadas e escaláveis, uma disposição adaptável de controles de borracha e um novo 'controle' para exibir diagramas. Com base nisso, é implementada uma GUI com a possibilidade de escolher indicadores ao longo dos eixos de coordenadas, funções de agregação, tipos de gráficos e classificações.
Biblioteca para o desenvolvimento fácil e rápido de programas para a MetaTrader (parte IV): eventos de negociação Biblioteca para o desenvolvimento fácil e rápido de programas para a MetaTrader (parte IV): eventos de negociação
Nos artigos anteriores, nós começamos a criar uma grande biblioteca multi-plataforma, simplificando o desenvolvimento de programas para as plataformas MetaTrader 5 e MetaTrader 4. Nós já temos as coleções do histórico de ordens e negócios, ordens e posições de mercado, bem como a classe para a seleção conveniente e ordenação das ordens. Nesta parte, nós continuaremos com o desenvolvimento do objeto base e ensinaremos a Biblioteca Engine a monitorar os eventos de negociação na conta.