MQL5: Crea tu propio indicador

MetaQuotes | 18 diciembre, 2013

Introducción

¿Qué es un indicador? Se trata de un conjunto de valores calculados y que queremos que se muestren en la pantalla de manera cómoda para nosotros. Los conjuntos de valores se representan en los programas en forma de matrices. De este modo, la creación del indicador consiste en escribir un algoritmo que maneja algunas matrices (matrices de precios) y graba los resultados del procesamiento de otras matrices (valores del indicador).

A pesar de la existencia de un gran número de indicadores ya listos, y que ya son clásicos, siempre existirá la necesidad de crear sus propios indicadores. Dichos indicadores que creamos usando nuestros propios algoritmos se llaman indicadores personalizados. En este artículo comentaremos cómo crear un indicador personalizado sencillo.

Los indicadores son diferentes

Se pueden representar los indicadores por líneas o áreas coloreadas, o se pueden mostrar como símbolos especiales que señalan los momentos favorables para entrar posiciones. Además, se pueden combinar, lo que proporciona aún más tipos de indicadores. Vamos a considerar la creación de un indicador en el ejemplo del muy conocido True Strength Index (Índice de Fuerza Verdadera) desarrollado por William Blau.

True Strength Index (Índice de Fuerza Verdadera)

El indicador TSI (por sus siglas en inglés) se basa en el impulso (momentum) de doble suavizado para identificar las tendencias, así como las áreas de sobre-venta/sobre-compra. Se puede encontrar su justificación matemática en Momentum, Dirección, y Divergencia por William Blau. Incluimos aquí, su fórmula de cálculo únicamente.

TSI(CLOSE,r,s) =100*EMA(EMA(mtm,r),s) / EMA(EMA(|mtm|,r),s)

dónde:

A partir de esta formula, se pueden identificar los tres parámetros que influyen en el cálculo del indicador. Se trata de los períodos r y s, y el tipo de precios utilizado para los cálculos. En este caso, utilizamos el precio CLOSE.

MQL5 Wizard (asistente)

Vamos a mostrar el TSI mediante una línea azul -ahora tenemos que ejecutar MQL5 Wizard. En la primera etapa, debemos especificar el tipo de programa que queremos crear; Indicador personalizado. En la segunda etapa, debemos fijar el nombre del programa, los parámetros r y s, además de sus valores.

MQL5 Wizard: Configuración del nombre del indicador y de los parámetros

En el siguiente paso, señalamos que el indicador se mostrará en una ventana separada mediante una línea azul, y especificamos la etiqueta TSI para esta línea.

MQL5 Wizard: configuración del tipo de indicador

Se han introducido todos los datos iniciales, por lo que hacemos clic en "Finalizar" y obtenemos un borrador de nuestro indicador. 

//+------------------------------------------------------------------+
//|                                          True Strength Index.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
//---- plot TSI
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      r=25;
input int      s=13;
//--- indicator buffers
double         TSIBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| 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[])
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+ 

MQL5 Wizard crea la cabecera del indicador, en la que se escriben las propiedades del indicador, a saber:

Ya están listos todos los preparativos, ahora podemos mejorar y perfeccionar nuestro código.

OnCalculate()

La función OnCalculate() es el controlador del evento Calculate, que aparece cuando hay que volver a calcular los valores de los indicadores y volver a dibujar en el gráfico. Este es el evento que corresponde a la recepción de un nuevo tick, actualización del historial de un símbolo, etc. Es por ello que el código principal para todos los cálculos de los valores de los indicadores debe estar ubicado exactamente en esta función.

Por supuesto, se pueden implementar cálculos adicionales en otras funciones, pero hay que utilizarlas en el controlador OnCalculate.

Por defecto, MQL5 Wizard crea la segunda forma de OnCalculate(), que proporciona un acceso a todo tipo de series de tiempo:

Pero en este caso, tenemos que calcular una única matriz de datos, por lo que vamos a cambiar OnCalculate() a la primera forma de llamada.

int OnCalculate (const int rates_total,      // size of the price[] array
                 const int prev_calculated,  // number of available bars at the previous call
                 const int begin,            // from what index in price[] authentic data start
                 const double& price[])      // array, on which the indicator will be calculated
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }  

Esto nos permitirá aplicar el indicador no sólo a los datos de los precios, sino también para crear un indicador basado en los valores de otros indicadores.

Especificación del tipo de datos para el cálculo del indicador personalizado

Si seleccionamos Close en la pestaña "Parámetros" (se activa por defecto), entonces la matriz price[] que se envía a OnCalculate() va a contener los precios de cierre. Si seleccionamos, por ejemplo, Typical Price, price[] va a contener los precios de (High+Low+Close)/3 para cada período.

El parámetro rates_total indica el tamaño de la matriz price[] ; será útil para organizar los cálculos en un bucle. La indexación de los elementos en price[] empieza desde cero y desde el pasado hacia el futuro. Es decir, el elemento price[0] contiene el valor más antiguo, mientras price[rates_total-1] contiene el elemento más actualizado de la matriz.

Organización de los buffers de indicadores auxiliares

Se mostrará sólo una línea por gráfico, es decir, los datos de una matriz de indicador. Pero primero tenemos que organizar los cálculos intermedios. Los datos intermedios se almacenan en matrices de indicadores marcadas por el atributo INDICATOR_CALCULATIONS. A partir de la fórmula observamos que necesitamos matrices adicionales:

  1. para valores de mtm -la matriz MTMBuffer[];
  2. para valores de |mtm| -la matriz AbsMTMBuffer[];
  3. para EMA(mtm,r) -la matriz EMA_MTMBuffer[];
  4. para EMA(EMA(mtm,r),s) -la matriz EMA2_MTMBuffer[];
  5. para EMA(|mtm|,r) -la matriz EMA_AbsMTMBuffer[];
  6. para EMA(EMA(|mtm|,r),s) -la matriz EMA2_AbsMTMBuffer[].

En total, necesitamos añadir 6 matrices de tipo doble a global level y unir estas matrices con los buffers de indicadores en la función OnInit(). No te olvides de especificar el nuevo número de buffers de indicadores; la propiedad indicator_buffers debe ser igual a 7 (había uno, y se añadieron 6).

#property indicator_buffers 7

Se muestra a continuación como queda el código del indicador:


Cálculos intermedios

Es muy fácil calcular los valores para los buffers MTMBuffer[] y AbsMTMBuffer[]. En el bucle, se recorren uno a uno los valores, desde price[1] hasta price[rates_total-1] y se escribe la diferencia en una matriz, y el valor absoluto de la diferencia en otra.

//--- calculate values of mtm and |mtm|
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     } 

La siguiente etapa consiste en el cálculo del promedio exponencial de estas matrices. Se puede hacer de dos maneras. En la primera escribimos el algoritmo entero, intentando no cometer errores. En la segunda utilizamos funciones listas que ya están depuradas y diseñadas justamente para estos propósitos.

En MQL5 no hay funciones integradas para el cálculo de promedios móviles mediante los valores de las matrices, pero hay una librería de funciones ya hechas MovingAverages.mqh, su ruta de acceso es terminal_directory/MQL5/Include/MovingAverages.mqh, donde el terminal_directory es el directorio donde está instalado el terminal de MetaTrader 5. La librería es un archivo de inclusión; contiene funciones para el cálculo de promedios móviles en las matrices, mediante cualquiera de los cuatro métodos clásicos:

Para poder utilizar estas funciones, hay que añadir en cualquier programa MQL5 la siguiente línea en el código del encabezado:

#include <MovingAverages.mqh>

Necesitamos la función ExponentialMAOnBuffer() que calcula el promedio exponencial móvil en la matriz de valores y el promedio registrado en otra matriz.

La función de suavizado (smoothing) de una matriz

En total, el archivo incluido MovingAverages.mqh contiene ocho funciones que se pueden dividir en dos grupos de 4 funciones del mismo tipo. El primer grupo contiene funciones que reciben una matriz y simplemente devuelven el valor del promedio móvil en una posición determinada:

Estas funciones están diseñadas para obtener el valor de un promedio sólo una vez para una matriz, y no están optimizadas para múltiples llamadas. Si necesitamos utilizar alguna función de este grupo en un bucle (para calcular los valores de un promedio y además escribir cada valor calculado en una matriz), tendremos que realizar un algoritmo óptimo.

El segundo grupo de funciones está diseñado para completar la matriz receptora con los valores de un promedio móvil, basado en la matriz de los valores iniciales:

Todas las funciones especificadas, excepto para las matrices buffer[], price[] y el tiempo promediado period, obtienen 3 parámetros más, que tienen un propósito análogo a los parámetros de la función OnCalculate(); rates_total, prev_calculated y begin. Las funciones de este grupo procesan correctamente las matrices enviadas price[] y buffer[], teniendo en cuenta la dirección de indexación (marcador AS_SERIES).

El parámetro begin indica el índice de la matriz de origen, y a partir del cual empiezan los datos relevantes, es decir, los datos que necesitan ser procesados. Para la matriz MTMBuffer[], los datos reales empiezan con el índice 1, ya que MTMBuffer[1]=price[1]-price[0]. El valor de MTMBuffer[0] no está definido, por eso begin=1.

//--- calculate the first moving
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // index, starting from which data for smoothing are available 
                         r,  // period of the exponential average
                         MTMBuffer,       // buffer to calculate average
                         EMA_MTMBuffer);  // into this buffer locate value of the average
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

Al hacer el promedio, hay que tener en cuenta el valor del período, ya que los valores calculados de la matriz de salida se rellenan con un pequeño retraso, que es más grande si los períodos promediados son más grandes. Por ejemplo, si period=10, los valores en la matriz resultante empiezan con begin+period-1=begin+10-1. En las demás llamadas de buffer[], hay que tener esto en cuenta, y el procesamiento debe comenzar con el índice begin+period-1.

Así, podemos obtener fácilmente el segundo promedio exponencial a partir de las matrices MTMBuffer[] y AbsMTMBuffer:

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

Ahora, el valor begin es igual a r, ya que begin=1+r-1 (r es el período del primer promedio exponencial, el procesamiento comienza con el índice 1). En las matrices de salida de EMA2_MTMBuffer[] y EMA2_AbsMTMBuffer[], la valores calculados comienzan con el índice r+s-1, puesto que comenzamos a identificar las matrices de entrada con el índice r, y el período del segundo promedio exponencial es igual a s.

Todos los cálculos preliminares están listos, podemos calcular ahora los valores del buffer del indicador TSIBuffer[], que se mostrará en el gráfico.

//--- now calculate values of the indicator
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }

Compilamos el código pulsando la tecla F5 y lo ejecutamos en el terminal MetaTrader 5. ¡Funciona!


La primera versión del Índice de Fuerza Verdadera (True Strength Index)

Todavía quedan cosas por resolver.

Optimización de los cálculos

Realmente, no basta con solo escribir un indicador que funcione. Si nos fijamos bien en la implementación actual de OnCalculate(), veremos que no está optimizada.

int OnCalculate (const int rates_total,    // size of the price[] array;
                 const int prev_calculated,// number of available bars;
                 // at the previous call;
                 const int begin,// from what index of the 
                 // price[] array true data start;
                 const double &price[]) // array, at which the indicator will be calculated;
  {
//--- calculate values of mtm and |mtm|
   MTMBuffer[0]=0.0;
   AbsMTMBuffer[0]=0.0;
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }
//--- calculate the first moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // index, starting from which data for smoothing are available 
                         r,  // period of the exponential average
                         MTMBuffer,       // buffer to calculate average
                         EMA_MTMBuffer);  // into this buffer locate value of the average
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

//--- calculate 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);
//--- now calculate values of the indicator
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }

En cada inicio de función se calculan los valores en las matrices MTMBuffer[] y AbsMTMBuffer[]. En este caso, si el tamaño de price[] es igual a cientos de miles o incluso millones, los cálculos repetidos e innecesarios pueden usar todos los recursos de la CPU, sin importar lo potente que es. 

Para organizar los cálculos de manera óptima, utilizamos el parámetro de entrada prev_calculated, que es igual al valor devuelto por OnCalculate() en la llamada anterior. En la primera llamada de la función, el valor de prev_calculated es siempre igual a 0. En este caso, calculamos todos los valores en el buffer del indicador. Durante la siguiente llamada, no tenemos que calcular todo el buffer; solo se calculará el último valor. Lo escribiremos así:

//--- if it is the first call 
   if(prev_calculated==0)
     {
      //--- set zero values to zero indexes
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }
//--- calculate values of mtm and |mtm|
   int start;
   if(prev_calculated==0) start=1;  // start filling out MTMBuffer[] and AbsMTMBuffer[] from the 1st index 
   else start=prev_calculated-1;    // set start equal to the last index in the arrays 
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

Los bloques de cálculo de EMA_MTMBuffer[], EMA_AbsMTMBuffer[], EMA2_MTMBuffer[] y EMA2_AbsMTMBuffer[] no requieren una optimización de los cálculos, puesto que ExponentialMAOnBuffer() ya está escrita de manera óptima. Necesitamos optimizar únicamente el cálculo de los valores de las matrices TSIBuffer[]. Usamos el mismo método aplicado con MTMBuffer[].

//--- now calculate the indicator values
   if(prev_calculated==0) start=r+s-1; // set the starting index for input arrays
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- return value of prev_calculated for next call
   return(rates_total);

La última observación sobre el procedimiento de optimización: OnCalculate() devuelve el valor de rates_total. Este representa el número de elementos en la matriz de entrada price[], que se utiliza para los cálculos del indicador.

El valor devuelto por OnCalculate() se almacena en la memoria del terminal, y en la siguiente llamada de OnCalculate() se transfiere a la función como el valor del parámetro de entrada prev_calculated.

Esto permite saber siempre el tamaño de la matriz de entrada de la llamada anterior de OnCalculate() y empezar el cálculo de los buffers del indicador a partir de un índice correcto, sin hacer nuevos cálculos inútiles

Comprobación de los datos de entrada

Debemos hacer una cosa más, par que OnCalculate() funcione perfectamente. Vamos a añadir una comprobación de la matriz price[], en la cual se calculan los valores del indicador. Si el tamaño de la matriz (rates_total) es muy pequeño, no hacen falta cálculos; tenemos que esperar hasta la siguiente llamada de OnCalculate(), cuando los datos sean suficientes.

//--- if the size of price[] is too small
  if(rates_total<r+s) return(0); // do not calculate or draw anything
//--- if it's the first call 
   if(prev_calculated==0)
     {
      //--- set zero values for zero indexes
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }

Ya que el suavizado exponencial se usa de forma secuencial y dos veces para calcular el Índice de Fuerza Verdadera, el tamaño de price[] debe ser por lo menos igual o superior a la suma de los períodos r y s; de lo contrario se finaliza la ejecución, y OnCalculate() devuelve 0. Si se devuelve el valor cero, significa que el indicador no se representará en el gráfico, ya que sus valores no se han calculado.

Los ajustes de la representación

Desde el punto de vista de los cálculos, el indicador ya está listo para su uso. Pero si le llamamos desde otro programa MQL5, se basará por defecto en los precios Close. Podemos especificar otro tipo de precio por defecto; seleccionamos un valor a partir de la enumeración ENUM_APPLIED_PRICE en la propiedades indicator_applied_price del indicador. 

Por ejemplo, para establecer el precio típico ( (high+low+close)/3) como precio por defecto, vamos a escribir lo siguiente:

#property indicator_applied_price PRICE_TYPICAL


Si tenemos previsto utilizar únicamente sus valores mediante las funciones iCustom() o IndicatorCreate(), no se requieren nuevas mejoras. Pero para su uso directo, es decir, su representación en el gráfico, es conveniente realizar unos ajustes adicionales:

Se pueden configurar estos ajustes en el controlador OnInit(), mediante funciones del grupo Indicadores personalizados. Añadimos nuevas líneas y guardamos el indicador como True_Strength_Index_ver2.mq5.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//--- bar, starting from which the indicator is drawn
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,r+s-1);
   string shortname;
   StringConcatenate(shortname,"TSI(",r,",",s,")");
//--- set a label do display in DataWindow
   PlotIndexSetString(0,PLOT_LABEL,shortname);   
//--- set a name to show in a separate sub-window or a pop-up help
   IndicatorSetString(INDICATOR_SHORTNAME,shortname);
//--- set accuracy of displaying the indicator values
   IndicatorSetInteger(INDICATOR_DIGITS,2);
//---
   return(0);
  }

Si ejecutamos ambas versiones del indicador y desplazamos el gráfico al principio, veremos todas las diferencias.


La segunda versión de los indicadores del Índice de Fuerza Verdadera se ve mejor

Conclusión

Mediante el ejemplo de creación del indicador del Índice de Fuerza Verdadera (TSI), podemos identificar los puntos principales del proceso de escritura de cualquier indicador MQL5: