Aplicar un Indicador a Otro

MetaQuotes | 18 diciembre, 2013

Introducción

Consideremos la tarea de mejorar un indicador que se aplica a valores de otro indicador. En este artículo, continuaremos trabajando con el True Strength Index (TSI), un índice creado y tratado en el artículo anterior  "MQL5: Create Your Own Indicator" ("MQL5: Cree su Propio Indicador").

Indicador Personalizado Basado en los Valores de Otro Indicador

Al escribir un indicador que usa la forma corta de la llamada de función OnCalculate(), puede que no se dé cuenta del hecho de que un indicador se puede calcular no solo por datos de precio, sino también por datos de otro indicador (independientemente de si viene incorporado o es personalizado).

Hagamos un sencillo experimento: adjunte el indicador incorporado RSI con una configuración estándar a un gráfico, y arrastre el indicador personalizado True_Strength_Index_ver2.mq5 a la ventana del indicador RSI. En la pestaña de Parameters (Parámetros) de la ventana que aparece, especifique que el indicador debe aplicarse a  los Previous Indicator's Data (Datos del Indicador Anterior) (RSI(14)).

El resultado será muy diferente a lo que esperábamos. La línea adicional del indicador TSI no apareció en la ventana del indicador RSI, y en la ventana de datos verá que sus valores tampoco están claros.

A pesar del hecho de que los valores del RSI están definidos casi a través del historial entero, los valores del TSI (aplicados a los datos del RSI) son completamente inexistentes (al principio), o siempre son iguales a -100:

Tal comportamiento se da a causa del hecho de que el valor del parámetro begin no se usa en ningún lugar en la función OnCalculate() de nuestro índice True_Strength_Index_ver2.mq5. El parámetro begin  especifica el número de valores vacíos en el parámetro de entrada price[]. Estos valores vacíos no se pueden usar en los cálculos de valores del indicador. Recordemos la definición de la primera forma de la llamada de función OnCalculate().
int OnCalculate (const int rates_total,      // price[] array length
                 const int prev_calculated,  // number of bars calculated after previous call
                 const int begin,            // start index of meaningful data
                 const double& price[]       // array for calculation
   );

Al aplicar el indicador a los datos de precio especificando una de las constantes de precio, el parámetro begin es igual a 0, porque hay un tipo de precio especificado para cada barra. Por tanto, el array de entrada price[]  tiene datos correctos empezando por su primer elemento, price[0]. Pero si especificamos datos de otro indicador como fuente de cálculos, ya no está garantizado.

El Parámetro begin de OnCalculate()

Revisemos los valores que contiene el array price[], si se realiza el cálculo usando datos de otro indicador. Para ello, añadiremos otro código a la función OnCalculate(), que imprimirá los resultados que queremos revisar. Ahora, el comienzo de la función OnCalculate() tendrá este aspecto:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate (const int rates_total,    // price[] array length;
                 const int prev_calculated,// number of available bars after previous call;
                 const int begin,          // start index of meaningful data in price[] array 
                 const double &price[])    // data array, that will be used for calculations;
  {
//--- flag for single output of price[] values
   static bool printed=false;
//--- if begin isn't zero, then there are some values that we shouldn't take into account

   if(begin>0 && !printed)
     {
      //--- let's output them
      Print("Data for calculation begin from index equal to ",begin,
            "   price[] array length =",rates_total);

      //--- let's show the values that we shouldn't take into account for calculation
      for(int i=0;i<=begin;i++)
        {
         Print("i =",i,"  value =",price[i]);
        }
      //--- set printed flag to confirm that we have already logged the values
      printed=true;
     }

De nuevo, arrastremos y soltemos la versión modificada de nuestro indicador a la ventana RSI(14) y especifiquemos los datos del indicador anterior para el cálculo. Ahora veremos los valores que no están representados y que no deberíamos tener en cuenta para los cálculos en los que se usan los valores del indicador RSI(14).


Valores Vacíos en Buffers del Indicador y DBL_MAX

Los primeros 14 elementos del array price[] con índices de 0 a 13, ambos inclusive, tienen el mismo valor igual a 1.797693134862316e+308. Se encontrará con este número muy a menudo, porque es el valor numérico de la constante incorporada EMPTY_VALUE, que se usa para señalar valores vacíos en un buffer del indicador.

Rellenar valores vacíos con ceros no es una solución universal, porque este valor puede ser el resultado de un cálculo de otros indicadores. Por ello, todos los indicadores incorporados del terminal de cliente devuelven este número para valores vacíos. El valor 1.797693134862316e+308 fue elegido porque es el valor máximo posible del tipo doble, y por conveniencia, se presenta como la constante DBL_MAX en MQL5.

Para comprobar si un cierto número de tipo doble está vacío o no, puede compararlo con las constantes EMPTY_VALUE o DBL_MAX . Ambas variantes son iguales, pero es mejor usar la constante EMPTY_VALUE en su código para dejarlo claro.

//+------------------------------------------------------------------+
//| returns true for "empty" values                               |
//+------------------------------------------------------------------+
bool isEmptyValue(double value_to_check)
  {
//--- if the value is equal DBL_MAX, it has an empty value
   if(value_to_check==EMPTY_VALUE) return(true);
//--- it isn't equal DBL_MAX
   return(false);
  }

¡El DBL_MAX es un número enorme, y el indicador RSI, de forma inherente, no puede devolver tales valores! Y solo el décimo quinto elemento del array (con índice 14) tiene un valor razonable igual a 50. De modo que, aún si no sabemos nada sobre el indicador como fuente de datos a calcular, usando el parámetro begin podemos organizar el procesamiento de datos correctamente en tales casos. Para ser más precisos, debemos evitar usar estos valores vacíos en nuestros cálculos.

La Relación entre el Parámetro begin y la Propiedad PLOT_DRAW_BEGIN

Hay que destacar que hay una estrecha relación entre el parámetro begin, que se transfiere a la función OnCalculate(), y la propiedad PLOT_DRAW_BEGIN, que define el número de barras iniciales sin dibujar. Si miramos al código fuente del RSI del paquete estándar de MetaTrader5, veremos el siguiente código en la función OnInit():
//--- sets first bar from what index will be drawn
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,ExtPeriodRSI);

Esto significa que la representación gráfica con índice 0 comienza solo con la barra cuyo índice es igual a la ExtPeriodRSI (es una variante de entrada que especifica el período del indicador RSI), y no hay representación de barras anteriores.

En el lenguaje MQL5, el cálculo de un indicador A basado en los datos del indicador B siempre se realiza en valores de buffer cero del indicador B. Los valores cero del buffer del indicador B se transfieren como un parámetro de entrada price[] a la función OnCalculate() del indicador A. De forma inherente, el buffer cero se asigna a la representación gráfica cero con la función SetIndexBuffer(). Por tanto:

Regla de transferencia de la propiedad PLOT_DRAW_BEGIN al parámetro begin: Para cálculos del indicador personalizado A basados en los datos del otro indicador (base) B, el valor del parámetro de entrada begin en la función OnCalculate() siempre es igual al valor de la propiedad PLOT_DRAW_BEGIN del trazado gráfico cero del indicador base B .

De modo que si hemos creado un indicador RSI (indicador B) con un período 14 y después hemos creado nuestro indicador personalizado True Strength Index (Indicador A) basado en sus datos, entonces:

Recuerde que el indicador TSI no se dibuja desde el comienzo del gráfico, porque el valor del indicador no está determinado por las primeras barras. El índice de la primera barra, que se representará como línea en el indicador TSI, es igual a r+s-1, donde:

Para barras con índices menores de r+s-1 no hay valores para representar el indicador TSI. Por tanto, para los arrays finales MA2_MTMBuffer[] y EMA2_AbsMTMBuffer[] usados para calcular el indicador TSI, los datos tienen un offset adicional y comienzan desde el índice r+s-1. Puede encontrar más información en el artículo "MQL5: Create Your Own Indicator" ("MQL5: Cree su Propio Indicador").

Hay una declaración en la función OnInit() para desactivar el dibujo de las primeras barras r+s-1:

//--- first bar to draw
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,r+s-1);

Puesto que el comienzo de los datos de entrada ha sido empujado hacia delante por barras begin, debemos tenerlo en cuenta y aumentar la posición inicial de dibujo de datos por barras begin en la función OnCalculate():

  if(prev_calculated==0)
     { 
      //--- let's increase beginning position of data by begin bars,
      //--- because we use other indicator's data for calculation
      if(begin>0)PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,begin+r+s-1); 
     } 
Ahora estamos teniendo en cuenta el parámetro begin para calcular los valores del indicador TSI. Además, el parámetro begin se transferirá correctamente si otro indicador usa valores del TSI para hacer cálculos: beginother_indicator=beginour_indicator+r+s-1. Por tanto, podemos formular la regla de imponer un indicador en valores de otro indicador:

Regla de imposición de indicadores : Si un indicador personalizado A se dibuja empezando de una posición Na (los primeros valores Na no se representan) y está basado en datos de otro indicador B dibujado de la posición Nb, el indicador resultante A{B} se dibujará a partir de la posición Nab=Na+Nb, en la que A{B} significa que el indicador A está calculado en valores de buffer cero del indicador B.

Por tanto, TSI (25,13) {RSI (14)} significa que el indicador TSI (25,13) está compuesto de valores del indicador RSI (14). Como resultado de la imposición, ahora el comienzo de los datos es (25+13-1)+14=51. En otras palabras, el dibujo del indicador comenzará a partir de la barra 52 (el índice de las barras empieza con 0).

Añadir Valores begin para Usar en los Cálculos del Indicador

Ahora sabemos exactamente que los valores importantes del array price[] siempre comienzan la formación de la posición, especificada por el parámetro begin. Modifiquemos nuestro código paso a paso. Primero viene el código que calcula los valores de los arrays MTMBuffer[] y AbsMTMBuffer[]. Sin el parámetro begin, el relleno del array comienza con el índice 1.

//--- calculate values for mtm and |mtm|
   int start;
   if(prev_calculated==0) start=1;  // start filling MTMBuffer[] and AbsMTMBuffer[] arrays from 1st index 
   else start=prev_calculated-1;    // set start equal to last array index
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

Ahora empezaremos con la posición (begin+1), y el código modificado tiene el siguiente aspecto (los cambios en el código se destacan en negrita):

//--- calculate values for mtm and |mtm|
   int start;
   if(prev_calculated==0) start=begin+1;  // start filling MTMBuffer[] and AbsMTMBuffer[] arrays from begin+1 index 
   else start=prev_calculated-1;           // set start equal to the last array index
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]); 
     }

Puesto que los valores de price[0] a price[begin-1] no se pueden usar para hacer cálculos, empezaremos a partir de price[begin]. Los primeros valores calculados para los arrays MTMBuffer[] y AbsMTMBuffer[] siempre serán así:

      MTMBuffer[begin+1]=price[begin+1]-price[begin];
      AbsMTMBuffer[begin+1]=fabs(MTMBuffer[begin+1]);

Por esta razón, la variable start en el ciclo for ahora tiene un valor inicial de start=begin+1, en lugar de 1.

Cuenta para begin para Arrays Dependientes

Ahora viene el suavizado exponencial de los arrays MTMBuffer[] y AbsMTMBuffer[]. La regla es también sencilla: si la posición inicial del array base ha aumentado por barras begin, entonces la posición inicial de todos los arrays dependientes también debería aumentar por barras begin.

//--- calculating the first moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,               // index of the starting element in array 
                         r,               // period of exponential average
                         MTMBuffer,       // source buffer for average
                         EMA_MTMBuffer);  // target buffer
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

Aquí, MTMBuffer [] y AbsMTMBuffer [] son arrays base, y los valores calculados en estos arrays ahora empiezan desde el mayor índice por begin. De modo que simplemente añadiremos este offset a la función ExponentialMAOnBuffer().

Ahora, este bloque tiene este aspecto:

//--- calculating the first moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+1,        // index of the starting element in array 
                         r,               // period for exponential average
                         MTMBuffer,       // source buffer for average
                         EMA_MTMBuffer);  // target buffer
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

Como puede ver, la modificación entera se ha hecho para acomodar el aumento de los datos de la posición inicial, definidos por el parámetro begin. Nada complicado. Del mismo modo, cambiaremos el segundo bloque de suavizado.

Antes:

//--- calculating the second moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

Después:

//--- calculating the second moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

Del mismo modo, cambiaremos el último bloque de cálculo.

Antes:

//--- calculating values of our indicator
   if(prev_calculated==0) start=r+s-1; // set initial index for input arrays
   else start=prev_calculated-1;       // set start equal to last array index
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }

Después:

//--- calculating values of our indicator
   if(prev_calculated==0) start=begin+r+s-1; // set initial index for input arrays
   else start=prev_calculated-1;              // set start equal to last array index
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i]; 
     }

Así finalizamos la puesta a punto del indicador, y ahora se saltará los primeros valores vacíos begin del array de entrada price[] en la función OnCalculate() y tomará en cuenta el offset causado por esta omisión. Pero debemos recordar que otros indicadores pueden usar valores TSI para hacer cálculos. Por este motivo, configuraremos los valores vacíos de nuestro indicador como EMPTY_VALUE.

¿Es Necesaria la Inicialización de Buffers de Indicador?

En MQL5, los arrays no se inicializan por defecto con valores definidos. Lo mismo se puede decir de los arrays especificados en la función SetIndexBuffer() para buffers de indicador. Si un array es un buffer del indicador, su tamaño dependerá del valor del parámetro en la función rates_total OnCalculate();

Puede que se sienta tentado a inicializar todos los buffers de indicador a la vez con valores EMPTY_VALUE usando la función ArrayInitialize(), por ejemplo, al comienzo de OnCalculate():

//--- if it is the first call of OnCalculate() 
   if(prev_calculated==0)
     {
      ArrayInitialize(TSIBuffer,EMPTY_VALUE); 
     }

Pero no es recomendable por la siguiente razón: mientras el terminal de cliente está en funcionamiento, se reciben nuevas cotizaciones para el símbolo, cuyos datos se usan para calcular el indicador. Tras un tiempo, el número de barras aumentará, de modo que el terminal de cliente reservará memoria adicional para los buffers de indicador.

Pero los valores de los nuevos elementos del array ("adjuntos") pueden tener cualquier valor, puesto que durante la relocalización de memoria para cualquier array, no se lleva a cabo la inicialización. La inicialización inicial le puede dar una certeza falsa de que todos los elementos del array que no se han definido expresamente se rellenarán con valores que se han especificado durante la inicialización. Por supuesto, esto no es cierto, y usted nunca debe pensar que el valor numérico de una variable o un elemento del array se inicializará con valores necesarios para nosotros.

Debe configurar el valor de cada elemento del buffer del indicador. Si los valores de algunas barras no están definidas por el algoritmo del indicador, debería configurarlas usted mismo explícitamente con valores vacíos. Por ejemplo, si algún valor del buffer del indicador se calcula por la operación de división, en algunos casos el divisor puede ser cero.

Sabemos que una división entre cero es un error crítico de ejecución en MQL5, y lleva a la inmediata terminación de un programa mql5. En lugar de evitar la división entre cero gestionando este caso especial en código, es necesario configurar el valor de este elemento del buffer. Quizás sea mejor usar valores que hayamos asignado como vacíos para este estilo de dibujo.

Por ejemplo, para algún estilo de dibujo hemos definido cero como un valor vacío, usando la función PlotIndexSetDouble():

   PlotIndexSetDouble(plotting_style_index,PLOT_EMPTY_VALUE,0);   

Entonces, para todos los valores vacíos del buffer del indicador en este dibujo, es necesario definir el valor de cero explícitamente:

   if(divider==0)
      IndicatorBuffer[i]=0;
   else
      IndicatorBuffer[i]=... 

Además, si DRAW_BEGIN se ha especificado para alguna extracción, todos los elementos del buffer del indicador con índices de 0 a DRAW_BEGIN se rellenarán con ceros automáticamente.

Conclusión

Hagamos un breve resumen. Hay algunas condiciones necesarias para que un indicador se calcule correctamente basándose en los datos de otro indicador (y que sea apto para el uso en otros programas mql5):

  1. Los valores vacíos en indicadores incorporados se rellenan con los valores de la constante EMPTY_VALUE, que es exactamente igual al máximo valor para el tipo doble (DBL_MAX).
  2. Para obtener más detalles sobre el inicio del índice de valores importantes para un indicador, debería analizar el parámetro de entrada begin de la forma corta de OnCalculate().
  3. Para prohibir el dibujo de los primeros valores N para el estilo de dibujo especificado, configure el parámetro DRAW_BEGIN usando el siguiente código:
    PlotIndexSetInteger(plotting_style_index,PLOT_DRAW_BEGIN,N);
  4. Si DRAW_BEGIN está especificado para un dibujo, todos los elementos del buffer del indicador con índices de 0 a DRAW_BEGIN se llenarán automáticamente con valores vacíos (por defecto, EMPTY_VALUE).
  5. En la función OnCalculate(), añada un offset adicional por barras begin para el uso correcto de otros datos del indicador en su propio indicador.
    //--- if it's the first call 
       if(prev_calculated==0)
         { 
          //--- increase position of data beginning by begin bars, 
          //--- because of other indicator's data use      
          if(begin>0)PlotIndexSetInteger(plotting_style_index,PLOT_DRAW_BEGIN,begin+N);
         }
  6. Puede especificar su propio valor vacío diferente de EMPTY_VALUE en la función OnInit() usando el siguiente código:
    PlotIndexSetDouble(plotting_style_index,PLOT_EMPTY_VALUE,your_empty_value);
  7. No dependa en una sola inicialización de los buffers del indicador usando el siguiente código:
    ArrayInitialize(buffer_number,value);
        
    Debe configurar todos los valores del buffer del indicador para la función OnCalculate() explícita y consistentemente, incluyendo los valores vacíos.

Por supuesto, en el futuro tendrá más experiencia para escribir indicadores, y se encontrará con casos que van más allá de lo tratado en este artículo, pero espero que, llegado ese momento, su conocimiento de MQL5 le permita gestionarlos sin problemas.