English Русский 中文 Deutsch 日本語 Português
preview
Creación simple de indicadores complejos usando objetos

Creación simple de indicadores complejos usando objetos

MetaTrader 5Ejemplos | 3 noviembre 2022, 16:35
692 0
Manuel Alejandro Cercos Perez
Manuel Alejandro Cercos Perez

1. Introducción

Si alguna vez ha intentado crear o modificar un indicador complejo, probablemente haya tenido problemas al incrementar el número de búferes, pues hay que declarar toneladas de arrays dobles, establecerlos como búferes y hacer los ajustes necesarios...

Pero es que además tenemos gráficos: debemos declarar el tipo de gráfico, establecer todas sus propiedades y luego asegurarnos de que todo encaje correctamente y que declaremos el número correcto de búferes y gráficos (si hay menos, obtendremos el error Array Fuera de Rango o Array Out Of Range, o bien un gráfico invisible que se detectará solo durante el trabajo posterior).

Finalmente, el asunto llega hasta los datosdel búfer: si deseamos concatenar los datos de una gran cantidad de búferes (por ejemplo, obtener el promedio/máximo/mínimo de 10 búferes en un solo búfer), deberemos escribir líneas muy largas de código repetitivo que compare/concatene cada búfer, o aplicar trucos con macros o funciones para ahorrar espacio. Como resultado, lo más probable es que terminemos con un código complejo y propenso a errores con muchas líneas y funciones duplicadas. Si cometemos aunque sea un error tipográfico en alguna parte, ¡encontrarlo será una pesadilla!

Semejantes situaciones pueden desalentar a los programadores principiantes (e incluso avanzados) respecto a la creación de indicadores complejos desde el punto de vista visual o funcional. No obstante, existe un pequeño truco sutil que puede convertir la codificación de indicadores en algo más rápido y sencillo:

Podemos establecer como búferes arrays que estén contenidos dentro de los objetos

En este artículo, mostraremos qué nos ofrece esta función y cómo podemos usarla en la programación orientada a objetos.


2. Primer ejemplo

Antes de comenzar a crear el indicador, veamos qué aspecto tiene la forma más simple de un objeto que contendrá arrays de búfer:

class CIndicatorPlot
{
public:
   double            array[];
};

Aquí solo hay un array público. Esto debe ser así para que podamos acceder a él al configurarlo como un búfer o configurar/acceder a sus datos (como es el caso con cualquier otro indicador).

Vamos a crear un indicador para representar 10 RSI con diferentes periodos y su valor promedio. Comenzaremos con las propiedades, los parámetros de entrada y la función OnInit .

#property indicator_buffers 11
#property indicator_plots 11

input int firstPeriod = 6;
input int increment = 2;

CIndicatorPlot indicators[];
int handles[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{

   ArrayResize(indicators, 11);
//--- indicator buffers mapping

   for (int i=0; i<11; i++)
   {
      SetIndexBuffer(i, indicators[i].array, INDICATOR_DATA);
      PlotIndexSetInteger(i, PLOT_DRAW_TYPE, DRAW_LINE);
   }

   for (int i=0; i<10; i++)
      PlotIndexSetInteger(i, PLOT_LINE_COLOR, clrRed);


   PlotIndexSetInteger(10, PLOT_LINE_COLOR, clrCyan);
   PlotIndexSetInteger(10, PLOT_LINE_STYLE, STYLE_DASH);
   PlotIndexSetInteger(10, PLOT_LINE_WIDTH, 2);

   ArrayResize(handles, 10);
   for (int i=0; i<10; i++)
      handles[i] = iRSI(NULL, PERIOD_CURRENT, firstPeriod+i*increment, PRICE_CLOSE);


//---
   return(INIT_SUCCEEDED);
}

Tenga en cuenta que solo usaremos dos propiedades: indicator_buffers y indicator_plots. Estas son dos propiedades que se usan constantemente, si no consideramos las comunes (copyright, link, version, separate/chart window, etc.). Otras propiedades (el color de línea, el tipo de dibujado...) son opcionales, pero para obtener un código más compacto, las configuraremos con PlotIndexSetInteger en un ciclo.
Para este indicador necesitaremos 10 búferes para cada RSI con un periodo diferente, y uno más para su valor promedio. Los pondremos todos dentro de un array y también crearemos los manejadores de los indicadores en OnInit. 

Ahora vamos a realizar los cálculos y a copiar los datos...

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
//---
   int limit = MathMax(0, prev_calculated-1);

   for (int i=0; i<10; i++)
   {
      if (limit==0)
         CopyBuffer(handles[i], 0, 0, rates_total-limit, indicators[i].array);
      else
      {
         double newValues[];
         CopyBuffer(handles[i], 0, 0, rates_total-limit, newValues);

         for (int k=0; k<rates_total-limit; k++)
         {
            indicators[i].array[limit+k] = newValues[k];
         }
      }
   }

   for (int i=limit; i<rates_total; i++)
   {
      indicators[10].array[i] = 0.0;
      for (int j=0; j<10; j++)                            
         indicators[10].array[i] +=indicators[j].array[i];

      indicators[10].array[i]/=10.0;
   }


//--- return value of prev_calculated for next call
   return(rates_total);
}

Tenga en cuenta que el cálculo del promedio de todos los búferes ahora es tan fácil como ejecutar un ciclo. Si cada búfer se declarara a nivel global como un array doble (como es habitual), su adición no resultaría tan fácil y ocuparía más líneas.

No se olvide de liberar los manejadores...

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      IndicatorRelease(handles[i]);
}
//+------------------------------------------------------------------+

El resultado se muestra a continuación:

Considerando la longitud del código, no está mal, pero se podría hacer mejor. Realizaremos algunas mejoras en la siguiente sección.


3. Añadiendo más funciones

Aunque ahorramos algo de espacio usando esta clase y configurando las propiedades de inicialización (en lugar de #property), todavía tendríamos que configurar manualmente los búferes y los gráficos, cosa no siempre fácil en circunstancias normales. ¿Podemos simplificar este proceso? Sí que podemos. Para hacer esto, deberemos delegar una serie de funciones en la clase.

Primero, añadiremos algunas funciones adicionales a la clase que necesitaremos más adelante.

class CIndicatorPlot
{
private:
   int               indicator_plot;

public:
   double            array[];

   void              SetBuffer(int &buffer, int &plot);
   void              SetLineWidth(int width);
   void              SetLineStyle(ENUM_LINE_STYLE style);
   void              SetLineColor(color line_color);
   void              SetLabel(string label);
};

La función SetBuffer establecerá el búfer de indicador y el gráfico. Al transmitir las dos variables por enlace, los cambios realizados en ellas por un objeto se reflejarán en las llamadas posteriores de otros objetos. El índice del gráfico se guardará para establecer otras propiedades.

El resto de las funciones del conjunto son setters simples de las propiedades de gráfico.

//+------------------------------------------------------------------+
void CIndicatorPlot::SetBuffer(int &buffer,int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, array, INDICATOR_DATA);
   PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE);

   buffer++; //Increment for other steps (One buffer in this case)
   plot++;   //Increment one plot in any case
}

//+------------------------------------------------------------------+
void CIndicatorPlot::SetLineWidth(int width)
{
   PlotIndexSetInteger(indicator_plot, PLOT_LINE_WIDTH, width);
}

//---
//...

Para hacer que el indicador resulte visualmente más atractivo, crearemos una función de interpolación del color que usaremos más adelante:

//+------------------------------------------------------------------+
//| Function to linearly interpolate 2 colors                        |
//+------------------------------------------------------------------+
color InterpolateColors(color colorA, color colorB, double factor)
{
   if (factor<=0.0) return colorA;
   if (factor>=1.0) return colorB;

   int result = 0;

   for (int i=0; i<3; i++) //R-G-B
   {
      int subcolor = int(
                        ((colorA>>(8*i))&(0xFF))*(1.0-factor) +
                        ((colorB>>(8*i))&(0xFF))*factor
                     );

      subcolor = subcolor>0xFF?0xFF:(
                    subcolor<0x00?0x00:
                    subcolor);

      result |= subcolor<<(8*i);
   }
   return (color)result;
}

Ahora la función OnInit tiene el aspecto que sigue:

CIndicatorPlot* indicators[];
CIndicatorPlot average;
int handles[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   ArrayResize(indicators, 10);
   ArrayResize(handles, 10);


   int index=0, plot=0;

   for (int i=0; i<10; i++)
   {
        indicators[i] = new CIndicatorPlot();
   
      indicators[i].SetBuffer(index, plot);
      indicators[i].SetLineColor(InterpolateColors(clrYellow, clrRed, i/9.0));
      indicators[i].SetLabel("RSI ("+IntegerToString(firstPeriod+i*increment)+")");

      handles[i] = iRSI(NULL, PERIOD_CURRENT, firstPeriod+i*increment, PRICE_CLOSE);
   }

   average.SetBuffer(index, plot);
   average.SetLineColor(clrBlue);
   average.SetLineStyle(STYLE_DASH);
   average.SetLineWidth(2);
   average.SetLabel("Average");

//---
   return(INIT_SUCCEEDED);
}

Preste atención a que no nos hemos referido a ningún búfer o gráfico en ninguna parte por su número. Las clases se han ocupado de este problema. Ahora será más fácil establecer correctamente cualquier propiedad del gráfico o reordenar los búferes, porque podremos referirnos a ellos con un objeto en lugar de un índice. También hemos añadido colores y etiquetas a los gráficos.

En este ejemplo, también hemos cambiado la estructura del indicador, usando un array de punteros para RSI (para demostrar la posibilidad de usar objetos creados dinámicamente) y separando el promedio del array. Ahora necesitamos cambiar los enlaces al promedio en OnCalculate y eliminar los indicadores en el array de punteros en OnDeInit.

void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      IndicatorRelease(handles[i]);
   for (int i=0; i<10; i++)
        delete indicators[i];
}

El resultado tiene el aspecto siguiente:

Los colores son el único cambio visual (también las etiquetas en la ventana de datos). Internamente, hemos mejorado nuestro proceso de trabajo facilitando las operaciones con gráficos y búferes, pero a nivel interno, todavía tenemos margen de mejora en cuanto a su organización.

Si observamos con detenimiento, podremos ver que cada descriptor es usado por solo uno de los búferes: cada búfer de RSI se puede calcular de forma independiente, por lo que podremos obligar a la clase a realizar los cálculos de forma interna (en lugar de en OnCalculate). El promedio necesita acceso al resto de los búferes, pero estos cálculos también se podrán delegar en una clase. Podemos usar la herencia para añadir cierta funcionalidad sin cambiar funciones o agregar condiciones a la clase básica.

Primero, añadiremos a la clase básica varios manejadores de eventos vacíos virtuales:

class CIndicatorPlot
{
   //...

public:
   
   //...

   virtual void      Init() { }
   virtual void      DeInit() { }
   virtual void      Update(const int start, const int rates_total) { }
};

Como hemos visto antes en este ejemplo, la actualización solo necesita start y rates_total para realizar sus cálculos, por lo que se omitirán los demás valores.

Ahora vamos a crear una clase RSI individual para crear y eliminar los descriptores necesarios. Además, hemos incluido una función para establecer el periodo de este identificador, pero también podremos incluir este parámetro en Init().

class CRSIIndividual : public CIndicatorPlot
{
private:
   int               handle;
   int               rsi_period;

public:

   void              SetPeriodRSI(int period);

   virtual void      Init();
   virtual void      DeInit();
   virtual void      Update(const int start, const int rates_total);
};

//+------------------------------------------------------------------+
void CRSIIndividual::SetPeriodRSI(int period)
{
   rsi_period = period;
}

//+------------------------------------------------------------------+
void CRSIIndividual::Init(void)
{
   handle = iRSI(NULL, PERIOD_CURRENT, rsi_period, PRICE_CLOSE);
}

//+------------------------------------------------------------------+
void CRSIIndividual::Update(const int start,const int rates_total)
{
   if (start==0)
      CopyBuffer(handle, 0, 0, rates_total-start, array);
   else
   {
      double newValues[];
      CopyBuffer(handle, 0, 0, rates_total-start, newValues);

      for (int k=0; k<rates_total-start; k++)
      {
         array[start+k] = newValues[k];
      }
   }
}

//+------------------------------------------------------------------+
void CRSIIndividual::DeInit(void)
{
   IndicatorRelease(handle);
}

Para la clase Average, necesitaremos almacenar los punteros para acceder al resto de los objetos del gráfico del indicador (Individual RSI). En este caso, Init() y DeInit() no serán necesarios.

class CRSIAverage : public CIndicatorPlot
{
private:
   CRSIIndividual*   rsi_indicators[];

public:
   void              SetRSIPointers(const CRSIIndividual &rsi_objects[]);

   virtual void      Update(const int start, const int rates_total);
};

//+------------------------------------------------------------------+
void CRSIAverage::SetRSIPointers(const CRSIIndividual &rsi_objects[])
{
   int total = ArraySize(rsi_objects);
   ArrayResize(rsi_indicators, total);

   for (int i=0; i<total; i++)
      rsi_indicators[i] = (CRSIIndividual*)GetPointer(rsi_objects[i]);
}

//+------------------------------------------------------------------+
void CRSIAverage::Update(const int start,const int rates_total)
{
   for (int i=start; i<rates_total; i++)
   {
      array[i] = 0.0;
      for (int j=0; j<10; j++)
         array[i] +=rsi_indicators[j].array[i];

      array[i]/=10.0;
   }
}

La creación de un array de punteros puede parecer demasiado complicada, ya que podemos acceder a los objetos directamente desde el nivel global, pero esto facilitará la reutilización de la clase en otros indicadores sin realizar cambios adicionales. En este ejemplo, volveremos a utilizar un array de objetos en lugar de punteros para los indicadores de RSI, por lo que necesitaremos obtener los punteros de ellos.

En la etapa final, la función OnInit (y las declaraciones de objetos anteriores) se verán así...

CRSIIndividual indicators[];
CRSIAverage average;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   ArrayResize(indicators, 10);


   int index=0, plot=0;

   for (int i=0; i<10; i++)
   {
      indicators[i].SetBuffer(index, plot);
      indicators[i].SetLineColor(InterpolateColors(clrYellow, clrRed, i/9.0));
      indicators[i].SetLabel("RSI ("+IntegerToString(firstPeriod+i*increment)+")");

      indicators[i].SetPeriodRSI(firstPeriod+i*increment);
      indicators[i].Init();                               
   }

   average.SetBuffer(index, plot);
   average.SetLineColor(clrBlue);
   average.SetLineStyle(STYLE_DASH);
   average.SetLineWidth(2);
   average.SetLabel("Average");

   average.SetRSIPointers(indicators);                    

//---
   return(INIT_SUCCEEDED);
}

...y podremos hacer que otras funciones de gestión de eventos sean mucho más limpias:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
//---
   int limit = MathMax(0, prev_calculated-1);

   for (int i=0; i<10; i++)
      indicators[i].Update(limit, rates_total);

   average.Update(limit, rates_total);

//--- return value of prev_calculated for next call
   return(rates_total);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      indicators[i].DeInit();
}
//+------------------------------------------------------------------+

Visualmente, el indicador tendrá exactamente el mismo aspecto que en el segundo ejemplo.



4. Ampliando la clase

Por ahora, todas estas clases hacen bien su trabajo, pero son muy específicas para el tipo de indicador con el que estamos trabajando: solo hemos usado algunas propiedades del gráfico y solo el dibujo lineal, pero ¿qué sucederá si queremos usar un gráfico con búferes de color? ¿O un histograma, o un zigzag?... Para utilizar de nuevo lo que hemos hecho, necesitaremos generalizar las clases. Para ello, deberemos cumplir tres condiciones:

  • Deberemos poder crear cualquier tipo de gráfico/búfer o cambiar las propiedades del gráfico sin abandonar la clase o conocer los detalles de los índices del búfer/gráfico.
  • Deberemos poder añadir gráficos con cualquier estilo de dibujado (líneas, gráficos de barras, velas...) sin tener que preocuparnos por la cantidad y los tipos de búfer que tiene cada uno (no obstante, siempre seremos responsables de los datos que colocamos en estas matrices).
  • Deberemos poder añadir fácilmente ciertas funciones a las clases usando la herencia (no es obligatorio).

Con eso en mente, primero explicaremos cómo se implementan las clases y cómo se estructura la herencia.

Primero, las clases se estructuran así:

  • CIndicatorBufferBase
    • CIndicatorCalculations
    • CIndicatorPlotBase
      • CIndicator_1Datos
        • CIndicatorPlotLine
        • CIndicatorPlotHistogram
        • ...
        • CIndicator_1Data1Color
          • CIndicatorPlotColorLine
          • ...
      • CIndicator_2Datos
        • CIndicatorPlotHistogram2
        • ...
        • CIndicator_2Data1Color
          • CIndicatorPlotColorHistogram2
          • ...
      • CIndicator_4Data
        • CIndicatorPlotCandles
        • ...
        • CIndicator_4Data1Color
          • CIndicatorPlotColorCandles
          • ...

Algunos momentos clave:

  • Los tres puntos significan que hay más clases que heredarán de lo mismo que arriba (se diferenciarán solo en el estilo de dibujado, que está indirectamente relacionado con cada clase).
  • Las clases marcadas en rojoson clases abstractas que no pueden tener copias, pero que pueden almacenar punteros a otras clases derivadas de ellas (polimorfismo).
  • El resto de las clases heredarán de la clase básica, que tiene el número correspondiente de búferes de datos/colores. En este caso, el polimorfismo también será posible, porque podríamos tener un indicador que necesite acceso a una clase que tenga un búfer de datos, ya sea una línea, un histograma, etc.
  • Las clases de color heredarán de los búferes de datos por la misma razón que la anterior.
  • CIndicatorCalculations se utiliza para los búferes de cálculo auxiliares que no tienen gráficos.

En resumen, la implementación se verá así:

//+------------------------------------------------------------------+
//| Base class for plots and calculation buffers                     |
//+------------------------------------------------------------------+
class CIndicatorBufferBase
{
public:
   virtual void      SetBuffer(int &buffer, int &plot)=NULL;
   virtual void      SetAsSeries(bool set)=NULL;

   virtual void      Init() { }
   virtual void      DeInit() { }
   virtual void      Update(const int rates_total,
                            const int prev_calculated,
                            const datetime &time[],
                            const double &open[],
                            const double &high[],
                            const double &low[],
                            const double &close[],
                            const long &tick_volume[],
                            const long &volume[],
                            const int &spread[]) { }
};

//+------------------------------------------------------------------+
//| Calculations Buffer (with no plot)                               |
//+------------------------------------------------------------------+
class CIndicatorCalculations : public CIndicatorBufferBase
{
public:
   double            array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicatorCalculations::SetBuffer(int &index, int &plot)
{
   SetIndexBuffer(index, array, INDICATOR_CALCULATIONS);

   index++;
//No plot is used
}

//+------------------------------------------------------------------+
void CIndicatorCalculations::SetAsSeries(bool set)
{
   ArraySetAsSeries(array, set);
}

//+------------------------------------------------------------------+
//| Base indicator plot class                                        |
//+------------------------------------------------------------------+
class CIndicatorPlotBase : public CIndicatorBufferBase
{
protected:

   int               indicator_plot;

   virtual void      SetDrawType()=NULL; //Implicit in each class

public:

   void              SetArrow(uchar arrow);
   void              SetArrowShift(int shift);
   void              SetDrawBegin(int begin);
   void              SetShowData(bool show);
   void              SetShift(int shift);
   void              SetLineStyle(ENUM_LINE_STYLE style);
   void              SetLineWidth(int width);
   void              SetColorIndexes(color &color_array[]);
   void              SetLineColor(color line_color);
   void              SetLineColor(color line_color, int index);
   void              SetEmptyValue(double empty);
   void              SetLabel(string label);

   int               GetInteger(ENUM_PLOT_PROPERTY_INTEGER property_id, int property_modifier=0);
};

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 1 Data Buffer                           |
//+------------------------------------------------------------------+
class CIndicator_1Data : public CIndicatorPlotBase
{
public:

   double            array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicator_1Data::SetBuffer(int &buffer,int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, array, INDICATOR_DATA);
   SetDrawType();

   buffer++;
   plot++;
}

//...

//+------------------------------------------------------------------+
//| Plot Line (1 data buffer)                                        |
//+------------------------------------------------------------------+
class CIndicatorPlotLine : public CIndicator_1Data
{
protected:

   virtual void      SetDrawType() final;
};

//+------------------------------------------------------------------+
void CIndicatorPlotLine::SetDrawType(void)
{
   PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE);
}

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 2 Data Buffers                          |
//+------------------------------------------------------------------+
class CIndicator_2Data : public CIndicatorPlotBase
{
public:

   double            first_array[];
   double            second_array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};


//+------------------------------------------------------------------+
void CIndicator_2Data::SetBuffer(int &buffer, int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, first_array, INDICATOR_DATA);
   SetIndexBuffer(buffer+1, second_array, INDICATOR_DATA);
   SetDrawType();

   buffer+=2;
   plot++;
}

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 1 Data Buffer & 1 Color Buffer          |
//+------------------------------------------------------------------+
class CIndicator_1Data1Color : public CIndicator_1Data
{
public:

   double            color_buffer[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicator_1Data1Color::SetBuffer(int &buffer, int &plot)
{
   CIndicator_1Data::SetBuffer(buffer, plot);

   SetIndexBuffer(buffer, color_buffer, INDICATOR_COLOR_INDEX);

   buffer++; //Add color buffer
}

//+------------------------------------------------------------------+
void CIndicator_1Data1Color::SetAsSeries(bool set)
{
   CIndicator_1Data::SetAsSeries(set);
   ArraySetAsSeries(color_buffer, set);
}

//...

Cada clase contendrá (y establecerá) el número requerido de búferes. CIndicatorBufferBase tendrá controladores de eventos que, opcionalmente, podrán ser redefinidos por cualquier tipo de clase de búfer. CIndicatorPlotBase contendrá setters para todas las propiedades del gráfico (y un getter). Cada clase de datos básicos (con o sin color) contendrá declaraciones de arrays y funciones de establecimiento para búferes, y cada clase específica anulará la función SetDrawType() y la declarará como final para que no se pueda anular nuevamente (si necesitamos una clase con un tipo de dibujado indefinido, podremos heredar de la clase de básica de datos correspondiente y redefinir esta función).

Tenga en cuenta que en esta implementación, Update tiene todos los valores usados en el evento OnCalculate, pero se pueden redefinir con menos parámetros si no necesitamos usar el polimorfismo.

También hemos incluido ArraySetAsSeries, ya que es una característica muy común que casi siempre requiere que todos los búferes estén configurados de la misma forma.


Ahora que tenemos las clases, podremos crear el indicador. Vamos a añadir algo más como ejemplo:

  • Primero, crearemos bandas basadas en el indicador ATR y las mostraremos rellenas de color.
  • Luego, crearemos 10 medias móviles con diferentes periodos y las mostraremos en el gráfico como líneas.
  • Finalmente, usaremos el color de las velaspara cambiar el color de las velas dependiendo de cuántas medias móviles estén por encima o por debajo de las bandas.

Primero declararemos los datos de entrada e incluiremos los archivos para las clases de indicador y la interpolación de color que hicimos en el apartado 3:

#property indicator_buffers 19
#property indicator_plots 13

#include <OOPIndicators/IndicatorClass.mqh>
#include <OOPIndicators/ColorLerp.mqh>

input int atr_period = 10; //ATR Period
input double atr_band_multiplier = 0.8; //ATR Multiplier for bands
input bool show_bands = true; //Show Bands
input bool show_data = false; //Show Extra Data

input int ma_faster_period = 14; //MA Faster Period
input int ma_step = 2; //MA Step
input ENUM_MA_METHOD ma_method = MODE_SMA; //MA Method

Ya hemos definido el número de búferes y gráficos necesarios. No necesitaremos saber de antemano cuántos de estos necesitamos, pero como verá a continuación, resulta bastante fácil averiguar los valores (en OnInit()).

Luego crearemos clases para cada parte del indicador.

Comenzaremos con los rangos de ATR:

//+------------------------------------------------------------------+
//| ATR Bands class (inherit from Filling Plot)                      |
//+------------------------------------------------------------------+
class CATRBand : public CIndicatorPlotFilling
{
private:

   int               handle;

public:

   virtual void      Init();
   virtual void      DeInit();
   virtual void      Update(const int limit, const int rates_total, const double &close[]);
};

//+------------------------------------------------------------------+
void CATRBand::Init(void)
{
   handle = iATR(NULL, PERIOD_CURRENT, atr_period);
}

//+------------------------------------------------------------------+
void CATRBand::Update(const int limit,const int rates_total,const double &close[])
{
   double atr[];
   CopyBuffer(handle, 0, 0, rates_total-limit, atr);

   for (int i=limit; i<rates_total; i++)
   {
      first_array[i] = close[i]+atr[i-limit]*atr_band_multiplier;
      second_array[i] = close[i]-atr[i-limit]*atr_band_multiplier;
   }
}

//+------------------------------------------------------------------+
void CATRBand::DeInit(void)
{
   IndicatorRelease(handle);
}

Clase de media móvil que contiene los parámetros en Init() para el periodo y el método:

//+------------------------------------------------------------------+
//| Moving Averages class (inherit from Line Plot)                   |
//+------------------------------------------------------------------+
class CMA : public CIndicatorPlotLine
{
private:

   int               handle;

public:
   virtual void      Init(int period, ENUM_MA_METHOD mode);
   virtual void      DeInit();
   virtual void      Update(const int limit, const int rates_total);
};

//+------------------------------------------------------------------+
void CMA::Init(int period, ENUM_MA_METHOD mode)
{
   handle = iMA(NULL, PERIOD_CURRENT, period, 0, mode, PRICE_CLOSE);
}

//+------------------------------------------------------------------+
void CMA::Update(const int limit,const int rates_total)
{
   if (limit==0) CopyBuffer(handle, 0, 0, rates_total, array);
   else
   {
      double newVals[];
      CopyBuffer(handle, 0, 0, rates_total-limit, newVals);

      for (int i=limit; i<rates_total; i++)
         array[i] = newVals[i-limit];
   }
}

//+------------------------------------------------------------------+
void CMA::DeInit(void)
{
   IndicatorRelease(handle);
}

Y también la clase de velas. En este caso, y para evitar la complejidad adicional del ejemplo, nos referiremos a los objetos del nivel global. No obstante, no le recomiendo hacer esto si planea utilizar de nuevo una clase.

Asimismo, contiene macros, que también declararemos a continuación. Nota: las funciones se encuentran debajo de la macro en el código, pero el orden de las funciones se ha cambiado en el artículo.

//+------------------------------------------------------------------+
//| Color Candles class (inherit from Color Candles Plot)            |
//+------------------------------------------------------------------+
class CColorCandles : public CIndicatorPlotColorCandles
{
public:
   virtual void      Update(const int limit,
                            const int rates_total,
                            const double &open[],
                            const double &high[],
                            const double &low[],
                            const double &close[]);
};

//+------------------------------------------------------------------+
void CColorCandles::Update(const int limit,
                           const int rates_total,
                           const double &open[],
                           const double &high[],
                           const double &low[],
                           const double &close[])
{
   for (int i=limit; i<rates_total; i++)
   {
      open_array[i] = open[i];
      high_array[i] = high[i];
      low_array[i] = low[i];
      close_array[i] = close[i];

      int count_ma = TOTAL_MA;

      for (int m=0; m<TOTAL_MA; m++)
      {
         if (maIndicators[m].array[i] > bands.first_array[i]) count_ma++;
         if (maIndicators[m].array[i] < bands.second_array[i]) count_ma--;
      }

      color_buffer[i] = count_ma;

      //Update inside of this other object (to avoid making an extra inheritance, or an external loop)
      showIndex.array[i] = TOTAL_MA - count_ma;
   }
}

Ahora necesitaremos declarar los objetos y configurar los efectos visuales de los búferes y gráficos:

#define TOTAL_MA 10

CMA maIndicators[TOTAL_MA];
CATRBand bands;
CColorCandles candles;
CIndicatorPlotNone showIndex; //To show MAs above/below

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   int buffer=0, plot=0;

   bands.SetBuffer(buffer, plot);
   candles.SetBuffer(buffer, plot);
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].SetBuffer(buffer, plot);
   showIndex.SetBuffer(buffer, plot);

//Print("Buffers: ", buffer, "  Plots: ", plot);

//--- plot settings
   if (show_bands) bands.SetLineColor(clrDarkSlateGray);
   else bands.SetLineColor(clrNONE);
   bands.SetShowData(show_data);
   if (show_data)
      bands.SetLabel("Close + ATR;Close - ATR");


   for (int i=0; i<TOTAL_MA; i++)
   {
      maIndicators[i].SetLineColor(InterpolateColors(clrAqua, clrRoyalBlue, i/(TOTAL_MA-1.0)));
      maIndicators[i].SetLabel("MA("+IntegerToString(ma_faster_period+i*ma_step)+")");
      maIndicators[i].SetShowData(show_data);
      if (i>0 && i <TOTAL_MA-1) maIndicators[i].SetLineStyle(STYLE_DOT);
      else maIndicators[i].SetLineWidth(2);
   }

   color arrow_colors[TOTAL_MA*2+1];

   for (int i=0; i<TOTAL_MA; i++)
      arrow_colors[i] = InterpolateColors(clrGreenYellow, clrGray, i/double(TOTAL_MA));
   arrow_colors[TOTAL_MA] = clrGray;
   for (int i=TOTAL_MA+1; i<TOTAL_MA*2+1; i++)
      arrow_colors[i] = InterpolateColors(clrGray, clrOrange, (i-TOTAL_MA)/double(TOTAL_MA));

   candles.SetColorIndexes(arrow_colors);
   candles.SetLabel("Open;High;Low;Close");
   candles.SetShowData(false);

   showIndex.SetLabel("MAs above/below");
   showIndex.SetShowData(true);

//--- initialize classes
   bands.Init();
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].Init(ma_faster_period+i*ma_step, ma_method);

   return(INIT_SUCCEEDED);
}

Primero, configuraremos los búferes, luego las propiedades del gráfico y después inicializaremos los subindicadores (como se especifica en sus clases).

Como ya mencionamos anteriormente, podemos averiguar fácilmente el número de búferes y gráficos necesario imprimiendo los valores de las variables buffer y plot. Esto nos permitirá configurar las propiedades correctamente (al principio, es posible que deseemos establecerlas en un número mayor del que necesitamos para evitar errores).

Observe también que hemos incluido un ejemplar de la clase Plot None. Este objeto se actualizará usando el objeto de velas, por lo que no necesitará manejadores de eventos especiales, simplemente mostrará el número de medias móviles que están por encima o por debajo de las bandas en la ventana de datos.

Finalmente, no existe mucha funcionalidad en otros manejadores de eventos, ya que todo se encuentra dentro de los objetos. Solo necesitaremos llamar a las funciones de los objetos en el orden correcto:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
//---
   int limit = MathMax(0, prev_calculated-1);

   bands.Update(limit, rates_total, close);

   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].Update(limit, rates_total);

   candles.Update(limit, rates_total, open, high, low, close);

//--- return value of prev_calculated for next call
   return(rates_total);
}

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   bands.DeInit();
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].DeInit();
}

El resultado final se verá así:


5. Limitaciones del método

Si bien resulta cómodo, el uso de funciones en lugar de propiedades tiene varias desventajas: la más notable es la interferencia al cambiar los colores o el estilo de cualquier gráfico. A veces permanecen y, a veces, se sobrescriben durante la inicialización.

Podemos evitar este problema usando datos de entrada para los colores (en lugar de cambiarlos en la pestaña Colors) o comprobando si hay un color predeterminado que no sea el negro (0x000000). Esto funcionará con todos los colores excepto el negro.

if (obj.GetInteger(PLOT_LINE_COLOR)==clrBlack)
   obj.SetLineColor(clrYellow);

Además, en este artículo, no vamos a analizar el impacto del uso de estas clases en el rendimiento. En teoría, el uso directo de propiedades y menos funciones debería acelerar las cosas, pero no mucho, en la mayoría de los casos.

Finalmente, como habrá notado, las clases no contienen gráficos o manejadores de eventos OnTimer. La razón es que resulta mejor procesar los eventos de los gráficos directamente en OnChartEvent llamando a ciertas funciones después del procesamiento (en lugar de llamar a un manejador para cada indicador cada vez que ocurre un evento y tener que procesar cada evento varias veces). En el caso del temporizador, podemos usar el manejador de actualizaciones de forma diferente si nuestro indicador es de marco temporal múltiple o multidivisa (no tendremos acceso directo a los array de OnCalculate). Otra solución, que podrá parecer controvertida para muchos, es declarar los arrays utilizados como búferes con visibilidad pública: podremos establecer los arrays con visibilidad protegida. El indicador seguirá funcionando, pero es posible que tengamos que añadir métodos getter para acceder a los datos desde el exterior.


6. Conclusión

En este artículo, hemos desarrollado un método que permite crear indicadores complejos de forma más fácil y con menos líneas. Hemos comenzado con pequeños trucos organizativos para un caso específico, y luego hemos implementado una estructura de clases que permitirá la reutilización y la personalización de la funcionalidad. Finalmente, hemos reunido todo en un ejemplo de indicador que utiliza la mayoría de las funciones descritas en el artículo.


Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/11233

Archivos adjuntos |
MQL5.zip (10.17 KB)
Redes neuronales: así de sencillo (Parte 23): Creamos una herramienta para el Transfer Learning Redes neuronales: así de sencillo (Parte 23): Creamos una herramienta para el Transfer Learning
En esta serie de artículos, hemos mencionado el Aprendizaje por Transferencia más de una vez, pero hasta ahora no había sido más que una mención. Le propongo rellenar este vacío y analizar más de cerca el Aprendizaje por Transferencia.
DoEasy. Elementos de control (Parte 13): Optimizando la interacción de los objetos WinForms con el ratón. Comenzamos el desarrollo del objeto WinForms TabControl DoEasy. Elementos de control (Parte 13): Optimizando la interacción de los objetos WinForms con el ratón. Comenzamos el desarrollo del objeto WinForms TabControl
En el presente artículo, corregiremos y optimizaremos el procesamiento de la apariencia de los objetos WinForms después de mover el cursor del ratón lejos del objeto y comenzaremos a desarrollar el objeto TabControl WinForms.
Aprendizaje automático y Data Science - Redes neuronales (Parte 01): Análisis de redes neuronales con conexión directa Aprendizaje automático y Data Science - Redes neuronales (Parte 01): Análisis de redes neuronales con conexión directa
A muchos les gustan todas las operaciones que hay detrás de las redes neuronales, pero pocos las entienden. En este artículo, intentaremos explicar en términos sencillos lo que ocurre detrás un perceptrón multinivel con conexión Feed Forward.
Indicador CCI. Tres pasos para la transformación Indicador CCI. Tres pasos para la transformación
En este artículo, intentaremos realizar cambios adicionales en el indicador CCI. Estos cambios afectarán a la propia lógica del indicador, hasta el punto de que podremos ver este indicador en la ventana del gráfico principal.