Obtención de datos de series temporales a partir de un indicador: CopyBuffer

Un programa MQL puede leer datos de los búferes públicos del indicador por su manejador. Recordemos que en los indicadores personalizados, dichos búferes son arrays especificados en el código fuente en llamadas a la función SetIndexBuffer.

La API de MQL5 proporciona la función CopyBuffer para la lectura de búferes; la función tiene 3 formas.

int CopyBuffer(int handle, int buffer, int offset, int count, double &array[])

int CopyBuffer(int handle, int buffer, datetime start, int count, double &array[])

int CopyBuffer(int handle, int buffer, datetime start, datetime stop, double &array[])

El parámetro handle especifica el manejador recibido de la llamada iCustom u otras funciones (para obtener más detalles, consulte las secciones sobre IndicatorCreate e indicadores integrados). El parámetro buffer establece el índice del búfer del indicador del que se van a solicitar los datos. La numeración se realiza empezando por 0.

Los elementos recibidos de la serie temporal solicitada se introducen en el conjunto array por referencia.

Las tres variantes de la función difieren en la forma de especificar el rango de marcas de tiempo (start/stop) o números (offset) y la cantidad (count) de barras para los que se obtienen los datos. Los fundamentos del trabajo con estos parámetros coinciden plenamente con lo que estudiamos en Visión general de las funciones Copy para obtener arrays de cotizaciones. En particular, los elementos de datos copiados en offset y count se cuentan del presente al pasado, es decir, la posición inicial igual a 0 significa la barra actual. Los elementos del array receptor se ordenan físicamente del pasado al presente (sin embargo, este direccionamiento puede invertirse a nivel lógico utilizando ArraySetAsSeries).

CopyBuffer es un análogo de funciones para leer series temporales integradas del tipo Copy Open, CopyClose, entre otros. La principal diferencia es que las series temporales con cotizaciones son generadas por el propio terminal, mientras que las series temporales de los búferes de los indicadores son calculadas por indicadores personalizados o integrados. Además, en el caso de los indicadores, establecemos un par específico de símbolo y marco temporal que definen e identifican una serie temporal de antemano, en la función de creación de manejadores como iCustom, y en CopyBuffer esta información se transmite indirectamente a través de handle.

Cuando se copia una cantidad desconocida de datos como array de destino, es conveniente utilizar un array dinámico. En este caso, la función CopyBuffer distribuirá el tamaño del array receptor en función del tamaño de los datos copiados. Si es necesario copiar repetidamente una cantidad conocida de datos, es mejor hacerlo en un búfer asignado estáticamente (local con el modificador de static o de tamaño fijo en el contexto global) para evitar repetidas asignaciones de memoria.

Si el array receptor es un búfer de indicadores (un array previamente registrado en el sistema por la función SetIndexBufer), entonces la indexación en la serie temporal y en el búfer receptor son las mismas (sujeto a una petición para el mismo par símbolo/marco temporal). En este caso, es fácil implementar el rellenado parcial del receptor (en concreto, se utiliza para actualizar las últimas barras, véase un ejemplo más abajo). Si el símbolo o el marco temporal de la serie temporal solicitada no coincide con el símbolo y/o el marco temporal del gráfico actual, la función no devolverá más elementos que el número mínimo de barras en estos dos: origen y destino.

Si un array ordinario (no un búfer) se pasa como el argumento array, la función lo rellenará empezando por los primeros elementos, por entero (en el caso de dinámico) o parcialmente (en el caso de estático, con exceso de tamaño). Por lo tanto, si es necesario copiar parcialmente los valores de indicadores en una ubicación arbitraria en otro array, entonces para estos propósitos es necesario utilizar un array intermedio, en el cual se copia el número requerido de elementos, y desde allí se transfieren al destino final.

La función devuelve el número de elementos copiados o -1 en caso de error, incluida la ausencia temporal de datos preparados.

Dado que, por regla general, los indicadores dependen directa o indirectamente de las series temporales de precios, su cálculo no comienza antes de que se sincronicen las cotizaciones. A este respecto, hay que tener en cuenta los aspectos técnicos de la organización y el almacenamiento de series temporales en el terminal y prepararse para que los datos solicitados no aparezcan inmediatamente. En concreto, podemos recibir 0 o una cantidad inferior a la solicitada. Todos estos casos deben tratarse en función de las circunstancias, como esperar a una compilación o informar de un problema al usuario.

Si las series temporales solicitadas aún no se han construido, o necesitan descargarse del servidor, entonces la función se comporta de forma diferente dependiendo del tipo de programa MQL desde el que se llame.
 
Cuando se solicitan datos que aún no están listos del indicador, la función devolverá inmediatamente -1, pero se iniciará el proceso de carga y construcción de series temporales.
 
Al solicitar datos a un Asesor Experto o a un script, se iniciará la descarga desde el servidor y/o la construcción de las series temporales requeridas si los datos pueden construirse a partir del historial local. La función devolverá la cantidad de datos que estarán listos en el tiempo de espera (45 segundos) asignado para la ejecución sincrónica de la función (el código de llamada está esperando a que la función finalice).

Tenga en cuenta que la función CopyBuffer puede leer datos de los búferes independientemente de su modo de funcionamiento, INDICATOR_DATA, INDICATOR_COLOR_INDEX, INDICATOR_CALCULATIONS, mientras que los dos últimos están ocultos para el usuario.

También es importante tener en cuenta que el desplazamiento de la serie temporal puede establecerse en el indicador llamado mediante la propiedad PLOT_SHIFT y afecta al desplazamiento de los datos leídos con CopyBuffer. Por ejemplo, si las líneas del indicador se desplazan hacia el futuro en N barras, en los parámetros CopyBuffer (primera forma) hay que dar offset igual a (- N), es decir, con un menos, ya que la barra de la serie temporal actual tiene un índice de 0, y los índices de las barras futuras con desplazamiento disminuyen en uno en cada barra. En concreto, tal situación surge con el indicador Gator, ya que su gráfico nulo está desplazado hacia delante por el valor del parámetro TeethShift, y el primer diagrama está desplazado por el valor del parámetro LipsShift. La corrección debe hacerse en función de la más alta de ellas. Veremos un ejemplo en la sección Leer datos de gráficos que tienen un desplazamiento.

MQL5 no proporciona herramientas programáticas para encontrar la propiedad PLOT_SHIFT de un indicador de terceros. Por lo tanto, si es necesario, tendrá que solicitar esta información al usuario a través de una variable de entrada.

Trabajaremos con CopyBuffer desde el código del Asesor Experto en el capítulo sobre Asesores Expertos, pero por ahora nos limitaremos a los indicadores.

Vamos a continuar desarrollando un ejemplo con un indicador auxiliar IndWPR. Esta vez, en la versión UseWPR3.mq5 proporcionaremos un búfer de indicador y lo rellenaremos con datos de IndWPR utilizando CopyBuffer. Para ello, aplicaremos las directivas con el número de búferes y los ajustes de renderización.

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
   
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrBlue
#property indicator_width1  1
#property indicator_label1  "WPR"

En el contexto global, describimos el parámetro de entrada con el periodo WPR, un array para el búfer y una variable con un descriptor.

input int WPRPeriod = 14;
   
double WPRBuffer[];
   
int handle;

El manejador OnInit prácticamente no cambia: sólo se ha añadido la llamada SetIndexBuffer.

int OnInit()
{
   SetIndexBuffer(0WPRBuffer);
   handle = iCustom(_Symbol_Period"IndWPR"WPRPeriod);
   return handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

En OnCalculate copiaremos los datos sin transformaciones.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   // waiting for the calculation to be ready for all bars
   if(BarsCalculated(Handle) != rates_total)
   {
      return prev_calculated;
   }
   
   // copy the entire timeseries of the subordinate indicator or on new bars to our buffer
   const int n = CopyBuffer(handle00rates_total - prev_calculated + 1WPRBuffer);
   // if there are no errors, our data is ready for all bars rates_total
   return n > -1 ? rates_total : 0;
}

Al compilar y ejecutar UseWPR3, obtendremos de hecho una copia del WPR original, con la excepción del ajuste de los niveles, la exactitud de los números y el título. Esto es suficiente para probar el mecanismo, pero normalmente los nuevos indicadores basados en uno o más indicadores auxiliares ofrecen alguna idea y transformación de datos propia. Por lo tanto, desarrollaremos otro indicador que genere señales de compra y venta (desde la posición del trading, no deben considerarse como un modelo, ya que se trata únicamente de una tarea de programación). La idea del indicador se muestra en la siguiente imagen:

Indicadores IndWPR, IndTripleEMA, IndFractals

Indicadores IndWPR, IndTripleEMA, IndFractals

Utilizamos la salida del WPR de las zonas de sobrecompra y sobreventa como recomendación, respectivamente, para vender y comprar. Para que las señales no reaccionen a fluctuaciones aleatorias, aplicaremos una media móvil triple a WPR y comprobaremos si su valor cruza los límites de las zonas superior e inferior.

Como filtro para estas señales, comprobaremos qué fractal fue el último antes de este momento: un fractal superior significa un retroceso del precio a la baja y confirma una venta, y un fractal inferior significa un retroceso al alza y, por tanto, apoya una compra. Los fractales aparecen con un desfase de un número de barras igual al orden de los fractales.

El nuevo indicador está disponible en el archivo UseWPRFractals.mq5.

Necesitamos tres búferes: dos de señal y uno más para el filtro. Podríamos emitir este último en el modo INDICATOR_CALCULATIONS. En su lugar, hagámoslo el INDICATOR_DATA estándar, pero con el estilo DRAW_NONE; de esta forma no interferirá en el gráfico, pero sus valores serán visibles en la ventana de datos.

Las señales se mostrarán en el gráfico principal (en los precios Close por defecto), por lo que utilizaremos la directiva indicator_chart_window. Todavía podemos llamar a los indicadores del tipo WPR que se dibujan en una ventana separada, ya que todos los indicadores subordinados se pueden calcular sin visualización. Si es necesario, podemos trazarlos, pero hablaremos de ello en el capítulo sobre gráficos (véase ChartIndicatorAdd).

#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots   3
// buffer drawing settings
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrRed
#property indicator_width1  1
#property indicator_label1  "Sell"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrBlue
#property indicator_width2  1
#property indicator_label2  "Buy"
#property indicator_type3   DRAW_NONE
#property indicator_color3  clrGreen
#property indicator_width3  1
#property indicator_label3  "Filter"

En las variables de entrada proporcionaremos la posibilidad de especificar el periodo WPR, el periodo de promediación (suavizado) y el orden fractal. Estos son los parámetros de los indicadores subordinados. Además, introducimos la variable offset con el número de la barra en la que se analizarán las señales. El valor 0 (por defecto) significa la barra actual y el análisis en modo tick (nota: las señales de la última barra pueden ser redibujadas; a algunos operadores de trading eso no les gusta). Si hacemos offset igual a 1, analizaremos las barras ya formadas, y tales señales no cambian.

input int PeriodWPR = 11;
input int PeriodEMA = 5;
input int FractalOrder = 1;
input int Offset = 0;
input double Threshold = 0.2;

La variable Threshold define el tamaño de las zonas de sobrecompra y sobreventa como una fracción de ±1.0 (en cada dirección). Por ejemplo, si sigue la configuración clásica de WPR con niveles -20 y -80 en una escala de 0 a -100, entonces Threshold debería ser igual a 0.4.

Se proporcionan los siguientes arrays para los búferes de indicadores.

double UpBuffer[];   // upper signal means overbought, i.e. selling
double DownBuffer[]; // lower signal means oversold, i.e. buy
double filter[];     // fractal filter direction +1 (up/buy), -1 (down/sell)

Los manejadores de indicadores se guardarán en variables globales.

int handleWPRhandleEMA3handleFractals;

Realizaremos todos los ajustes, como de costumbre, en OnInit. Dado que la función CopyBuffer utiliza la indexación del presente al pasado, para la uniformidad de la lectura de datos fijamos la bandera «serie» (ArraySetAsSeries) para todos los arrays.

int OnInit()
{
   // binding buffers
   SetIndexBuffer(0UpBuffer);
   SetIndexBuffer(1DownBuffer);
   SetIndexBuffer(2FilterINDICATOR_DATA); // version: INDICATOR_CALCULATIONS
   ArraySetAsSeries(UpBuffertrue);
   ArraySetAsSeries(DownBuffertrue);
   ArraySetAsSeries(Filtertrue);
   
   // arrow signals
   PlotIndexSetInteger(0PLOT_ARROW234);
   PlotIndexSetInteger(1PLOT_ARROW233);
   
   // subordinate indicators
   handleWPR = iCustom(_Symbol_Period"IndWPR"PeriodWPR);
   handleEMA3 = iCustom(_Symbol_Period"IndTripleEMA"PeriodEMA0handleWPR);
   handleFractals = iCustom(_Symbol_Period"IndFractals"FractalOrder);
   if(handleWPR == INVALID_HANDLE
   || handleEMA3 == INVALID_HANDLE
   || handleFractals == INVALID_HANDLE)
   {
      return INIT_FAILED;
   }
   
   return INIT_SUCCEEDED;
}

En las llamadas de iCustom debe prestarse atención a cómo se crea handleEMA3. Dado que esta media debe calcularse a partir del WPR, pasamos handleWPR (obtenido en la llamada anterior a iCustom) como último parámetro, después de los parámetros reales del indicador IndTripleEMA. Al hacerlo, debemos especificar la lista completa de parámetros de entrada de IndTripleEMA (los parámetros que contiene son int InpPeriodEMA y BEGIN_POLICY InpHandleBegin; utilizamos el segundo parámetro para estudiar la omisión de las barras iniciales y no lo necesitamos ahora, pero debemos pasarlo, así que simplemente lo ponemos a 0). Si omitiéramos el segundo parámetro en la llamada por considerarlo irrelevante en el contexto actual de la aplicación, entonces el manejador handleWPR pasado se interpretaría en el indicador llamado como InpHandleBegin. En consecuencia, IndTripleEMA se aplicaría al precio Close normal.

Cuando no necesitamos pasar un manejador extra, la sintaxis de la llamada iCustom le permite omitir un número arbitrario de últimos parámetros, mientras que éstos recibirán los valores por defecto del código fuente.

En el manejador OnCalculate esperamos a que los indicadores WPR y los fractales estén preparados, y luego calculamos las señales para todo el historial o la última barra utilizando la función auxiliar MarkSignals.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   if(BarsCalculated(handleEMA3) != rates_total
   || BarsCalculated(handleFractals) != rates_total)
   {
      return prev_calculated;
   }
   
   ArraySetAsSeries(datatrue);
   
   if(prev_calculated == 0// first launch
   {
      ArrayInitialize(UpBufferEMPTY_VALUE);
      ArrayInitialize(DownBufferEMPTY_VALUE);
      ArrayInitialize(Filter0);
      
      // look for signals throughout history
      for(int i = rates_total - FractalOrder - 1i >= 0; --i)
      {
         MarkSignals(iOffsetdata);
      }
   }
   else // online
   {
      for(int i = 0i < rates_total - prev_calculated; ++i)
      {
         UpBuffer[i] = EMPTY_VALUE;
         DownBuffer[i] = EMPTY_VALUE;
         Filter[i] = 0;
      }
      
      // looking for signals on a new bar or each tick (if Offset == 0)
      if(rates_total != prev_calculated
      || Offset == 0)
      {
         MarkSignals(0Offsetdata);
      }
   }
   
   return rates_total;
}

Nos interesa sobre todo trabajar con la función CopyBuffer oculta en MarkSignals. Los valores del WPR suavizado se leerán en el array wpr[2], y los fractales se leerán en peaks[1] y hollows[1].

int MarkSignals(const int barconst int offsetconst double &data[])
{
   double wpr[2];
   double peaks[1], hollows[1];
   ...

A continuación, rellenamos los arrays locales mediante tres llamadas a CopyBuffer. Tenga en cuenta que no necesitamos lecturas directas de IndWPR, porque se utiliza en los cálculos de IndTripleEMA. Leemos los datos en el array wpr a través del manejador handleEMA3. También es importante que hay 2 búferes en el indicador fractal, y por lo tanto la función CopyBuffer llamada dos veces con diferentes índices 0 y 1 para los arrays peaks y hollows, respectivamente. Los arrays de fractales se leen con una sangría de FractalOrder, porque un fractal sólo puede formarse en una barra que tenga un cierto número de barras a la izquierda y a la derecha.

   if(CopyBuffer(handleEMA30bar + offset2wpr) == 2
   && CopyBuffer(handleFractals0bar + offset + FractalOrder1peaks) == 1
   && CopyBuffer(handleFractals1bar + offset + FractalOrder1hollows) == 1)
   {
      ...

A continuación, tomamos de la barra anterior del búfer Filter la dirección anterior del filtro (al principio del historial es 0, pero cuando aparece un fractal alcista o bajista, escribimos ahí +1 o -1, esto se puede ver en el código fuente justo debajo) y la cambiamos en consecuencia cuando se detecta algún fractal nuevo.

      int filterdirection = (int)Filter[bar + 1];
      
      // the last fractal sets the reversal movement
      if(peaks[0] != EMPTY_VALUE)
      {
         filterdirection = -1// sell
      }
      if(hollows[0] != EMPTY_VALUE)
      {
         filterdirection = +1// buy
      }
   
      Filter[bar] = filterdirection// remember the current direction

Por último, analizamos la transición del indicador WPR suavizado de la zona superior o inferior a la zona media, teniendo en cuenta la anchura de las zonas especificadas en Threshold.

      // translate 2 WPR values into the range [-1,+1]
      const double old = (wpr[0] + 50) / 50;     // +1.0 -1.0
      const double last = (wpr[1] + 50) / 50;    // +1.0 -1.0
      
      // bounce from the top down
      if(filterdirection == -1
      && old >= 1.0 - Threshold && last <= 1.0 - Threshold)
      {
         UpBuffer[bar] = data[bar];
         return -1// sale
      }
      
      // bounce from the bottom up
      if(filterdirection == +1
      && old <= -1.0 + Threshold && last >= -1.0 + Threshold)
      {
         DownBuffer[bar] = data[bar];
         return +1// purchase
      }
   }
   return 0// no signal
}

A continuación se muestra una captura de pantalla del indicador resultante en el gráfico.

Indicador de señal UseWPRFractals basado en WPR, EMA3 y fractales

Indicador de señal UseWPRFractals basado en WPR, EMA3 y fractales