Implementando OLAP en la negociación (Parte 3): analizando las cotizaciones con el fin de desarrollar las estrategias comerciales

Stanislav Korotky | 10 febrero, 2020

En este artículo, seguiremos estudiando la aplicación de la tecnología OLAP, que proviene de On-Line Analytical Processing (Procesamiento Analítico en Línea), en el trading. En dos primeros artículos, fueron descritos los métodos generales de la construcción de las clases para la acumulación y el análisis de datos multidimencionales, así como, la visualización de los resultados del análisis en la interfaz gráfica. Ambos artículos solucionaban los problemas del procesamiento de los informes comerciales obtenidos de maneras diferentes: desde el Simulador de estrategias, desde el historial del tradng en línea, desde los archivos en el formato HTML y CSV (incluyendo las señales comerciales MQL5). No obstante, el OLAP también puede aplicarse en otras áreas. En particular, conviene usarlo para analizar las cotizaciones y elaborar las estrategias del trading.

Introducción

Recordemos qué es lo que fue implementado en los artículos anteriores (al que no los leyó por alguna razón, se le recomienda insistentemente que los estudie). El núcleo se encontraba en el archivo OLAPcube.mqh que contenía lo siguiente:

Algunas cosas específicas referidas a los informes HTML han sido colocadas en el archivo HTMLcube.mqh, en el que, en particular, se definen las clases de las transacciones comerciales del informe HTML HTMLTradeRecord y el adaptador HTMLReportAdapter que las genera.

De la misma manera, el archivo CSVcube.mqh incluye las clases para las transacciones comerciales de los informes CSV CSVTradeRecord y el adaptador CSVReportAdapter.

Finalmente, para simplificar la integración de OLAP con los programas MQL5 ha sido escrito el archivo OLAPcore.mqh con una clase «envoltorio» de toda la funcionalidad OLAP usada en los proyectos de demostración (OLAPWrapper).

Puesto que la tarea pendiente del procesamiento OLAP atañe a un área nuevo, necesitaremos realizar la refactorización del código existente y seleccionar dentro de él las partes que son comunes no sólo para el historial de de trading, sino también para las cotizaciones (lo ideal sería para cualquier tipo de fuentes de datos).

Refactorización

A base del archivo OLAPcube.mqh, se forma el archivo nuevo OLAPCommon.mqh que conserva sólo los tipos básicos. Entre los tipos extraídos, en primer lugar, se encuentran las enumeraciones que describen el sentido práctico de los campos de datos, por ejemplo, SELECTORS y TRADE_RECORD_FIELDS. También han sido excluidas las clases de los selectores relacionadas con el trading y las clases de los registros. Está claro que todo eso no ha sido eliminado de forma irrrescatable, sino ha sido transferido al nuevo archivo OLAPTrades.mqh creado para el trabajo con el historial comercial y los informes.

Además, la ex clase «envoltorio» OLAPWrapper ha mudado al archivo OLAPCommon.mqh. Ahora esta clase es de plantilla, y por tanto tiene el nombre nuevo OLAPEngine. Como parámetro de parametrizado, se debe usar las enumeraciones de los campos de datos (por ejemplo, para adaptar los proyectos de los artículos 1 y 2, será TRADE_RECORD_FIELDS, véase más detalles a continuación).

El archivo OLAPTrades.mqh contiene los siguientes tipos (están descritos en los artículos 1 y 2):

Obsérvese que el selector DaysRangeSelector también está aquí, es decir, se convirtió en un selector reglamentario del análisis del historial de trading, mientras que antes se encontraba en el archivo OLAPcore.mqh como ejemplo del selector de usuario.

Por defecto, al final del archivo se crea una instancia del adaptador:

  HistoryDataAdapter<RECORD_CLASS> _defaultHistoryAdapter;

así como, una instancia del motor OLAP:

  OLAPEngineTrade _defaultEngine;

Es conveniente usar estos objetos desde el código fuente de cliente. Nosotros vamos a aplicar esta técnica de la representación de los objetos listos en otras áreas aplicadas también (archivos de cabecera), en particular, en el analizador de cotizaciones planeado.

Los archivos HTMLcube.mqh y CSVcube.mqh han quedado casi inalterados. Toda la funcionalidad anterior del análisis del historial y de los informes permanece sin alterar. Para demostrar eso, al artículo se le adjunta el nuevo Asesor Experto OLAPRPRT.mq5 (es el análogo de OLAPDEMO.mq5 del primer artículo).

Usando el archivo OLAPTrades.mqh como ejemplo, se puede crear con facilidad las implementaciones especializadas de las clases OLAP para otros tipos de datos.

Como nos disponemos a complicar el proyecto añadiendo una nueva funcionalidad, todos los aspectos de la integración de OLAP con la interfaz gráfica han sido dejados al margen de propósito. En este artículo, nos centraremos en el análisis de los datos sin pararnos en la visualización (tanto más que los modos de visualización pueden ser diferentes). Después de leer este artículo, el que desea puede combinar personalmente el motor actualizado con los recursos del artículo 2 en el campo de GUI.

Mejoras

En el contexto del análisis de las cotizaciones, algunos métodos nuevos de la división lógica y acumulación de los datos. No obstante, nosotros añadiremos las clases destinadas para eso al archivo OLAPCommon.mqh, ya que tienen un carácter básico, y así estarán disponibles para cualquier «cubo» aplicable, incluyendo los anteriores de OLAPTrades.mqh.

Bien, hemos añadido:

MonthSelector permitirá agrupar los datos por meses. El hecho de que no había este selector antes, se puede considerar como un descuido.

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

WorkWeekDaySelector es el análogo de WeekDaySelector pero divide los datos sólo por días de trabajo (de 1 a 5). Eso es muy conveniente para el análisis de los mercados cuyo horario comercial excluye los fines de la semana, ya que los indicadores de los fines de la semana siempre serán nulos y no tiene sentido reservar las células del hipercubo para ellos.

El agregador VarianceAggregator permite calcular la dispersión de datos, y así completa el agregador del AverageAggregator medio. La esencia del agregador puede ser comparada con el valor del indicador Average True Range (ATR), pero a diferencia del último, el agregador puede ser calculado para cualquier sección (por ejemplo, separadamente por horas del día o por días de la semana), así como, para otras fuentes de datos (por ejemplo, la dispersión de ganancias en el historial del trading).

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

Fig. 1 Diagrama de las clases de los agregadores

Fig. 1 Diagrama de las clases de los agregadores

Los selectores QuantizationSelector y SerialNumberSelector son derivados de BaseSelector, en vez de TradeSelector que es más específico. Además, QuantizationSelector ha adquirido un nuevo parámetro del constructor que permite definir la granularidad del selector. Por defecto, es igual a cero, lo que significa que los datos se agrupan según la coincidencia exacta del valor del campo correspondiente (el campo se especifica en el selector). Por ejemplo, en el artículo anterior, usamos la cuantización por el tamaño del lote para obtener el informe de beneficios en división por los tamaños del lote, y los lotes como 0.01, 0,1, y los demás en el historial del trading podían servir de las células del cubo. A veces, es más conveniente hacer la cuantización con un paso definido (tamaño de la célula). Pues, el nuevo parámetro del constructor permite definirlo. En el código fuente, los fragmentos agregados están marcados con el comentario "+".

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

Además, en las clases existentes han sido introducidas otras mejoras. En particular, las clases de los filtros Filter y FilterRange ahora soportan la comparación por el valor del campo, y no sólo por el índice de la célula en la que se coloca este valor. Es cómodo desde el punto de vista del usuario, porque el índice de la célula no siempre es sabido de antemano. El nuevo modo de trabajo se activa si el selector devuelve el índice igual a -1 (las líneas añadidas están marcadas con '+'):

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

Está claro que vamos a necesitar un selector que sepa devolver -1 como índice. Tiene el nombre FilterSelector.

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

Como se puede observar, para cualquier registro este selector devuelve true como indicio de que este registro tiene que ser procesado, y -1 como índice. Gracias a eso, el filtro podrá «entender» que el usuario pide «tamizar» el registro por el valor del campo, en vez de usar el índice. Veremos el ejemplo de su uso más abajo.

Para remate, la pantalla del log ahora soporta la ordenación del cubo multidimensional por valores (antes era imposible ordenar los cubos multidimensionales). La ordenación del cubo multidimensional por etiquetas está disponible parcialmente. Se puede hacerla sólo para los selectores que «saben» formar las etiquetas de forma unificada con líneas en orden lexicográfico. En particular, el nuevo selector por días laborales representa sus etiquetas como "1`Monday", "2`Tuesday", "3`Wednesday", "4`Thursday", "5`Friday", y la presencia del número del día en el primer carácter asegura la ordenación correcta. De lo contrario, habrá que introducir las funciones de comparación de las etiquetas para una implementación correcta. Además de eso, para los agregadores «consecutivos» especiales —IdentityAggregator, ProgressiveTotalAggregator— seguramente será necesario definir las prioridades de los lados del cubo, ya que estos agregadores por el eje X tienen el número de orden del registro que puede ser deseable usar durante la ordenación en el último lugar en vez del primer lugar.

No son todas las modificaciones de los códigos fuente antiguos; puede comprender su esencia completa comparando el contexto de los códigos fuente.

Expansión de OLAP para el área aplicable de cotizaciones

A base de las clases básicas de OLAPCommon.mqh y por la analogía con OLAPTrades.mqh, crearemos el archivo con las clases destinadas para analizar las cotizaciones— OLAPQuotes.mqh. En primer lugar, describiremos los tipos como:

La enumeración QUOTE_SELECTORS se determina de la siguiente manera:

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

El selector del formulario (shape) diferencia las barras por tipos: alcista, bajista y neutral, dependiendo de la dirección del movimiento del precio.

El selector del índice corresponde a la clase SerialNumberSelector que está determinado en las clases base (archivo OLAPCommon.mqh). Si en el caso de las operaciones comerciales, eran los números de orden de las transacciones, para las cotizaciones serán los números de las barras.

El selector del mes ha sido escrito más arriba. Los demás selectores han sido heredados de los artículos anteriores.

La siguiente enumeración describe los campos de datos en las cotizaciones:

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

Los nombres y los comentarios explican el destino de cada uno.

Dos enumeraciones arriba mencionadas han sido colocadas en las macros:

  #define SELECTORS QUOTE_SELECTORS
  #define ENUM_FIELDS QUOTE_RECORD_FIELDS

Obsérvese que estas macrodefiniciones (SELECTORS y ENUM_FIELDS) están presentes en todos los archivos «aplicables». En nuestro caso, por ahora son dos (OLAPTrades.mqh, OLAPQuotes.mqh —para el historial de operaciones comerciales y cotizaciones), pero pueden ser más. De esta manera, en cualquier proyecto que utiliza OLAP, ahora se puede analizar simultáneamente sólo un área aplicable (incluyendo sólo OLAPTrades.mqh o OLAPQuotes.mqh, pero no ambos a la vez). Para llevar a cabo el análisis cruzado de diferentes cubos, necesitaremos realizar otra pequeña refactorización. Dejaremos eso para el trabajo individual, ya que las tareas del análisis paralelo de varios metacubos son más específicos y menos frecuentes.

El selector padre para las cotizaciones representa una especialización de BaseSelector con los campos QUOTE_RECORD_FIELDS:

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

El selector del tipo barra (alcista o bajista) ShapeSelector está implementado de la siguiente manera:

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

Tenemos reservados tres valores que reflejan los tipos: -1 es el movimiento del precio hacia abajo, 0 es el movimiento lateral (flat), +1 es el movimiento hacia arriba. Por tanto, los índices de las células se encuentran dentro del rango de 0 a 2 (inclusive). El relleno del campo con valores actuales del tipo de cada determinada barra se muestra más abajo, en la clase QuotesRecord.

Fig. 2 Diagrama de las clases de los selectores

Fig. 2 Diagrama de las clases de los selectores

La clase del registro que guarda la información sobre determinada barra tiene que ser intuitivamente comprensible.

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

Toda la información viene de la estructura MqlRates. La creación de las instancias de la clase será mostrada a continuación en la implementación del adaptador.

En la misma clase, se determina el destino aplicable de los campos (entero, real, fecha), lo cual es necesario debido al hecho de que todos los campos de los registros se almacenan técnicamente en el array tipo double.

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

La presencia de esta bandera para indicar la especialización de los campos permite adaptar la entrada/salida de los datos en la interfaz de usuario, lo que será mostrado más abajo.

Para soportar la posibilidad del relleno de los campos personalizados, existe una clase intermedia cuyo objetivo principal es llamar a fillCustomFields de la clase personalizada, indicada como clase base mediante la plantilla (así, en el momento de la llamada al constructor CustomQuotesBaseRecord, nuestro objeto de usuario ya está creado y llenado con campos estándar, los cuales a menudo son necesarios para el cálculo de los campos personalizados):

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

Se utiliza en el adaptador de cotizaciones:

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

Nótese que las barras se repasan en la dirección de las más antiguas a las más recientes, es decir, en orden cronológico. En particular, eso significa que la indexación (campo FIELD_INDEX) se realiza como en un array común, en vez de la serie temporal.

Finalmente, el motor de OLAP para las cotizaciones tiene el siguiente aspecto:

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

Todas las funciones principales se quedaron en la clase OLAPEngine que fue considerada en el primer artículo, aunque con otro nombre OLAPWrapper. Aquí, nos queda sólo crear los selectores específicos para las cotizaciones.

Las instancias del adaptador predefinido y del motor OLAP predefinido se presentan como unos objetos hechos:

  QuotesDataAdapter<RECORD_CLASS> _defaultQuotesAdapter;
  OLAPEngineQuotes _defaultEngine;

Basándose en las clases creadas para dos áreas aplicadas del análisis (OLAPTrades.mqh, OLAPQuotes.mqh), es fácil expandir la funcionalidad de OLAP para otras aplicaciones, como el procesamiento de los resultados de las optimizaciones o de los datos obtenidos de las fuentes externas.

Fig. 3 Diagrama de las clases controladoras de OLAP

Fig. 3 Diagrama de las clases controladoras de OLAP

Asesor Experto para el análisis OLAP de las cotizaciones

Tenemos todo listo para usar las clases creadas. Escribiremos el Asesor Experto no negociable OLAPQTS.mq5. Su estructura será análoga a OLAPRPRT.mq5 con el que se analizan los informes comerciales.

Para demostrar el cálculo/relleno de los campos personalizados, tenemos prevista la clase CustomQuotesRecord heredada de QuotesRecord. Vamos a usar algunos campos personalizados para revelar las regularidades en las cotizaciones que pueden ser usadas para construir las estrategias comerciales. Todos estos campos se rellenan en el método fillCustomFields, pero los describiremos con más detalles un poco más tarde.

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

Para que el adaptador «sepa» de nuestra clase de registros CustomQuotesRecord y crea sus instancias, hace falta definir la siguiente macro antes de la inclusión de OLAPQuotes.mqh:

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

Para controlar el EA, se usan los parámetros de entrada similares a los del proyecto del análisis del historial comercial. Igual como en aquel proyecto, se admite acumular los datos en tres dimensiones del metacubo, para lo cual tenemos disponible la selección de los selectores por los ejes X, Y, Z. Además, existe la posibilidad de especificar el filtro por un valor o por un rango de valores. Finalmente, el usuario tiene que elegir el tipo del agregador (recordemos que algunos agregadores requieren la especificación del campo de agregación, otros suponen un determinado campo) y el tipo de ordenación (opcionalmente).

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

Todos los selectores y sus campos se reúnen en los arrays para que la siguiente transacción en el motor sea más cómoda:

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

Como podemos observar, el EA utiliza las instancias proporcionadas por defecto del motor y del adaptador de cotizaciones. Debido a su particularidad, el EA tiene que procesar los datos únicamente para la introducción de los parámetros. Para este propósito, así como para el inicio los fines de semana, cuando no hay ticks, en el procesador OnInit se inicia el temporizador.

El arranque del procesamiento en OnTimer es el siguiente:

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

Durante el análisis de las cotizaciones, vamos a necesitar un filtro por fechas. Debido a eso, los valores para los filtros se establecen en los parámetros de entrada en forma de cadenas, y dependiendo del tipo del campo al que se aplica el filtro, las cadenas se interpretan como un número o como una fecha (en el formato YYYY.MM.DD). En el ejemplo desde el primer artículo, siempre se introducían los valores numéricos, lo cual era incómodo para el usuario en caso de las fechas.

Todos los parámetros de entrada preparados se pasan al método process del motor OLAP, y luego el trabajo se realiza sin la intervención del usuario, después de lo cual los resultados se muestran en el log de los EAs a través de la instancia de LogDisplay.

Simulación del análisis OLAP de las cotizaciones

Para familiarizarse con la herramienta diseñada, vamos a realizar algunos estudios simples de las cotizaciones.

Abrimos el gráfico EURUSD D1 y le aplicamos el Asesor Experto OLAPQTS. Dejamos todos los parámetros con valores predefinidos lo que significa la selección del selector type por el eje X y el agregador COUNT. Cambiamos sólo los ajustes del filtro: establecemos "filter(field)" en el parámetro Filter1, datetime en Filter1Field, "2019.01.01" y "2020.01.01" en Filter1Value1 y Filter1Value2, respectivamente. Así limitamos el rango del cálculo con el año 2019.

Como resultado del inicio del EA, obtenemos aproximadamente el siguiente resultado:

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

En el log vemos que han sido analizadas 12626 barras (todo el historial disponible de EURUSD D1), pero sólo 259 han satisfecho las condiciones del filtro, 134 son bajistas y 125 son alcistas.

Si cambiamos el timeframe por H1, obtenemos la estimación de las barras de una hora:

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

A continuación, trataremos de analizar los spreads. Una de las particularidades de MetaTrader 5 consiste en el hecho de que, por alguna razón, ha sido decidido guardar y mostrar el spread mínimo en la estructura MqlRates y en la ventana de datos. Desde el punto de vista de la verificación de las ideas comerciales, este enfoque puede suponer un peligro, ya que ofrece unas estimaciones optimistas del beneficio. Lo más conveniente sería que en el historial estén disponibles no sólo los spreads mínimos, sino también los máximos. Está claro que si hace falta, se puede usar el historial de los ticks, pero a pesar de todo, el modo barra por barra es más económico. Intentaremos estimar los spreads reales en división por horas del día.

En el mismo gráfico EURUSD H1 mantenemos el filtro anterior por 2019 e introducimos los siguientes ajustes del EA. Selector X — "hour-of-day", agregador — "AVERAGE", campo del agregador — "spread". Aquí tenemos un ejemplo del resultado:

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

Para cada hora del día se especifica un valor medio del spread, pero se trata de una promediación por el spread mínimo, y por tanto es delusoria. Para obtener una imagen más adecuada, cambiamos al timeframe M1. Así, el análisis va a realizarse con una especificación del historial detallada al máximo (si no se consideran los ticks).

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

Aquí la imagen es más realista: durante algunas horas, el spread mínimo se multiplicaba doble o triplemente. Pero con el fin de hacer el análisis aún más detallado, podemos solicitar el valor máximo, en vez del valor medio, usando el agregador "MAX". A pesar de que los valores obtenidos seguirán siendo máximos de los _mínimos_, no hay que olvidar de que ellos se construyen a base de las barras minuteras dentro de cada hora, y por tanto, describen muy bien las condiciones de las entradas y las salidas durante el trading a corto plazo.

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

Como se dice, aprecie la diferencia: hemos empezado con un spread alrededor de 4 puntos, y hemos terminado con decenas e incluso centena al medianoche.

Estimemos la dispersión del spread, y de paso comprobemos cómo funciona el nuevo agregador seleccionando "DEVIATION".

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

Estos valores representan una desviación estándar singular, partiendo de la cual, podemos ajustar los filtros en las estrategias de scalping o EAs que se basan en los impulsos de la volatilidad.

Vamos a comprobar el relleno del campo con el rango del movimiento de precios en la barra, trabajo de la cuantización con el tamaño especificado de la célula, así como, la ordenación.

Para eso, volvemos a cambiar a EURUSD D1, mantenemos el filtro anterior para 2019. En los parámetros especificamos lo siguiente:

Obtenemos el siguiente resultado:

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

Como era de esperar, el mayor número de las barras (72) entró en el rango cero, es decir, el cambio del precio en ellas no superó 100 puntos. Los cambios ±100 y ±200 puntos son siguientes en ser «más populares», etc.

Pero por ahora todo eso es sólo la demostración de la funcionalidad de OLAP al analizar las cotizaciones. Es el momento para pasar a la siguiente etapa, cuando OLAP permitirá crear estrategias comerciales.

Construcción de las estrategias comerciales a base del análisis OLAP de cotizaciones. Parte 1

Vamos a intentar averiguar si existen algunas regularidades en las cotizaciones, que están relacionadas con los ciclos diarios y semanales. Si durante algunas horas o días de la semana los movimientos del precio prevalecientes en promedio no son simétricos, podemos aprovechar de eso para abrir las transacciones. Para detectar esta variación cíclica, necesitaremos usar los selectores "hour-of-day" y "day-of-week". Se puede usar los selectores consecutivamente uno por uno o aplicar ambos a la vez (cada uno por su eje). La segunda opción es más conveniente porque permite construir las secciones de datos más precisas que toman en cuenta los dos factores a la vez (del ciclo). Para el programa no importa mucho qué selector va a colocar en el eje X y en el eje Y, pero de eso se cambian los resultados a mostrar para el usuario.

Puesto que el rango de los índices de los selectores es de 24 (horas del día) y de 5 (días laborales), respectivamente, el tamaño del cubo será igual a 120. En principio, tenemos la posibilidad de activar también la variación cíclica estacional dentro del año, seleccionando el selector "month-of-year" por el eje Z. Pero para simplificar, nos limitaremos con un cubo bidimensional.

El cambio del precio dentro de la barra se muestra en dos campos: FIELD_PRICE_RANGE_OC y FIELD_PRICE_RANGE_HL. El primer campo nos da la diferencia en puntos entre los precios Open y Close, el segundo, la diferencia entre High y Low. Usaremos el primer campo como la fuente de la estadística de las transacciones potenciales. Nos queda aclarar qué estadística va a calcularse, es decir, qué agregador va a aplicarse.

Puede parecer extraño, pero aquí el agregador ProfitFactorAggregator nos puede ser útil. Recordemos que él suma por separado los valores positivos y negativos del campo especificado de todos los registros, y luego devuelve su cociente: divide lo positivo por lo negativo según el módulo. De esta manera, si en alguna célula del hipercubo van a prevalecer los incrementos positivos del precio, obtendremos el factor del beneficio más alto de 1, y si van a prevalecer los incrementos negativos el factor del beneficio será más bajo de 1. En otras palabras, todos los valores que se diferencian mucho de 1 señalan en unas buenas condiciones para abrir una transacción larga o corta. Cuando el factor del beneficio es superior a 1, las compras son rentables, y cuando es menos de 1, las ventas son rentables. El factor del beneficio real de las ventas es una magnitud inversa para el valor calculado.

Hagamos un análisis en EURUSD H1. Seleccionamos los parámetros de entrada:

No nos interesa la lista completa de 120 líneas con los resultados. Mostraremos sólo los valores iniciales y finales que indican en las opciones más rentables de compras y ventas (se muestran al principio y al final gracias a la ordenación activada).

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

Nótese que las etiquetas de dos dimensiones X y Y (hora y día de la semana) se muestran para cada valor.

Los valores obtenidos no son de todo correctos, porque no se considera el spread. Para solucionar este problema, podemos usar los campos personalizados. Por ejemplo, para estimar la influencia potencial de los spreads, guardamos en el primer campo personalizado la amplitud de la barra descontando el spread. Para el segundo campo vamos a calcular la dirección de la barra descontando el spread.

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

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

Seleccionamos el campo personalizado número 1 y obtenemos los siguientes resultados:

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

Significan que las operaciones comerciales tienen que traer beneficios, en particular, el jueves: las compras a 0, 1 y 4 horas de la noche, y las ventas a las 19 y 23. El viernes se recomienda comprar a las 0, 3, 4, 9 de la mañana y vender a las 11, 14 y 23. La verdad es que las ventas a las 23 del viernes pueden ser arriesgadas debido al pronto cierre de la sesión y un gap potencial en la dirección desfavorable (pero, por cierto, el análisis del gap también puede ser automatizado fácilmente a través de los campos personalizados). Aquí, un nivel aceptable del factor del beneficio se considera 2 y más (para las ventas es de 0,5, respectivamente). En la práctica, los valores normalmente son peores que en la teoría, por eso, hay que dejar un cierto margen de seguridad.

Tiene sentido calcular el factor del beneficio no sólo a base de la amplitud, sino también por el número de las velas alcistas y bajistas. Para eso, hay que seleccionar el tipo (forma) de la barra como campo del agregador. A veces, algún importe rentable se forma por una o dos barras de un tamaño extraordinario. Eso será más notable si comparamos el factor del beneficio por los tamaños de las velas con el factor del beneficio por sus cantidades en ambas direcciones.

En general, no estamos obligados a realizar el análisis en el mismo timeframe que ha sido elegido en el selector menor por el campo «fecha». En particular, ahora hemos usado "hour-of-day" en H1. Se permite usar cualquier timeframe menor o igual al selector menor por el campo de la fecha. Por ejemplo, podemos hacer el mismo análisis en M15, guardando la agrupación por horas a través del selector "hour-of-day". Entonces, encontraremos el factor del beneficio para las barras de 15 minutos. La verdad es que para nuestra estrategia actual en este caso habrá que aclarar adicionalmente el momento de la entrada en el mercado dentro de la hora. Para eso se podría analizar qué modos más probables de la formación de la vela en cada hora (en otras palabras, después de qué movimientos contrarios se forma el cuerpo principal de la barra). En el código fuente de OLAPQTS, en los comentarios se muestra un ejemplo de la «digitalización» de las colas de las barras.

Otra manera más ilustrativa de detectar las barras estables de compra y de venta en la división por horas y días de la semana es usar ProgressiveTotalAggregator. Habría que establecer para él el selector "ordinal number" (repaso consecutivo por todas las barras) para el eje X, selectores "hour-of-day" y "day-of-week" para los ejes Y y Z, y el campo anterior para la agregación "custom 1". Como resultado, obtendríamos prácticamente las curvas de los balances del trading para cada barra horaria específica. No obstante, no es muy cómodo mostrar y analizar estos datos en el log, por tanto, este modo va a ser conveniente cuando tenemos conectada una «pantalla» gráfica. Pero eso dificultaría la implementación, así que nos quedamos con el log.

Creamos el Asesor Experto SingleBar que va a realizar las transacciones de acuerdo con los ciclos encontrados a través del análisis OLAP realizado. Los parámetros de entrada principales garantizan la negociación según el horario:

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

Los parámetros de cadena BuyHours y SellHours reciben las listas de las horas a las cuales se debe abrir las compras y las ventas, respectivamente. Las horas de cada lista se separan por comas: En el parámetro ActiveDayOfWeek se define el día de la semana (valores de 1 — lunes, a 5 — viernes). En la fase de la simulación nos limitaremos con la verificación de las hipótesis para cada día por separado, aunque en perspectiva el EA tiene que mantener el horario que combina todos los días de la semana. Si ActiveDayOfWeek se define en 0, el EA va a negociar durante todos los días según el mismo horario, pero para eso, ha que realizar previamente el análisis OLAP en la sección de la única dimensión "hour-of-day", quitando "day-of-week" por el eje Y. El que desea, puede comprobar esta estrategia personalmente.

Los ajustes se leen y se verifican en OnInit:

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

En el manejador OnTick vamos a verificar las listas de las horas comerciales y definir la variable especial mode en +1, o -1, si la hora actual ha sido encontrada en una de ellas. Si la hora no ha sido encontrada, mode será igual a 0, lo que significa el cierre de las posiciones existentes (si hay) y no apertura de las nuevas. Si todavía no hay órdenes, y mode no es 0, se abre una posición nueva. Si la posición ya existe y coincide con el horario por la dirección, ella se mantiene. Si la dirección de la posición y de la señal no coincide, se hace un giro. Sólo una posición puede estar abierta al mismo tiempo.

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

La negociación se realiza sólo por la apertura de las barras, eso fue fundado en la estrategia comercial. Las funciones auxiliares ArrayFind, CurrentOrderDirection, OrdersCloseAll se muestran más abajo. Todas ellas, igual como el propio EA, utilizan la biblioteca MT4Orders para simplificar el trabajo con API. Además, se usa MT4Bridge/MT4Time.mqh adjunto para trabajar con las fechas.

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

El código fuente completo se adjunta al artículo. Entre otras cosas que han sido omitidas en el texto del artículo por motivos de brevedad, hay por ejemplo el cálculo teórico del factor del beneficio según la misma lógica que en el motor OLAP. Eso permite comparar su valor con el valor práctico del factor del beneficio proporcionado por el Probador de Estrategias. Normalmente, estos dos valores son parecidos pero no coinciden exactamente. Está claro que el factor del beneficio teórico tiene sentido sólo en el caso cuando en los ajustes del EA tenemos seleccionado el horario del trading sólo en una dirección: sólo compra (BuyHours), o sólo venta (SellHours). De lo contrario, dos modos se solapan y el factor del beneficio teórico tiende al valor cerca de 1. Además de eso, recordemos que el factor del beneficio rentable para las ventas en el cálculo teórico está marcado con los valores menos de 1, ya que representa una magnitud inversa respecto al factor del beneficio normal. Por ejemplo, el factor del beneficio teórico para las ventas igual a 0,5 significa el factor del beneficio práctico del Probador de Estrategias igual 2. Para el modo de compras, los valores del factor del beneficio teórico y práctico tienen la misma definición: los valores por encima de 1 significan el beneficio, por debajo de 1, pérdidas.

Simulamos el trabajo del EA SingleBar en 2019 en EURUSD H1. Definimos los valores encontrados de las horas comerciales hasta el viernes:

El orden de las horas en principio no importa, pero aquí se muestran en orden descendiente de la rentabilidad esperada. Como resultado, obtenemos el siguiente informe:

Fig.4 Informe del EA SingleBar sobre el trading según el horario encontrado de los viernes de 2019, EURUSD H1

Fig.4 Informe del EA SingleBar sobre el trading según el horario encontrado de los viernes de 2019, EURUSD H1

Los valores son buenos, pero no es sorprendente ya que es el año en el que se realizaba el análisis. Movemos el inicio de la simulación hasta el principio de 2018 y veremos cuánto tiempo funcionan las tendencias encontradas.

Fig.5 Informe del EA SingleBar sobre el trading según el horario de los viernes de 2019, en el intervalo 2018-2019, EURUSD H1

Fig.5 Informe del EA SingleBar sobre el trading según el horario de los viernes de 2019, en el intervalo 2018-2019, EURUSD H1

Aunque las indicaciones se han empeorado, se ve que las mismas «reglas» funcionaban ya desde la mitad de 2018, y por tanto, podían ser encontradas antes a través del análisis OLAP para el trading en el «futuro actual». Sin embargo, la búsqueda del período óptimo del análisis y la aclaración de la duración de las regularidades encontradas es un tema grande aparte. En cierto sentido, el análisis OLAP requiere la misma optimización que la optimización de los EAs. Teóricamente, se podría implementar el enfoque durante el cual OLAP está incorporado en el EA que se repasa en el Probador de Estrategias en diferentes secciones del historial con el desplazamiento de las fechas iniciales y de diferente longitud, y luego se invoca la simulación en el período forwars para cada una. Esta tecnología se conoce en aplicación a los EAs comunes como el análisis cluster paso a paso (Cluster Walk-Forward), pero MetaTrader no la soporta en total (en el momento de la creación del artículo se puede usar el inicio automático de las pruebas forward, pero el desplazamiento de la ventana de la optimización y la modificación de sus tamaños tiene que realizarse personalmente usando MQL5, o algún otro medio como los scripts shell).

En general, OLAP tiene que ser considerado como una herramienta de investigaciones que detecta las direcciones para un análisis más detallado por otros medios, incluyendo también la tecnología tradicional de la optimización de los EAs escritos según los resultados del análisis. Más tarde, veremos la manera incorporar el motor OLAP en el EA y usarlo tanto en el Probador de Estrategias, como en línea.

Vamos a comprobar la estrategia comercial actual para algunos otros días. Mostraremos tanto los días felices, como los infelices.

Fig.6.a Informe del EA SingleBar sobre el trading durante los martes en 2018-2019, según el análisis de 2019, EURUSD H1

Fig.6.a Informe del EA SingleBar sobre el trading durante los martes en 2018-2019, según el análisis de 2019, EURUSD H1

Fig.6.b Informe del EA SingleBar sobre el trading durante los miércoles en 2018-2019, según el análisis de 2019, EURUSD H1

Fig.6.b Informe del EA SingleBar sobre el trading durante los miércoles en 2018-2019, según el análisis de 2019, EURUSD H1

Fig.6.c Informe del EA SingleBar sobre el trading durante los jueves en 2018-2019, según el análisis de 2019, EURUSD H1

Fig.6.c Informe del EA SingleBar sobre el trading durante los jueves en 2018-2019, según el análisis de 2019, EURUSD H1

Como se esperaba, el comportamiento ambiguo del trading durante los días diferentes demuestra la falta de soluciones universales y requiere mejoras.

Veamos qué horarios de las negociaciones podrían encontrarse al analizar las cotizaciones en un período más prolongado, por ejemplo de 2015 a 2019, negociando después en el modo forward en 2019.

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

Como podemos ver, con el aumento del período, la rentabilidad de cada hora determinada disminuye, en algún momento, la generalización empieza a jugar en contra de la búsqueda de las regularidades superando el plazo de su vida. Parece que el día más ventajoso es el miércoles, pero no se comporta muy estable en el forward, por ejemplo, para los siguientes ajustes:

obtenemos el informe:

Fig.7 Informe del EA SingleBar sobre el trading durante los miércoles de 2015 a 2019, sin incluir el año 2019, EURUSD H1

Fig.7 Informe del EA SingleBar sobre el trading durante los miércoles de 2015 a 2019, sin incluir el año 2019, EURUSD H1

Para solucionar este problema, se requiere una metodología más multifacética en la cual OLAP es sólo una de muchas herramientas. Además, tiene sentido buscar las regularidades más complejas (multifactoriales). Vamos a intentar otra estrategia comercial que va a considerar no sólo la variación cíclica temporal, sino también la dirección de la barra anterior.

Construcción de las estrategias comerciales a base del análisis OLAP de cotizaciones. Parte 2

Se puede suponer que la dirección de cada barra depende en cierta medida de la dirección de la barra anterior. Lo más probable es que esta dependencia tiene un carácter cíclico parecido, relacionado con las fluctuaciones diarias y semanales que hemos detectado en el apartado anterior. En otras palabras, al realizar el análisis OLAP es necesario no sólo acumular el tamaño y la dirección de las barras en la división por horas y días de la semana, sino también tomar en cuenta de alguna manera las características de la barra anterior. Vamos a usar los campos personalizados restantes para eso.

En el tercer campo personalizado vamos a calcular una covarianza «asimétrica» de dos barras adyacentes. Una covarianza habitual, como un producto de los rangos del movimiento de precios dentro de las barras considerando las direcciones (más es incremento, menos es decremento), no posee un valor predicativo muy grande, ya que la barra anterior y la siguiente son equivalentes en el valor obtenido de la covarianza. No obstante, las soluciones comerciales son eficaces sólo para la siguiente barra, a pesar de que se hacen a base de la barra anterior. En otras palabras, una alta covarianza gracias a grandes movimientos de la barra anterior ya ha sido recuperada ya que esta barra ya se encuentra en el historial. Precisamente por eso, ha sido propuesta la fórmula «asimétrica» de la covarianza en la que se considera prácticamente sólo el rango de la futura barra y el signo del resultado con la anterior.

Basándose en el valor de este campo, se puede comprobar 2 estrategias: de tendencia y de reversa. Por ejemplo, si usamos el agregador ya conocido del factor del beneficio por este campo, los valores más de 1 indican en la rentabilidad del trading en la dirección de la barra anterior, mientras que los valores menos de 1, indican en la rentabilidad del movimiento opuesto. Analógicamente con los cálculos anteriores, cuanto más extremo sea el valor (mucho más grande que 1, o mucho menos que 1), más beneficiosas serán las operaciones de tendencia o de reversa en la estadística, respectivamente.

En el cuarto campo personalizado vamos a guardar el indicio unidireccional (+1) o bidireccional (-1) de las barras adyacentes. Mediante los agregadores, permitirá determinar el número de las barras adyacentes de reversa y la eficacia de las entradas para las estrategias de tendencia y de reversa.

Puesto que las barras se repasan en orden cronológico de forma garantizada (se garantiza por el adaptador), podemos guardar la amplitud de la barra anterior y su spread necesarios para los cálculos en las variables estáticas. Desde luego, eso es posible hasta que se supone usar la única instancia del adaptador de cotizaciones (recordemos que, por defecto, su instancia se crea en el archivo de cabecera). Eso conviene para nuestro ejemplo y es más fácil para la percepción, pero en caso general, el adaptador tenía que transmitir un cierto contenedor al constructor de los registros personalizados (como CustomQuotesBaseRecord) y luego al método fillCustomFields. Este contenedor permite guardar y recuperar el estado, por ejemplo, como una referencia al array: fillCustomFields(double &bundle[]).

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

Modificamos los valores de los parámetros de entrada OLAPQTS. La modificación más principal es la selección de "custom 3" en AggregatorField. Los selectores por los ejes X y Y, tipo del agregador (factor del beneficio) y la ordenación se quedan sin alterar. Además, cambiaremos el filtro de las fechas.

Como ya hemos visto en el ejemplo del análisis de las cotizaciones desde 2015, la selección de un período prolongado, lo más probable, está justificada sólo para los sistemas en los cuales se busca una variación cíclica estacional (le correspondería el selector month-of-year). En nuestro caso con los selectores de las horas y días de la semana, hagamos el análisis del 2018, y luego hagamos la prueba forward para el año 2019.

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

Para comprobar la estrategia colocada en el campo "custom 3", escribiremos el EA NextBar. Permitirá comprobar las posibilidades comerciales encontradas en el Probador de Estrategias. La construcción general del EA parece a SingleBar: se usan los mismos parámetros de entrada, las mismas funciones y fragmentos del código. La verdad es que se complica un poco la lógica comercial: se puede conocerla en el archivo fuente adjunto.

Vamos a seleccionar las combinaciones de las horas más atractivas (con factor del beneficio aproximadamente igual a 2 y más, o 0,5 y menos), por ejemplo, para el lunes:

Iniciamos la prueba en el rango 2018.01.01-2019.05.01:

Fig.8 Informe del EA NextBar en el intervalo 01.01.2018-01.05.2019 después del análisis OLAP del año 2018, EURUSD H1

Fig.8 Informe del EA NextBar en el intervalo 01.01.2018-01.05.2019 después del análisis OLAP del año 2018, EURUSD H1

Como se puede ver del informe, la estrategia ha funcionado bien en enero de 2015, luego ha empezado una serie de pérdidas. De alguna manera, tenemos que averiguar los plazos de vida de las regularidades y aprender a cambiarlas «sobre la marcha».

Trading adaptable a base del análisis OLAP de cotizaciones

Hasta ahora, para el análisis OLAP usábamos el EA especial no comercial OLAPQTS, y diseñábamos unos EAs para verificar las hipótesis. Lo más lógico y conveniente sería tener un motor OLAP incorporado en el EA. De esta manera, el robót podría analizar automáticamente las cotizaciones con una determinada periodicidad y modificar los horarios de las negociaciones. Además de eso, al colocar los parámetros principales del análisis en los ajustes del EA, podemos realizar la optimización para ellos, que en cierto sentido va a emular la tecnología Walk-Forward mencionada anteriormente. Vamos a llamar al EA OLAPQRWF, como abreviación de OLAP of Quotes with Rolling Walk-Forward.

Principales parámetros de entrada:

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

Además, tenemos que definir la periodicidad con la que va a recalcularse el cubo OLAP.

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

Además, podemos seleccionar no sólo la estrategia, sino también los campos personalizados en los que se calcula el agregador. Recordemos que los campos 1 y 3 se calculan considerando la amplitud de las barras (para las estrategia 0 y 1, respectivamente), y los campos 2 y 4 consideran sólo el número de las barras en cada dirección.

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

Prestamos la clase CustomQuotesRecord desde OLAPQTS sin cambios. Todos los parámetros de entrada anteriores para el ajuste de los selectors, filtro y agregador los hacemos constantes (const) o simplemente como variables globales (si tienen que cambiarse dependiendo de la estrategia) sin cambiar sus nombres.

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

Nótese que no vamos a filtrar las barras por el tiempo, sino por su número usando FIELD_INDEX. El valor actual para _Filter1value1 va a calcularse como la diferencia entre el número general de las barras y BarNumberLookBack establecido. Así, siempre van a analizarse las últimas BarNumberLookBack barras.

El EA va a tradear en el modo barra por barra desde el manejador OnTick.

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

Dependiendo de la periodicidad del análisis, esperamos el cambio del mes o de la semana, e iniciamos OLAP en la función calcolap.

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

Esta parte del código también la conocemos. Algunos cambios se refieren sólo a la selección del campo de agregación de acuerdo con los parámetros de entrada, así como a la definición del índice de la primera barra analizada.

Otra modificación importante consiste en el uso de su objeto especial de pantalla (stats) que va a invocarse por el motor OLAP tras la conclusión del análisis.

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

Puesto que este objeto va a extraer las mejores horas del trading desde la estadística obtenida, sería conveniente confiarle la propia negociación, para lo cual ha sido reservado el método trade. De esta manera, en OnTick se puede escribir:

  void OnTick()
  {
    // ...

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

Ahora vamos a considerar la clase MyOLAPStats con más detalles. Los resultados del análisis OLAP se procesan por el método display (principal método virtual de la pantalla) y saveVector (auxiliar).

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

En la clase, se describe el array bidimensional index para almacenar los indicadores de la eficacia de las negociaciones en vinculación con el horario. Este array se rellena consecutivamente en el método display con los vectores desde el cubo OLAP. La función auxiliar saveVector copia los números para todas las 24 horas de un determinado día laboral. A través de la segunda dimensión, el valor del indicador, número de la hora y el número del día laboral se escriben consecutivamente dentro del array index. El hecho de que los indicadores se encuentran en el primer (cero) elemento permite ordenar el array por el factor del beneficio, pero, en principio, eso es necesario sólo para la conveniencia de la visualización en el log.

A base de los datos del archivo index, se realiza la selección del modo comercial y el envío de las órdenes para los cómputos de la hora del día y del día de la semana donde se supera el límite establecido del factor del beneficio.

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

Para abreviar, se muestra el procesamiento sólo de una estrategia comercial, ya conocemos este código gracias al EA de prueba. El código fuente completo se adjunta al artículo.

Hagamos la optimización del EA OLAPQRWF en el intervalo de 2015 a 2019, y a continuación hagamos pa prueba forward en 2019. Obsérvese que el sentido de la optimización se reduce prácticamente a la búsqueda de los parámetros meta del trading: duración del análisis OLAP, frecuencia de la reconstrucción del cubo OLAP, selección de la estrategia y del campo personalizado de la agregación. En cada recorrido de la optimización, el EA construye el cubo OLAP _a base del historial_ y opera en su _futuro_ virtual usando los ajustes del _pasado_. Podemos preguntar, ¿para qué necesitamos la prueba forward en este caso? La cosa es que la eficacia de este trading depende directamente de los parámetros meta, y precisamente por esa razón es necesario comprobar la aplicabilidad de los ajustes seleccionados en el intervalo out-of-sample.

Vamos a optimizar todos los parámetros que influyen en el análisis, salvo el período Update (los dejamos mensual):

El EA calcula el indicador sintético personalizado de la optimización que es igual al producto del coeficiente de Sharp y el número de transacciones. Según este indicador, es la mejor previsión con los siguientes parámetro del EA:

Iniciamos la prueba separada de 2015 a 2020 y marcamos el comportamiento en forward.

Fig. 9 Informe del EA OLAPQRWF para el período de 01.01.2015 a 01.01.2020 tras la «optimización» de la ventana del análisis OLAP hasta 2018 inclusive, EURUSD H1

Fig. 9 Informe del EA OLAPQRWF para el período de 01.01.2015 a 01.01.2020 tras la «optimización» de la ventana del análisis OLAP hasta 2018 inclusive, EURUSD H1

Se puede constatar que el EA que encuentra automáticamente un horario ventajoso opera con éxito en 2019, usando el tamaño de la ventana de agregación encontrado durante los años anteriores. Naturalmente, este sistema requiere el siguiente estudio y el análisis profundizado, pero la capacidad funcional de la herramienta está confirmada.

Conclusión

Pues bien, hemos perfeccionado y ampliado la funcionalidad de la biblioteca OLAP del procesamiento operativo de los datos. Luego, hemos realizado su vinculación a través de un adaptador especial y las clases de los registros con el área de cotizaciones. Usando los programas presentados, se puede encontrar las regularidades en el historial que ofrecen una negociación rentable. En la primera etapa, con el fin de familiarizarse con el análisis OLAP, conviene usar unos EAs no negociables, que sólo procesan los datos fuente y proporcionan los indicadores sumarios. También permiten desarrollar y depurar los algoritmos del cálculo de los campos personalizados en los que han sido depositados los elementos base de las estrategias comerciales (hipótesis). En las siguientes etapas del estudio de OLAP, el motor se integra con los robots comerciales nuevos o ya existentes. En este caso, la optimización de los robots tiene que considerar no sólo los parámetros de trabajo habituales, sino también los nuevos parámetros meta que están relacionados con OLAP e influyen en la recopilación de las estadísticas.

Esta claro que las herramientas de OLAP no es una panacea para todas las situaciones del mercado (sobre todo para las mal predecibles), y no pueden ofrecer un Santo Grial a la medida. Pero el análisis incorporado de las cotizaciones, desde luego, amplia las posibilidades del trader en cuanto a la búsqueda de las estrategias y el diseño de los Asesores Expertos.