English Русский 中文 Deutsch 日本語 Português
Aplicando OLAP en el trading (parte 1): Fundamentos del análisis corriente de datos multidimensionales

Aplicando OLAP en el trading (parte 1): Fundamentos del análisis corriente de datos multidimensionales

MetaTrader 5Trading | 8 julio 2019, 08:50
1 403 0
Stanislav Korotky
Stanislav Korotky

A menudo los traders necesitan analizar importantes volúmenes de datos. Normalmente, se trata de los números: cotizaciones, valores de los indicadores, resultados de los informes comerciales. Debido a una gran cantidad de parámetros y condiciones de las cuales estos números dependen, es mejor usar el principio «divide y domina», es decir, por partes, examinándolos de varios ángulos. En cierto sentido, el volumen entero de la información forma un hipercubo virtual donde cada parámetro define su dimensión de manera perpendicular al resto. Para procesar y analizar estos hipercubos se usa una tecnología bien conocida OLAP (en inglés, Online Analytical Processing).

La palabra «online» en el título no tiene nada que ver con la red Internet, y significa la rapidez (o interactividad) de la obtención de la información. El principio de la acción consiste en el cálculo preliminar de las células del hipercubo, y después de eso, se puede extraer rápidamente y visualizar cualquier sección transversal del cubo. Por ejemplo, se puede comparar eso con el proceso de la optimizacción en MetaTrader: primero, el Simulador de Estrategias calcula las variantes del trading (eso puede requerir bastante tiempo), y luego, obtenemos el informe que resume los valores de los indicadores en relación con los parámetros de entrada. A partir de la versión 1860, MetaTrader 5 permite cambiar dinámicamente los resultados de la optimización optimizados, alternando diferentes criterios de la optimización. Eso nos acerca a la idea de OLAP. Pero para un análisis completo sería bueno tener la posibilidad de seleccionar rápidamente muchas otras secciones del hipercubo.

Hoy, intentaremos aplicar el enfoque de OLAP en MetaTrader e implementar un análisis multidimensional usando herramientas de MQL. Pero antes de empezar, tenemos que decidir qué es lo que vamos a analizar exactamente. Por ejemplo, pueden ser informes comerciales, resultados de la optimización, valores de los indicadores. En principio, nuestra selección en esta etapa no es tan importante ya que el framework diseñado tiene que representar un mecanismo universal orientado a objetos aplicable a cualquier tipo de datos. Sin embargo, necesitaremos ejecutarlo en algo concreto, y una de las tareas más populares es el análisis del informe comercial. Así que nos centraremos en ella.

Para un informe comercial, puede ser interesante distinguir los beneficios por símbolos, días de la semana, compras y ventas. O comparar los valores de varios robots comerciales (es decir, separadamente para cada número mágico). En este punto, surge una pregunta lógica, ¿será posible combinar estas secciones (símbolos por días de la semana en relación a los EAs) o añadir algún otro tipo de agrupamiento? Pues sí, se puede hacer todo eso usando OLAP.

Arquitectura

El enfoque orientado a objetos supone que es necesario realizar una descomposición de la tarea de un conjunto de las partes simples, relacionadas lógicamente, donde cada parte ejecuta su propia función basándose en los datos de entrada, estado interno y algunos conjuntos de reglas.

La primera clase necesaria es un registro con los datos de origen Record. Este registro puede almacenar la información, por ejemplo, sobre una única operación comercial o sobre una única ejecución de la optimización.

El registro es un vector con un número aleatorio de campos. Puesto que es una entidad abstracta, en este caso, incluso no importa que significa cada campo. Para cada determinada aplicación del registro, se creará una clase derivada que «conoce» el propósito de los campos y los procesa de forma correspondiente.

Para leer los registros de una fuente abstracta (que en futuro puede ser el historial de la cuenta comercial, un archivo CSV, un informe HTML o los datos desde Internet obtenido usando WebRequest), hay que crear otra clase, es decir, un adaptador de datos (DataAdapter). A este nivel, es capaz de ejecutar la única función, o sea, repasar secuencialmente los registros y conceder el acceso a ellos. Posteriormente, para cada aplicación real podremos crear las clases derivadas que rellenan los arrays de registros desde las fuentes correspondientes.

Todos los registros tienen que visualizarse de alguna manera en las células del hipercubo. Todavía no sabemos cómo hacer eso, pero es la esencia de todo el proyecto: distribuir los valores de entrada desde los campos de los registros en las células del cubo y calcular una estadística generalizada usando las funciones agregadas seleccionadas.

A nivel básico, un cubo proporciona sólo las propiedades principales como el número de dimensiones, sus nombres, el tamaño de cada dimensión. Todo eso se encuentra en la clase MetaCube.

Las clases derivadas llenarán las células con estadísticas específicas. Como ejemplos más simples de los agregadores determinados, se puede citar la suma de todos los valores o la media del mismo campo en todos los registros, pero en realidad existen muchos más tipos de agregadores.

Para que los valores puedan agregarse en las células, cada registro debe recibir un conjunto de índices que lo muestran en una determinada célula del cubo de una manera unívoca. Delegamos esta tarea a una clase especial, selector (Selector). El selector corresponde a un lado (eje, coordenada) del hipercubo.

La clase base abstracta del selector proporciona una interfaz para definir el conjunto de valores válidos y visualizar cada entrada en uno de estos valores. Por ejemplo, si hace falta dividir los registros por días de la semana, la clase derivada del selector debe devolver el número del día de la semana (de 0 a 6). El número de los valores válidos de un determinado selector define el tamaño del cubo según esta dimensión. En caso del día de la semana, obviamente es 7.

Además de eso, a veces es útil filtrar algunos registros (no considerarlos). Por tanto, necesitaremos una clase del filtro (Filter). Parece muchísimo en el selector, pero impone restricciones adicionales en valores permitidos. Por ejemplo, continuando el ejemplo con el selector por días de la semana, podríamos crear a su base un filtro donde se especifican los días a excluir o, al revés, incluir en los cálculos.

Una vez construido el cubo (o sea, después de calcular las funciones agregadas para todas las células), habrá que visualizar y analizar el resultado. Para eso, reservaremos una clase especial (Display).

Finalmente, para conectar todas las clases mencionadas en un todo único, creamos una especie del centro de control (Analyst).

En la notación UML, todo eso se ve aproximadamente así (una especie del plano de acciones que puede ser consultado en cualquier etapa del desarrollo).

Online Analytical Processing в MetaTrader

Online Analytical Processing в MetaTrader

Algunas clases han sido omitidas, pero, en general, usando este esquema, Usted puede hacerse una idea qué principios se supone usar para construir el hipercubo y qué funciones agregadas estarán disponibles para el cálculo en sus células.

Implementando las clases base

Vamos a empezar a diseñar las clases descritas anteriormente. Primero, la clase 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];
      }
  };

Simplemente almacena los valores aleatorios en el array (vector) data. La longitud del vector se establece en el constructor.

Usamos DataAdapter para leer los registros de diferentes fuentes.

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

El método getNext debe invocarse en el ciclo hasta que no devuelva NULL (significa que ya no hay más entradas). Todos los registros recibidos deben almacenarse en algún lugar (hablaremos de eso más abajo). El método reservedSize permite optimizar la distribución de la memoria (si el número de registros en la fuente se sabe de antemano).

Cada dimensión del hipercubo se calcula a base de uno o dos campos de registros. Conviene designar cada campo como un elemento de una enumeración. Por ejemplo, para el caso del análisis del historial de trading de la cuenta, se puede proponer la siguiente enumeración.

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

Dos últimos campos sirven para calcular los valores no estándar.

Si analizáramos los resultados de la optimización de MetaTrader, podríamos usar esta enumeración.

  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 aplicación, es necesario desarrollar su propia enumeración. Luego, se puede usarla como parámetro de la clase de plantilla Selector.

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

El campo selector almacena un valor— elemento de la enumeración. Por ejemplo, si se usa TRADE_RECORD_FIELDS, se puede crear el selector para las operaciones compra/venta de la siguiente manera:

  new Selector<TRADE_RECORD_FIELDS>(FIELD_TYPE);

El campo _typename es auxiliar. Será sobrescrito en todas las clases derivadas para identificar los selectores, lo que será útil al visualizar los resultados. El campo se usa en el método virtual getTitle.

El trabajo principal se ejecuta por la clase en el método getTitle. Precisamente aquí cada registro de entrada se muestra en un valor del índice especificado por el eje de coordenadas formado por el selector actual. El índice tiene que encontrarse en el rango entre los valores devueltos por los métodos getMin y getMax, mientras que el número total de los índices tiene que ser igual al número devuelto por el método getRange. Si por algún motivo, no se puede mostrar correctamente este registro en el área del selector, el selector select devuelve false. Si se muestra correctamente, se devuelve true.

El método getLabel devuelve una descripción clara del índice al usuario. Por ejemplo, para las operaciones de compra/venta, el índice 0 tiene que generar "buy", y el índice 1 generar "sell".

Implementando las clases de determinados selectores y adaptador de datos para el historial de trading

Puesto que tenemos pensado centrarnos en el análisis del historial de trading, vamos a introducir una clase intermedia de los selectores a base de la enumeración 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 defecto, muestra todas las entradas en la misma célula. Al usar este selector, Usted puede obtener, por ejemplo, el beneficio total.

Ahora, a base de este selector, es fácil determinar los tipos derivados específicos de los selectores, en particular, para dividir los registros por tipo de la operación (compra/venta).

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

Hemos definido la clase usando el elemento FIELD_TYPE en el constructor. El método getRange devuelve 2 porque aquí tenemos sólo 2 posibles valores del tipo: OP_BUY o OP_SELL. Los métodos getMin y getMax devuelven los constantes correspondientes. ¿Y qué debe haber en el método?

Para contestar a esta pregunta, es necesario decidir qué información va a almacenarse en cada registro. Lo hacemos usando la clase TradeRecord, derivada de Record y adaptada para trabajar con el historial de tradicng.

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

El método auxiliar fillByOrder muestra cómo la mayoría de los campos del registro puede rellenarse a base de la orden actual. Está claro que la orden debe ser seleccionada previamente en algún otro lugar del código. Aquí usamos la notación de las funciones comerciales de MetaTrader 4 y aseguramos el soporte de MetaTrader 5 incluyendo la biblioteca MT4Orders (una de las versiones se adjunta al final del artículo, siempre verifique y descargue la versión actual) , así conseguimos el código tipo multiplataforma.

El número de campos TRADE_RECORD_FIELDS_NUMBER puede estar escrito en el código como una definición de macro, o calcularse dinámicamente, basándose en la capacidad de la enumeración TRADE_RECORD_FIELDS. En el código adjunto, se usa el segundo método, para eso se usa una función especial de plantilla EnumToArray.

Como podemos observar en el código del método fillByOrder, campo FIELD_TYPE se rellena con el tipo de la operación desde OrderType. Ahora, podemos volver a la clase TypeSelector e implementar su método select.

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

Aquí, leemos el valor del campo (selector) del registro (r) pasado a la entrada y asignamos su valor (que puede ser OP_BUY o OP_SELL) al parámetro de salida index. Se consideran sólo las órdenes de mercado, por eso, para todos los demás tipos se devuelve false. Vamos a consideraremos otros tipos de selectores un poco más tarde.

Es la hora de diseñar un adaptador de datos para el historial de trading. Es una clase en la que los registros TradeRecord van a generarse a base del historial real de la cuenta.

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

El adaptador repasa sucesivamente todas las órdenes en el historial y crea una instancia TradeRecord para cada orden de mercado. El código presentado es un poco simplificado. En la práctica, tal vez, sea necesario crear objetos que no sean de la clase TradeRecord, sino de alguna clase derivada de ella, especialmente porque hemos reservado dos campos personalizados en la enumeración TRADE_RECORD_FIELDS. Debido a ello, la clase HistoryDataAdapter en realidad es de plantilla, mientras que el parámetro de la plantilla es una clase actual de los objetos-registros generados. Para rellenar los campos personalizados, la clase Record debe contener un método virtual vacío:

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

Puede estudiar la implementación completa del enfoque por sí mismo: en el núcleo se usa la clase CustomTradeRecord (heredada de TradeRecord), que en el método fillCustomFields calcula MFE (Maximum Favorable Excursion, beneficio máximo flotante) y MAE (Maximum Adverse Excursion, pérdida máxima flotante) en por cientos para cada posición y los escribe en los campos FIELD_CUSTOM1 y FIELD_CUSTOM2, respectivamente.

Implementando agregadores y una clase de control

Naturalmente, es necesario crear el adaptador en algún lugar y llamar a su método getNext. Así, hemos llegado al «centro de control», es decir, a la clase Analyst. Además de iniciar el adaptador, tiene que almacenar los registros recibidos en el 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);
      }
  };

La clase no crea el adaptador personalmente, sino lo recibe ya hecho como un parámetro del constructor. Es un principio del diseño bien conocido llamado «inyección de dependencias» (en inglés, dependency injection). Permite separar Analyst de una implementación DataAdapter determinada. En otras palabras, podremos sustituir libremente diferentes variantes del adaptador sin tener que hacer modificaciones en la clase Analyst.

La clase Analyst ahora es capaz de rellenar el array interno de los registros, pero todavía no «sabe» ejecutar la función principal, es decir, agregar los datos. No va a hacerlo por sí mismo, sino delegará la tarea al agregador.

Obsérvese que los agregadores son las clases que pueden calcular los valores predefinido (estadísticas) para los campos seleccionados de los registros. La clase base de los agregadores será MetaCube (repositorio basado en un 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;
  };

El array dimensions describe la estructura del hipercubo. Su tamaño es igual al número de selectores usados, o sea, dimensiones. Cada elemento del array dimensions contiene el tamaño del cubo en esta dimensión, lo que se determina por el rango de valores del selector correspondiente. Por ejemplo, si queremos ver las ganancias por días de la semana, necesitamos crear un selector que devuelve el número del día como un índice de 0 a 6, según la hora de apertura/cierre de la orden (posición). Puesto que es el único selector, el array dimensions va a tener 1 elemento y su valor es 7. Si queremos añadir otro selector, por ejemplo, TypeSelector descrito anteriormente para ver las ganancias por días de la semana y por tipos de las operaciones, el array dimensions va a contener 2 elementos con valores 7 y 2. Eso también significa que en el hipercubo habrá 14 células con la estadística.

La matriz con todos los valores (en el ejemplo considerado son 14) se encuentra en totals. Puesto que el hipercubo es multidimensional, a primera vista, parece extraño que el array está declarado con una dimensión. Eso ha sido hecho porque no sabemos de antemano las dimensiones del hipercubo que el usuario querrá añadir. Además de eso, MQL no soporta los arrays multidimensionales en los cuales todas las dimensiones se distribuyan dinámicamente. Por eso, se usa un array «plano» habitual (vector), y para almacenar las células en él en varias dimensiones, es necesario aplicar una indexación astuta. Luego, mostraremos cómo se calculan los desplazamientos para cada dimensión.

La clase base no distribuye y no inicializa los arrays, todo eso se deja para las clases derivadas.

Como todos los agregadores tendrán las características comunes, los empaquetamos en una clase intermedia única.

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

Cada agregador procesa un determinado campo de registros, que se especifica en la clase, en la variable field que se rellena en el constructor (ver abajo). Por ejemplo, eso puede ser el beneficio (FIELD_PROFIT_AMOUNT).

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

Los cálculos se ejecutan en un espacio multidimensional que se forma por un número aleatorio de los selectores (selectorCount). Antes hemos considerado un ejemplo del cálculo de los beneficios por día de la semana y por los tipos de las operaciones, lo que requiere dos selectores. Se almacenan en el array de los enlaces selectors. Los objetos de los selectores se pasan nuevamente como parámetros del constructor.

    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 puede recordar, el array para almacenar los valores calculados totals es unidimensional. Para convertir los índices de un espacio multidimensional de los selectores en el desplazamiento en el array unidimensional, se utiliza la siguiente función.

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

Ella recibe el array con índices en la entrada y devuelve el número del elemento. Aquí, se usa el array offsets,que ya debe estar llenado para este momento. Su inicialización es un punto clave, y se realiza en el 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
        }
      }

Su objetivo consiste en la obtención de los rangos de todos los selectores y en su multiplicación consecutiva: así, se determina el número de los elementos que hay que «saltar» cuando las coordenadas se aumentan a un uno en cada dimensión del hipercubo.

El cálculo de los indicadores agregados se realiza en el método calculate.

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

Este método se invoca para el array de los registros. Cada registro se pasa cíclicamente uno por uno en cada selector, y si ha sido mostrado con éxito en los índices válidos en todos los selectores (cada selector tiene su índice), el conjunto completo de los índices se guarda en el array local k. Si todos los selectores tiene los índices definidos, se invoca el método update. Él recibe un desplazamiento en el array totals como entrada (el desplazamiento se calcula por la función mixIndex mostrada anteriormente), y el valor del campo especificado field (se establece en el agregador) del registro actual. Si continuamos el ejemplo con el análisis de la distribución de beneficios, la variable field será igual a FIELD_PROFIT_AMOUNT, y los valores de este campo serán retirados desde la llamada a OrderProfit().

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

El método update es abstracto en esta clase y tiene que ser redefinido en los herederos.

Finalmente, el agregador tiene que proporcionar por lo menos un método para acceder a los resultados de los cálculos. El más simple de ellos es obtener los valores de una determinada célula usando el conjunto completo de los índices.

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

La clase base Aggregator realiza casi todo el trabajo no especializado. Ahora, podemos implementar muchos agregadores determinados con un mínimo de esfuerzos.

Pero, primero, volvemos un poco a la clase Analyst y la completamos con una referencia al agregador, que también será pasada a través del parámetro del constructor.

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

En el método acquireData, realizamos el ajuste de las dimensiones del hipercubo usando la llamada adicional al método setSelectorBounds del agregador.

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

La tarea principal, o sea, el cálculo de todos los valores del hipercubo, como ha sido previsto, se le delega al agregador (el método calculate ya ha sido considerado más arriba, y aquí se le pasa el array de los registros).

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

Pero todavía no es la clase Analyst completa. Antes, hemos planeado asignarle la capacidad de mostrar los resultados, formalizándola como una interfaz especial Display. La interfaz se conecta a Analyst de manera semejante (pasando la referencia al constructor):

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

La composición de Display es bastante simple:

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

Contiene el método virtual abstracto que recibe el hipercubo como una fuente de datos. Por motivos de brevedad, aquí, se omiten algunos parámetros que influyen en el orden de la impresión de valores. Los detalles específicos de la visualización y los ajustes adicionales necesarias van a aparecer en las clases derivadas.

Para testear la capacidad funcional de las clases analíticas, necesitamos tener por lo menos una implementación de la interfaz Display. Vamos a crearla usando la visualización de los mensajes en el log "Asesores Expertos", y llamarla LogDisplay: Va a recorrer el hipercubo cíclicamente por todas las coordenadas e imprimir los valores agregados con coordenadas correspondientes aproximadamente de la siguiente manera.

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

He escrito «aproximadamente» porque, en realidad, para los propósitos de un formateo más legible, la implementación de LogDisplay es algo más complicada, pero no es tan fundamental. La versión completa de la clase se encuentra en los códigos fuente.

Naturalmente, en cualquier caso, eso no es tan demostrativo como un gráfico, pero la construcción de las imágenes bidimensionales o tridimensionales es un tema totalmente separado, así que lo dejamos aparte por ahora, especialmente porque se puede aplicar las tecnologías totalmente diferentes: objetos, canva, bibliotecas gráficas externas, inclusive las que están construidas en las tecnologías web.

Pues bien, tenemos la clase base Aggregator. Basándose en ella es fácil obtener varias clases derivadas con cálculos específicos de los indicadores agregados en el método update. En particular, para calcular la suma de valores extraídos de todos los registros por algún selector, basta con escribir:

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

Y para calcular la media, necesitaremos sólo una complicación insignificativa.

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

Una vez consideradas, finalmente, todas las clases aplicadas, vamos a generalizar el algoritmo de su interacción:

  • Creamos el objeto HistoryDataAdapter;
  • Creamos varios selectores especificados (cada selector está vinculado por lo menos a un campo, por ejemplo, al tipo de operación comercial) y los guardamos en un array;
  • Creamos el objeto de un determinado agregador, por ejempo, SumAggregatorpasándole el array de selectores y la designación del campo según el cual hay que realizar la agregación;
  • Creamos el objeto LogDisplay;
  • Creamos el objeto Analyst usando los objetos del adaptador, agregador y visualización;
  • Llamamos consecutivamente a:
      analyst.acquireData();
      analyst.build();
      analyst.display();
  • Al final, no olvidemos eliminar los objetos.

Caso especial: selectores dinámicos

Nuestro programa ya casi está hecho. Pero hay una pequeña omisión que ha sido hecha a propósito para simplificar la representación, y ahora es el momento para eliminarla.

Todos los selectores que hemos conocido hasta ahora tenían un rango constante de valores. Por ejemplo, la semana siempre tiene 7 días, una orden de mercado siempre es de compra o de venta. No obstante, no siempre se sabe el rango de antemano, y eso es bastante común.

El deseo de construir un hipercubo en contexto de los símbolos de trabajo o números mágicos de los EAs es bien justificado. Para resolver este problema, primero, necesitaremos reunir todos los símbolos únicos o los «magec» en un array interno, y luego, usar su tamaño como el rango del selector correspondiente.

Creamos la clase Vocabulary para gestionar este tipo de arrays internos, y mostramos cómo aplicarla, por ejemplo, en conjunto con la clase SymbolSelector.

Nuestra implementación del diccionario es bastante simple (pero Usted puede sustituirla por la que prefiera).

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

Reservamos el array index para almacenar los valores únicos.

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

El método get permite comprobar si el array ya incluye un determinado valor. Si es así, el método devuelve el índice encontrado. De lo contrario, el método devuelve el tamaño actual del array aumentado en 1 con el signo de menos. Eso permite optimizar ligeramente el siguiente método para añadir un valor nuevo al 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;
      }

Además, tenemos que proporcionar los métodos para obtener el tamaño del array y los valores almacenados en él por el índice.

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

Como los símbolos de trabajo en nuestro caso se analizan en contexto de las órdenes (posiciones), incorporamos el diccionario en la clase 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);
      }

El diccionario está descrito como una variable estática, ya que es común para todo el historial de trading.

Ahora, podemos implementar 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);
      }
  };

El selector de los números mágicos está organizado de la misma manera.

La lista general de selectores proporcionados incluye lo siguiente (entre paréntesis se indica la necesidad de la vinculación externa con el campo; si está omitida, eso significa que dentro de la clase del selector ya existe la vinculación con un determinado campo):

  • TradeSelector (cualquier campo) — escalar, un valor (generalización de todos los registros en caso de agregadores «reales» o el valor del campo de un determinado registro en caso de IdentityAggregator (véase a continuación));
  • TypeSelector — compra o venta dependiendo de OrderType();
  • WeekDaySelector (campo tipo datetime) — día de la semana, por ejemplo, en OrderOpenTime() o OrderCloseTime();
  • DayHourSelector (campo tipo datetime) — hora dentro del día;
  • HourMinuteSelector (campo tipo datetime) — minuto dentro de la hora;
  • SymbolSelector — símbolo de trabajo, índice en el diccionario de OrderSymbol() únicos;
  • SerialNumberSelector — número de secuencia del registro (orden);
  • MagicSelector — número mágico, índice en el diccionario de OrderMagicNumber() únicos;
  • ProfitableSelector — true = beneficio, false = pérdida, del campo OrderProfit();
  • QuantizationSelector (campo tipo double) — diccionario de valores aleatorios tipo double (por ejemplo, tamaños del lote);
  • DaysRangeSelector — ejemplo del selector personalizado en dos campos tipo datetime (OrderCloseTime() y OrderOpenTime()), construido a base de la clase DateTimeSelector — antecedente común de todos los selectores para los campos tipo datetime; a diferencia de los demás selectores definidos en el núcleo, este tipo del selector está implementado en el EA de demostración (véase a continuación).

El selector SerialNumberSelector se diferencia considerablemente de los demás. Su rango es igual al número total de los registros. Eso permite formar el hipercubo en el que los propios registros se cuentan consecutivamente de acuerdo con una de las dimensiones (generalmente, la primera, es decir X), y los campos especificados se copian consecutivamente de acuerdo con la segunda dimensión. Los campos se definen por los selectores: en los selectores especializados, ya existe una vinculación al campo, y si hace falta un campo para el cual no hay un selector hecho (como swap), se puede usar TradeSelector universal. En otras palabras, usando SerialNumberSelector, se puede leer los datos iniciales de los registros dentro de la metáfora de un hipercubo de agregación. Para este propósito se usa el pseudo-agregador IdentityAggregator (véase a continuación).

Entre los agregadores, tenemos disponibles los siguientes:

  • SumAggregator — suma de los valores del campo;
  • AverageAggregator — valor medio del campo;
  • MaxAggregator — valor máximo del campo;
  • MinAggregator — valor mínimo del campo;
  • CountAggregator — número de registros;
  • ProfitFactorAggregator — razón entre la suma de los valores positivos del campo y los valores negativos del campo;
  • IdentityAggregator (SerialNumberSelector por el eje X) — tipo especial del selector para un copiado «transparente» de los valores de los campos al hipercubo, sin agregación;
  • ProgressiveTotalAggregator (SerialNumberSelector por el eje X) — total acumulativo para el campo;

Dos últimos agregadores se diferencian de los demás. Cuando tenemos seleccionado IdentityAggregator, el tamaño del hipercubo siempre es igual a 2. En el primer eje X, hay que iterar los regictros usando SerialNumberSelector, en el segundo eje, cada recuento (prácticamente vector/columna) corresponde a un selector a través del cual se determina el campo que hay que leer desde los registros iniciales. De esta manera, si hay tres selectores adicionales (aparte de SerialNumberSelector), hay 3 recuentos por el eje Y. No obstante, la dimensión del cubo sigue siendo 2: eje X y Y. Hay que recordar que en un modo general, el hipercubo se construye sobre otro principio: cada selector corresponde a su dimensión, por eso, si son 3, hay 3 ejes.

El agregador ProgressiveTotalAggregator también interpreta la primera dimensión de una manera especial. Como procede de su nombre, él permite calcular un total acumulativo, además, haciendo eso a lo largo del eje X. Por ejemplo, si en el parámetro de este agregador se indica el campo del beneficio, obtenemos una curva del balance general. Si en el segundo selector (por el eje Y), se especifica la división por símbolos (SymbolSelector), obtenemos varias [N] curvas del balance (cada una para su símbolo). Si el segundo selector e MagicSelector, obtenemos las curvas separadas [M] del balance para diferentes EAs. Pero podemos combinar las dos divisiones seleccionando SymbolSelector por Y, y MagicSelector por Z (o al revés), entonces, obtenemos [N*M] de las curvas del balance separadas por la señal del símbolo de trabajo y por el código del EA.

En principio, el mecanismo de OLAP está listo para el trabajo. Para reducir la representación, hemos omitido algunos detalles. Por ejemplo, en este artículo no se describen los filtros (clases Filter, FilterRange), que han sido presentados en la arquitectura. Además, nuestro hipercubo es capaz proporcionar los valores agrecados no sólo uno por uno (método getValue(const int &indices[])), sino también como un vector (para eso, ha sido implementado este método):

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

El parámetro de salida aquí es la clase especial PairArray. Almacena un array de estructuras con pares [valor;nombre]. Por ejemplo, si hemos construido un cubo con el beneficio por símbolos, cada suma corresponde a un determinado símbolo, y por tanto, su nombre se indica junto con el valor. Como puede observar en el prototipo del método, el núcleo es capaz de ordenar PairArray en modos diferentes: ascendiente o descendiente, por valores o por etiquetas:

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

La ordenación se soporta sólo en los hipercubos unidimensionales. Hipotéticamente, se podría implementarla también para una dimensión aleatoria, pero es un trabajo bastante rutinario. El que quiere, puede hacerlo.

Los códigos fuente completos se adjuntan.

Ejemplo OLAPDEMO

Vamos a testear el hipercubo en acción. Para eso creamos un EA no comercial, capaz de realizar un procesamiento analítico del historial comercial de la cuenta. Lo llamaremos OLAPDEMO. Incluimos el archivo de cabecera que contiene todas las clases principales de OLAP.

  #include <OLAPcube.mqh>

Aunque el hipercubo puede procesar un número aleatorio de dimensiones, para simplificar, limitamos su número a tres. Eso significa que el usuario puede seleccionar de hasta 3 selectores simultáneamente. Denotamos los tipos de los selectores soportados con los elementos de una enumeración especial:

  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 la enumeración para describir las variables de entrada que configuran los selectores:

  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;

En el grupo de cada selector hay una variable de entrada para especificar un campo opcional del registro (algunos selectores requieren los campos, otros no).

Especificamos un filtro (aunque pueden ser muchos), que estará desactivado por defecto.

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

El objetivo del filtro es tomar en cuenta en los cálculos sólo aquellos registros en los cuales el campo Filter1Field especificado tienen un determinado valor Filter1value1 (Filter1value2 tiene que ser el mismo, ya que solamente en estas condiciones en el ejemplo se crea el objeto Filter). Tenga presente que para los campos como símbolo o número mágico, el valor indica en el índice en el diccionario correspondiente. Opcionalmente, el filtro puede incluir un intervalo de valores entre Filter1value1 y Filter1value2 (si no son iguales, solamente en este caso se crea el objeto FilterRange). La presente implementación ha sido diseñada para demostrar la propia posibilidad de la filtración y deja un amplio campo para mejorar la interfaz de usuario, con fines de aplicación práctica.

Para referirse a los agregadores, describimos otra enumeración:

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

La usamos en el grupo de variables de entrada que describen el agregador de trabajo:

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

Todos los selectores, incluyendo los que se usan en el filtro opcional, se inicializan en 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 se ejecuta sólo una vez, de acuerdo con el 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];
    }
  }

La función auxiliar createSelector se define de la siguiente manera.

  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 las clases, salvo DaysRangeSelector, se importan desde el archivo de cabecera, DaysRangeSelector está descrito en el propio EA OLAPDEMO de la siguiente manera.

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

Es un ejemplo de la implementación del selector personalizado. Agrupa las posiciones comerciales según la duración de su presencia en el mercado (en días).

Si ejecutamos el EA en una cuenta en línea y seleccionamos 2 selectores: SymbolSelector y WeekDaySelector, podemos obtener en el log los resultados como éstos:

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

En este caso, se negociaban 5 símbolos diferentes en la cuenta. Tamaño del hipercubo — 35 células. Todas las combinaciones de los símbolos y los días de la semana se listan junto con el importe correspondiente de las ganancias/pérdidas. Nótese que WeekDaySelector requiere una especificación explícita del campo, porque cada posición tiene dos fechas: la apertura (FIELD_DATETIME1) y el cierre (FIELD_DATETIME2). Aquí, hemos elegido FIELD_DATETIME2.

Para analizar no sólo el historial de la cuanta actual, sino también los informes aleatorios del trading en el formato HTML, así como los archivos CSV con el historial de las señales MQL5, la biblioteca OLAP ha sido completada con las clases de mis artículos anteriores Extrayendo datos estructurados de las páginas web usando los selectores CSS y Visualización del historial del trading multidivisas usando los informes en HTML y CSV. Para su integración con OLAP, fueros escritas las clases intermedias. En particular, el archivo de cabecera HTMLcube.mqh contiene la clase de los registros comerciales HTMLTradeRecord y el adaptador HTMLReportAdapter, heredado de DataAdapter. El archivo de cabecera CSVcube.mqh contiene la clase de registros CSVTradeRecord y el adaptador CSVReportAdapter. La lectura de HTML se realiza por WebDataExtractor.mqh, y la lectura de CSV, por CSVReader.mqh. Los parámetros de entrada para descargar los informes y los principios generales del trabajo con ellos (incluyendo la selección de caracteres de trabajo adecuados a través de los prefijos y sufijos, si el informe de terceros contiene símbolos ausentes en su cuenta) se describen en detalle en el segundo artículo mencionado.

Por ejemplo, aquí tenemos los resultados del análisis de una de las señales (cargado como archivo CSV) usando el agregador por el factor del beneficio dividido por símbolos y con ordenación por el índice descendiente:

	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"

El valor inf se genera en el código fuente cuando hay ganancias y no hay pérdidas. Como puede ver, la comparación de valores reales y su ordenación se realiza de tal manera que la «infinidad» es mayor que cualquier otro número finito.

Está claro que visualizar los resultados del análisis de las cuentas comerciales reales en los logs no es muy conveniente. Sería mucho mejor tener la implementación de la interfaz Display, que mostrara un hipercubo de forma gráfica visual. Esta tarea, a pesar de su parecida sencillez, requiere una preparación y un gran volumen de codificación rutinaria. Debido a eso, vamos a considerarla en la segunda parte.


Conclusiones

Este artículo describe una técnica bien conocida para el análisis corriente de los datos de gran volumen (OLAP), aplicado al historial de operaciones comerciales. Con la ayuda de MQL, hemos implementado las clases principales que permiten diseñar un hipercubo virtual en el espacio de recursos seleccionados (selectores) y calcular, en su sección, diferentes indicadores agregados. Este mecanismo también puede aplicarse para precisar y descifrar los resultados de la optimización, para seleccionar las señales comerciales según los criterios también en otras áreas, donde una gran cantidad de datos requiere el uso de los algoritmos de la extracción de los conocimientos para tomar decisiones.

Archivos adjuntos:

  • Experts/OLAP/OLAPDEMO.mq5 — EA de demostración;
  • Include/OLAP/OLAPcube.mqh — archivo de cabecera principal con las clases OLAP;
  • Include/OLAP/PairArray.mqh — clase del array de pares [valor;nombre] con soporte de todas las opciones de ordenación;
  • Include/OLAP/HTMLcube.mqh — combinación de OLAP con descarga de datos desde los informes HTML;
  • Include/OLAP/CSVcube.mqh — combinación de OLAP con descarga de datos desde los informes CSV;
  • Include/MT4orders.mqh — biblioteca MT4orders para trabajar con las órdenes usando el mismo estilo en МТ4 y en МТ5;
  • Include/Marketeer/WebDataExtractor.mqh — analizador sintáctico HTML;
  • Include/Marketeer/empty_strings.h — lista de etiquetas vacías HTML;
  • Include/Marketeer/HTMLcolumns.mqh — definiciones de los índices de columnas en los informes HTML;
  • Include/Marketeer/CSVReader.mqh — analizador sintáctico CSV;
  • Include/Marketeer/CSVcolumns.mqh — definición de los índices de columnas en los informes CSV;
  • Include/Marketeer/IndexMap.mqh — archivo de cabecera auxiliar con implementación del array con acceso combinado por la clave e índice;
  • Include/Marketeer/RubbArray.mqh — archivo de cabecera auxiliar con el array de «goma»;
  • Include/Marketeer/TimeMT4.mqh — archivo de cabecera auxiliar con implementación de las funciones de trabajo con fechas en estilo MT4;
  • Include/Marketeer/Converter.mqh — archivo de cabecera auxiliar con unión para convertir los tipos de datos;
  • Include/Marketeer/GroupSettings.mqh — archivo de cabecera auxiliar para configurar el grupo de parámetros de entrada;

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/6602

Archivos adjuntos |
MQLOLAP1.zip (50.46 KB)
Cómo visualizar la historia del comercio multidivisa en informes con formato HTML y CSV Cómo visualizar la historia del comercio multidivisa en informes con formato HTML y CSV
Como sabemos, MetaTrader 5 ofrece la posibilidad de realizar simulaciones multidivisa desde su aparición. Esta función tiene mucha demanda entre la mayoría de los tráders, pero, por desgracia, no es tan universal como querríamos. En el presente artículo, ofrecemos varios programas para trazar gráficos con la ayuda de objetos gráficos usando como base la historia comercial de informes en los formatos HTML y CSV. El comercio con varios instrumentos puede analizarse paralelamente en varias subventanas, o en una sola ventana con la ayuda de la alternancia dinámica a una orden del usuario.
Web scraping de datos sobre la rentabilidad de los bonos Web scraping de datos sobre la rentabilidad de los bonos
Cuando diseñamos los sistemas del trading automático, casi siempre utilizamos los datos de los indicadores técnicos que analizan el pasado con el fin de predecir el futuro comportamiento del precio. Pero si no tomamos en cuenta las fuerzas fundamentales que mueven el mercado, evidentemente estaremos en una situación menos ventajosa en comparación con los traders que consideran adicionalmente los datos fundamentales en sus decisiones comerciales. Recopilando automáticamente los datos sobre los tipos de interés, Usted podrá mejorar el funcionamiento de su Asesor Experto.
Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte V): Clases y colección de eventos comerciales, envío de eventos al programa Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte V): Clases y colección de eventos comerciales, envío de eventos al programa
En anteriores artículos comenzamos a crear una gran biblioteca multiplataforma cuyo objetivo es simplificar la escritura de programas para las plataformas MetaTrader 5 y MetaTrader 4. En la cuarta parte, hemos puesto a prueba el seguimiento de eventos comerciales en la cuenta. En esta parte, vamos a crear las clases de los eventos comerciales y a colocarlas en la colección de eventos desde la que serán enviadas al objeto básico de la biblioteca Engine y al gráfico del programa de control.
Utilidad para la selección y navegación en MQL5 y MQL4: aumentando la informatividad de los gráficos Utilidad para la selección y navegación en MQL5 y MQL4: aumentando la informatividad de los gráficos
En este artículo, continuaremos expandiendo la funcionalidad de nuestra utilidad. Esta vez, añadiremos las posibilidades de visualizar la información en el gráfico, que sirve para facilitar nuestra negociación. En particular, añadiremos en el gráfico los precios máximos y mínimos del día anterior, niveles redondos, precios máximos y mínimos durante el año, hora del inicio de la sesión, etc.