Indicadores multidivisa y de marco temporal múltiple

Hasta ahora, hemos considerado indicadores que funcionan con cotizaciones o ticks del símbolo del gráfico actual. Sin embargo, a veces es necesario analizar varios instrumentos financieros o un instrumento diferente del actual. En tales casos, como vimos en el caso del análisis de ticks, las series temporales estándar pasadas al indicador a través de los parámetros de OnCalculate no son suficientes. Es necesario solicitar de algún modo cotizaciones «extranjeras», esperar a que se construyan y sólo entonces calcular el indicador basándose en ellas.

La solicitud y creación de cotizaciones para un marco temporal distinto del marco temporal del gráfico actual no difiere de los mecanismos para trabajar con otros símbolos. Por lo tanto, en esta sección, consideraremos la creación de indicadores multidivisa, mientras que los indicadores de marco temporal múltiple pueden organizarse según un principio similar.

Uno de los problemas que tendremos que resolver es la sincronización de las barras en el tiempo. En concreto, para los distintos símbolos puede haber diferentes horarios de trading, fines de semana y, en general, la numeración de las barras en el gráfico principal y en las cotizaciones del símbolo «extranjero» puede ser diferente.

Para empezar, vamos a simplificar la tarea y a limitarnos a un símbolo arbitrario, que puede diferir del actual. Muy a menudo, el operador de trading necesita ver varios gráficos de diferentes símbolos al mismo tiempo (por ejemplo, el líder y el seguidor en un par correlacionado). Vamos a crear el indicador IndSubChartSimple.mq5 para mostrar la cotización de un símbolo seleccionado por el usuario en una subventana.

IndSubChartSimple

Para repetir la apariencia del gráfico principal, proporcionaremos en los parámetros de entrada no sólo una indicación del símbolo, sino también el modo de dibujo: DRAW_CANDLES, DRAW_BARS, DRAW_LINE. Los dos primeros requieren cuatro búferes y dan salida a los cuatro precios: Open, High, Low y Close (velas japonesas o barras), y este último utiliza un único búfer para mostrar la línea en el precio Close. Para admitir todos los modos, utilizaremos el número máximo necesario de búferes.

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   1
#property indicator_type1   DRAW_CANDLES
#property indicator_color1  clrBlue,clrGreen,clrRed // border,bullish,bearish

Los arrays de búferes se describen mediante nombres de tipo de precio.

double open[];
double high[];
double low[];
double close[];

La visualización de velas japonesas está activada por defecto. En este modo, MQL5 le permite especificar no sólo un color, sino varios. En la directiva #property indicator_colorN, están separados por comas. Si hay dos colores, el primero determina el color de los contornos de la vela, y el segundo determina el relleno. Si hay tres colores, como en nuestro caso, el primero determina el color de los contornos, mientras que el segundo y el tercero determinan el cuerpo de las velas alcistas y bajistas, respectivamente.

En el capítulo dedicado a gráficosnos familiarizaremos con la enumeración ENUM_CHART_MODE, que describe tres modos de gráficos disponibles.

Elementos ENUM_CHART_MODE

Elementos ENUM_DRAW_TYPE

CHART_CANDLES

DRAW_CANDLES

CHART_BARS

DRAW_BARS

CHART_LINE

DRAW_LINE

Se corresponden con los modos de dibujo que hemos elegido, ya que hemos elegido deliberadamente los métodos de dibujo que repiten los estándares. Es conveniente usar aquí ENUM_CHART_MODE porque sólo contiene los 3 elementos que necesitamos, a diferencia de ENUM_DRAW_TYPE, que tiene muchos otros métodos de dibujo.

Así, las variables de entrada tienen las siguientes definiciones:

input string SubSymbol = ""// Symbol
input ENUM_CHART_MODE Mode = CHART_CANDLES;

Se implementa una función sencilla para traducir ENUM_CHART_MODE a ENUM_DRAW_TYPE.

ENUM_DRAW_TYPE Mode2Style(const ENUM_CHART_MODE m)
{
   switch(m)
   {
      case CHART_CANDLESreturn DRAW_CANDLES;
      case CHART_BARSreturn DRAW_BARS;
      case CHART_LINEreturn DRAW_LINE;
   }
   return DRAW_NONE;
}

La cadena vacía en el parámetro de entrada SubSymbol significa el símbolo de gráfico actual. Sin embargo, como MQL5 no permite editar variables de entrada, tendremos que añadir una variable global para almacenar el símbolo de trabajo real y asignarlo en el manejador OnInit.

string symbol;
...
int OnInit()
{
   symbol = SubSymbol;
   if(symbol == ""symbol = _Symbol;
   else
   {
      // making sure the symbol exists and is selected in the Market Watch
      if(!SymbolSelect(symboltrue))
      {
         return INIT_PARAMETERS_INCORRECT;
      }
   }
   ...
}

También necesitamos comprobar si el símbolo introducido por el usuario existe y añadirlo a Market Watch: esto se hace mediante la función SymbolSelect, que estudiaremos en el capítulo sobre símbolos.

Para generalizar la configuración de búferes y gráficos, el código fuente dispone de varias funciones de ayuda:

  • InitBuffer: configuración de un búfer
  • InitBuffers: configuración de todo el conjunto de buffers
  • InitPlot: configuración de un gráfico

Las funciones separadas combinan varias acciones que se repiten al registrar entidades idénticas. También abren la vía a un mayor desarrollo de este indicador en el capítulo sobre gráficos: apoyaremos el cambio interactivo de los ajustes de dibujo en respuesta a las manipulaciones del usuario con el gráfico (véase la versión completa del indicador IndSubChart.mq5 en el capítulo Modos de visualización de gráficos).

void InitBuffer(const int indexdouble &buffer[],
   const ENUM_INDEXBUFFER_TYPE style = INDICATOR_DATA,
   const bool asSeries = false)
{
   SetIndexBuffer(indexbufferstyle);
   ArraySetAsSeries(bufferasSeries);
}
   
string InitBuffers(const ENUM_CHART_MODE m)
{
   string title;
   if(m == CHART_LINE)
   {
      InitBuffer(0closeINDICATOR_DATAtrue);
      // hiding all buffers not used for the line chart
      InitBuffer(1highINDICATOR_CALCULATIONStrue);
      InitBuffer(2lowINDICATOR_CALCULATIONStrue);
      InitBuffer(3openINDICATOR_CALCULATIONStrue);
      title = symbol + " Close";
   }
   else
   {
      InitBuffer(0openINDICATOR_DATAtrue);
      InitBuffer(1highINDICATOR_DATAtrue);
      InitBuffer(2lowINDICATOR_DATAtrue);
      InitBuffer(3closeINDICATOR_DATAtrue);
      title = "# Open;# High;# Low;# Close";
      StringReplace(title"#"symbol);
   }
   return title;
}

Tenga en cuenta que, cuando activa el modo de gráfico de líneas, sólo se utiliza el array close. Se le asigna el índice 0. Los tres arrays restantes están completamente ocultos al usuario debido a la propiedad INDICATOR_CALCULATIONS. Los cuatro arrays se utilizan en los modos vela y barra, y su numeración cumple con el estándar OHLC, tal y como requieren los tipos de dibujo DRAW_CANDLES y DRAW_BARS. A todos los arrays se les asigna la propiedad «serie», es decir, se indexan de derecha a izquierda.

La función InitBuffers devuelve el encabezado de los búferes en Data Window.

Todos los atributos de trazado necesarios se establecen en la función InitPlot.

void InitPlot(const int indexconst string nameconst int style,
   const int width = -1const int colorx = -1,
   const double empty = EMPTY_VALUE)
{
  PlotIndexSetInteger(indexPLOT_DRAW_TYPEstyle);
  PlotIndexSetString(indexPLOT_LABELname);
  PlotIndexSetDouble(indexPLOT_EMPTY_VALUEempty);
  if(width != -1PlotIndexSetInteger(indexPLOT_LINE_WIDTHwidth);
  if(colorx != -1PlotIndexSetInteger(indexPLOT_LINE_COLORcolorx);
}

La configuración inicial de un único gráfico (con índice 0) se realiza mediante nuevas funciones en el manejador OnInit.

int OnInit()
{
   ...
   InitPlot(0InitBuffers(Mode), Mode2Style(Mode));
   IndicatorSetString(INDICATOR_SHORTNAME"SubChart (" + symbol + ")");
   IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(symbolSYMBOL_DIGITS));
     
   return INIT_SUCCEEDED;
}

Aunque la configuración se realiza una sola vez en esta versión del indicador, se hace de forma dinámica, teniendo en cuenta el parámetro de entrada mode, a diferencia de la configuración estática proporcionadas por las directivas #property. En el futuro, en la versión completa del indicador, podremos llamar a InitPlot muchas veces, cambiando la representación externa del indicador «sobre la marcha».

Los búferes se rellenan en OnCalculate. En el caso más sencillo, cuando el símbolo dado coincide con el gráfico, podemos utilizar simplemente la siguiente implementación.

int OnCalculate(const int rates_totalconst int prev_calculated,
   const datetime &time[],
   const double &op[], const double &hi[], const double &lo[], const double &cl[],
   const long &[], const long &[], const int &[]) // unused
{
   if(prev_calculated ==0// needs clarification (see further)
   {
      ArrayInitialize(openEMPTY_VALUE);
      ArrayInitialize(highEMPTY_VALUE);
      ArrayInitialize(lowEMPTY_VALUE);
      ArrayInitialize(closeEMPTY_VALUE);
   }
   
   if(_Symbol != symbol)
   {
      // being developed
      ...
   }
   else
   {
      ArraySetAsSeries(optrue);
      ArraySetAsSeries(hitrue);
      ArraySetAsSeries(lotrue);
      ArraySetAsSeries(cltrue);
      for(int i = 0i < MathMax(rates_total - prev_calculated1); ++i)
      {
         open[i] = op[i];
         high[i] = hi[i];
         low[i] = lo[i];
         close[i] = cl[i];
      }
   }
   
   return rates_total;
}

No obstante, al procesar un símbolo arbitrario, los parámetros del array no contienen las cotizaciones necesarias, y el número total de barras disponibles es probablemente diferente. Además, cuando se coloca un indicador en un gráfico por primera vez, es posible que las cotizaciones de un símbolo «extranjero» no estén listas en absoluto si no se ha abierto previamente otro gráfico cercano para él. Además, las cotizaciones de un símbolo ajeno se cargarán de forma asíncrona, por lo que puede «llegar» un nuevo lote de barras en cualquier momento, lo que requerirá un recálculo completo.

Por lo tanto, vamos a crear variables que controlan el número de barras en el otro símbolo (lastAvailable), un «clon» editable de un argumento constante prev_calculated, así como una bandera de cotizaciones preparadas.

   static bool initialized// symbol quotes readiness flag
   static int lastAvailable// number of bars for a symbol (and the current timeframe)
   int _prev_calculated = prev_calculated// editable copy of prev_calculated

Al principio de OnCalculate vamos a añadir una comprobación para la aparición simultánea de más de una barra: utilizamos la variable lastAvailable que rellenamos en función del valor de iBars(symbol, _Period) antes de la salida regular anterior de la función, es decir, en caso de que el cálculo se realice con éxito. Si se carga un historial adicional, debemos restablecer _prev_calculated y el número de barras a 0, así como eliminar la bandera de preparación para volver a calcular el indicador.

int OnCalculate(const int rates_totalconst int prev_calculated,
   const datetime &time[],
   const double &op[], const double &hi[], const double &lo[], const double &cl[],
   const long &[], const long &[], const int &[]) // unused
{
   ...
   if(iBars(symbol_Period) - lastAvailable > 1)
   {
      // loading additional history or first start
      _prev_calculated = 0;
      initialized = false;
      lastAvailable = 0;
   }
   
   // then everywhere we use a copy of _prev_calculated
   if(_prev_calculated == 0)
   {
      ArrayInitialize(openEMPTY_VALUE);
      ArrayInitialize(highEMPTY_VALUE);
      ArrayInitialize(lowEMPTY_VALUE);
      ArrayInitialize(closeEMPTY_VALUE);
   }
   
   if(_Symbol != symbol)
   {
      // request quotes and "wait" till they are ready
      ...
      // main calculation (filling buffers)
      ...
   }
   else
   {
      ... // as is
   } 
   lastAvailable = iBars(symbol_Period);
   return rates_total;
}

La palabra «wait» (esperar) del comentario no está entrecomillada accidentalmente. Como recordamos, no podemos esperar realmente en los indicadores (para no ralentizar el hilo de interfaz del terminal). En lugar de ello, si no hay suficientes datos, simplemente debemos salir de la función. Así, «wait» significa esperar a que se calcule el siguiente evento: a la llegada de un tick o en respuesta a una solicitud de actualización del gráfico.

El siguiente código comprobará si las cotizaciones están listas.

int OnCalculate(const int rates_totalconst int prev_calculated,
   const datetime &time[],
   const double &op[], const double &hi[], const double &lo[], const double &cl[],
   const long &[], const long &[], const int &[]) // unused
{
   ...
   if(_Symbol != symbol)
   {
      if(!initialized)
      {
         Print("Host "_Symbol" "rates_total" bars up to ", (string)time[0]);
         Print("Updating "symbol" "lastAvailable" -> "iBars(symbol_Period), " / ",
            (iBars(symbol_Period) > 0 ?
               (string)iTime(symbol_PeriodiBars(symbol_Period) - 1) : "n/a"),
            "... Please wait");
         if(QuoteRefresh(symbol_Periodtime[0]))
         {
            Print("Done");
            initialized = true;
         }
         else
         {
            // asynchronous request to update the chart
            ChartSetSymbolPeriod(0_Symbol_Period);
            return 0// nothing to show yet
         }
      }
      ...

El trabajo principal lo realiza la función especial QuoteRefresh. Recibe como argumentos el símbolo deseado, el marco temporal y la hora de la primera barra (la más antigua) del gráfico actual: no nos interesan fechas anteriores, pero el símbolo solicitado puede no tener un historial para toda esta profundidad. Por eso es conveniente ocultar todas las complejidades de las comprobaciones en una función independiente.

La función devolverá true en cuanto los datos se hayan descargado y sincronizado en la medida de lo posible. Analizaremos su estructura interna dentro de un minuto.

Una vez realizada la sincronización, utilizamos la función iBarShift para buscar barras síncronas y copiar sus valores OHLC (funciones iOpen, iHigh, iLow, iClose).

      ArraySetAsSeries(timetrue); // go from present to past
      for(int i = 0i < MathMax(rates_total - _prev_calculated1); ++i)
      {
         int x = iBarShift(symbol_Periodtime[i], true);
         if(x != -1)
         {
            open[i] = iOpen(symbol_Periodx);
            high[i] = iHigh(symbol_Periodx);
            low[i] = iLow(symbol_Periodx);
            close[i] = iClose(symbol_Periodx);
         }
         else
         {
            open[i] = high[i] = low[i] = close[i] = EMPTY_VALUE;
         }
      }

Una forma alternativa y, a primera vista, más eficiente de copiar arrays de precios enteros utilizando las funciones Copiar no es adecuada en este caso, porque las barras con índices iguales pueden corresponder a diferentes marcas de tiempo en diferentes símbolos. Por lo tanto, después de copiar, habría que analizar las fechas y mover los elementos dentro de los búferes, ajustándolos a la hora del gráfico actual.

Puesto que en la función iBarShift true se pasa como último parámetro, la función buscará una coincidencia exacta de la hora de las barras. Si no hay ninguna barra en otro símbolo, obtendremos -1 y mostraremos un espacio vacío (EMPTY_VALUE) en el gráfico.

Tras un cálculo completo satisfactorio, las nuevas barras se calcularán en modo económico, es decir, teniendo en cuenta _prev_calculated y rates_total.

Pasemos ahora a la función QuoteRefresh. Se trata de una función universal y útil, por lo que se incluye en el archivo de encabezado QuoteRefresh.mqh.

Al principio, comprobamos si las series temporales del símbolo actual y el marco temporal actual se solicitan a un programa MQL de tipo indicador. Tales solicitudes están prohibidas, ya que la serie temporal «nativa» sobre la que se ejecuta el indicador ya está siendo construida por el terminal o está lista: solicitarla de nuevo puede provocar bucles o bloqueos. Por lo tanto, simplemente devolvemos el indicador de sincronización (SERIES_SYNCHRONIZED) y, si aún no está listo, el indicador deberá comprobar los datos más tarde (en los próximos ticks, por temporizador, o cualquier otra cosa).

bool QuoteRefresh(const string assetconst ENUM_TIMEFRAMES period,
   const datetime start)
{
   if(MQL5InfoInteger(MQL5_PROGRAM_TYPE) == PROGRAM_INDICATOR
      && _Symbol == asset && _Period == period)
   {
      return (bool)SeriesInfoInteger(assetperiodSERIES_SYNCHRONIZED);
   }
   ...

La segunda comprobación se refiere al número de barras: si ya es igual al máximo permitido en los gráficos, no tiene sentido seguir descargando nada.

   if(Bars(assetperiod) >= TerminalInfoInteger(TERMINAL_MAXBARS))
   {
      return (bool)SeriesInfoInteger(assetperiodSERIES_SYNCHRONIZED);
   }
   ...

La siguiente parte de código solicita secuencialmente al terminal las fechas de inicio de las cotizaciones disponibles:

  • en un plazo determinado (SERIES_FIRSTDATE);
  • sin enlace a un marco temporal (SERIES_TERMINAL_FIRSTDATE) en la base de datos local del terminal;
  • sin un enlace a un marco temporal (SERIES_SERVER_FIRSTDATE) en el servidor.

Si en algún momento la fecha solicitada ya se encuentra en el área de datos disponibles, obtenemos true como señal de que está lista. En caso contrario, se solicitan datos a la base de datos local del terminal o al servidor, seguidos de la construcción de una serie temporal (todo ello se realiza de forma asíncrona y automática en respuesta a nuestras llamadas a CopyTime; pueden utilizarse otras funciones de Copy).

   datetime times[1];
   datetime first = 0server = 0;
   if(PRTF(SeriesInfoInteger(assetperiodSERIES_FIRSTDATEfirst)))
   {
      if(first > 0 && first <= start)
      {
         // application data exists, it is already ready or is being prepared
         return (bool)SeriesInfoInteger(assetperiodSERIES_SYNCHRONIZED);
      }
      else
      if(PRTF(SeriesInfoInteger(assetperiodSERIES_TERMINAL_FIRSTDATEfirst)))
      {
         if(first > 0 && first <= start)
         {
            // technical data exists in the terminal database,
            // initiate the construction of a timeseries or immediately get the desired
            return PRTF(CopyTime(assetperiodfirst1times)) == 1;
         }
         else
         {
            if(PRTF(SeriesInfoInteger(assetperiodSERIES_SERVER_FIRSTDATEserver)))
            {
               // technical data exists on the server, let's request it
               if(first > 0 && first < server)
                  PrintFormat(
                    "Warning: %s first date %s on server is less than on terminal ",
                     assetTimeToString(server), TimeToString(first));
              // you can't ask for more than the server has - so fmax
              return PRTF(CopyTime(assetperiodfmax(startserver), 1times)) == 1;
            }
         }
      }
   }
   
   return false;
}

El indicador está listo. Vamos a compilarlo y ejecutarlo, por ejemplo, en el gráfico EURUSD, H1, especificando USDRUB como símbolo adicional. El registro mostrará algo como esto:

Host EURUSD 20001 bars up to 2018.08.09 13:00:00
Updating USDRUB 0 -> 14123 / 2014.12.22 11:00:00... Please wait
SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first)=false / HISTORY_NOT_FOUND(4401)
Host EURUSD 20001 bars up to 2018.08.09 13:00:00
Updating USDRUB 0 -> 14123 / 2014.12.22 11:00:00... Please wait
SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first)=true / ok
Done

Una vez finalizado el proceso (mensaje «Done» (hecho)), la subventana mostrará las velas del otro gráfico.

Indicador IndSubChartSimple - DRAW_CANDLES con cotizaciones de un símbolo de terceros

Indicador IndSubChartSimple - DRAW_CANDLES con cotizaciones de un símbolo de terceros

Es importante señalar que, debido a la brevedad de la sesión de trading, las barras significativas para USDRUB ocupan sólo la parte diaria de cada intervalo diario.

IndUnityPercent

El segundo indicador que crearemos en esta sección es un indicador multidivisa (multiactivo) real IndUnityPercent.mq5. Su idea es mostrar la fuerza relativa de todas las divisas independientes (activos) incluidas en los instrumentos financieros dados. Por ejemplo, si negociamos una cesta de dos tickers EURUSD y XAUUSD, entonces en el valor de la cesta se tienen en cuenta el dólar, el euro y el oro: cada uno de estos activos tiene un valor relativo en comparación con los demás.

En cada momento existen precios corrientes, que se describen mediante las siguientes fórmulas:

EUR / USD = EURUSD
XAU / USD = XAUUSD

donde las variables EUR, USD, XAU son algunos «valores» independientes de los activos, y EURUSD y XAUUSD son constantes (cotizaciones conocidas).

Para encontrar las variables vamos a añadir otra ecuación al sistema, limitando la suma de los cuadrados de las variables a uno (de ahí la primera palabra del nombre del indicador, Unidad):

EUR * EUR + USD * USD + XAU * XAU = 1

Puede haber muchas más variables, y es lógico designarlas como xi. Tenga en cuenta que x0 es la divisa principal, necesaria y común a todos los instrumentos.

Entonces, en términos generales, las fórmulas para calcular variables se escribirán de la siguiente manera (omitiremos el proceso de su derivación):

x0 = sqrt(1 / (1 + sum(C(xix0)2))), i = 1..n
xi = C(xix0) * x0i = 1..n

donde n es el número de variables, C(xi,x0) es la cotización del par i-ésimo. Tenga en cuenta que el número de variables es mayor que el número de instrumentos en 1.

Dado que las cotizaciones que intervienen en el cálculo suelen ser muy diferentes (por ejemplo, como en el caso de EURUSD y XAUUSD) y se expresan sólo entre sí (es decir, sin referencia a ninguna base estable), tiene sentido pasar de los valores absolutos a los porcentajes de variación. Así, al escribir algoritmos según las fórmulas anteriores, en lugar de la cotización C(xi,x0) tomaremos la proporción C(xi,x0)[0] / C(xi,x0)[1], donde los índices entre corchetes significan la barra actual [0] y la anterior [1]. Además, para acelerar el cálculo, puede prescindir de elevar al cuadrado y sacar la raíz cuadrada.

Para visualizar las líneas, proporcionaremos un cierto número máximo admisible de divisas y búferes de indicadores. Por supuesto, es posible utilizar sólo algunos de ellos en los cálculos si el usuario introduce menos símbolos. Pero no puede aumentar el límite dinámicamente: tendrá que cambiar las directivas y recompilar el indicador.

#define BUF_NUM 15
#property indicator_separate_window
#property indicator_buffers BUF_NUM
#property indicator_plots BUF_NUM

Al aplicar este indicador, resolveremos un problema desagradable por el camino. Dado que habrá muchos búferes del mismo tipo, el enfoque estándar es codificarlos ampliamente por «multiplicación» (el indeseable estilo de programación «copiar y pegar»).

double buffer1[];
...
double buffer15[];
   
void OnInit()
{
   SetIndexBuffer(0buffer1);
   ...
   SetIndexBuffer(14buffer15);
}

Esto es incómodo, ineficaz y propenso a errores. En lugar de ello, vamos a aplicar la POO. Crearemos una clase que almacenará un array para el buffer del indicador y será responsable de su configuración uniforme, ya que nuestros búferes deberían ser iguales (excepto por los colores y, posiblemente, un mayor grosor para aquellas divisas que conforman el símbolo del gráfico actual, pero esto se afina más tarde, después de que el usuario introduzca los parámetros).

Con una clase de este tipo, podemos simplemente distribuir un array de sus objetos, y los búferes de indicadores se conectarán y configurarán automáticamente en la cantidad requerida. Esquemáticamente, este enfoque se ilustra con el siguiente pseudocódigo:

// "engine" code supporting an array of unified indicator buffers
class Buffer
{
   static int count// global buffer counter
   double array[];   // array for this buffer
   int cursor;       // pointer of assigned element
public:
   // constructor sets up and connects the array
   Buffer()
   {
      SetIndexBuffer(count++, array);
      ArraySetAsSeries(array, ...);
   }
   // overload to set the number of the element of interest
   Buffer *operator[](int index)
   {
      cursor = index;
      return &this;
   }
   // overload to write value to selected element
   double operator=(double x)
   {
      buffer[cursor] = x;
      return x;
   }
   ...
};
   
static int Buffer::count;

Con las sobrecargas de operadores, podemos ceñirnos a la sintaxis familiar para asignar valores a los elementos de un objeto de búfer: buffer[i] = value.

En el código del indicador, en lugar de muchas líneas con descripciones de arrays individuales, bastará con definir un «array de arrays».

// indicator code
// construct 15 buffer objects with auto-registration and configuration
Buffer buffers[15];
...

La versión completa de las clases que implementan este mecanismo está disponible en el archivo IndBufArray.mqh. Tenga en cuenta que sólo admite búferes, no diagramas. Lo ideal sería ampliar el conjunto de clases con otras nuevas, lo que permitiría crear objetos de diagrama ya hechos que ocuparían el número necesario de búferes en el array de búferes según el tipo de diagrama concreto. Le sugerimos que estudie y complete usted mismo el expediente. En concreto, el código contiene una clase que gestiona un array de búferes de indicador BufferArray para crear «arrays de arrays» con los mismos valores de propiedad, como el tipo ENUM_INDEXBUFFER_TYPE, dirección de indexación, valor vacío. Lo utilizamos en el nuevo indicador del siguiente modo:

BufferArray buffers(BUF_NUMtrue);

Aquí, el número requerido de búferes se pasa en el primer parámetro del constructor, y el indicador de indexación como en una serie temporal se pasa en el segundo parámetro (encontrará más información al respecto más adelante).

Después de esta definición, podemos utilizar una práctica notación en cualquier parte del código para establecer el valor de la barra j-ésima del búfer i-ésimo (utiliza una sobrecarga doble del operador [] en el objeto de búfer y también en el array de búferes):

buffers[i][j] = value;

En las variables de entrada del indicador, permitiremos que el usuario especifique una lista de símbolos separados por comas y limite el número de barras para el cálculo sobre el historial, a fin de controlar la carga y sincronización de un conjunto potencialmente grande de instrumentos. Si decide mostrar todo el historial disponible, deberá identificar y aplicar el menor número de barras disponibles para los distintos instrumentos y controlar la carga de historial adicional desde el servidor.

input string Instruments = "EURUSD,GBPUSD,USDCHF,USDJPY,AUDUSD,USDCAD,NZDUSD";
input int BarLimit = 500;

Al iniciar el programa, analice la lista de símbolos y forme un array Symbols independiente de tamaño SymbolCount.

string Symbols[];
int direction[]; // direct(+1)/reverse(-1) rate to the common currency
int SymbolCount;

Todos los símbolos deben tener la misma divisa común (normalmente USD) para revelar correlaciones mutuas. Dependiendo de si esta divisa común en un símbolo concreto es la base (en el primer lugar del par, si hablamos de Forex) o la divisa de cotización (en el segundo lugar del par Forex), el cálculo utiliza sus cotizaciones directas o inversas (1.0 / tasa). Esta dirección se almacenará en el array Direction.

Veamos la función InitSymbols que realiza las acciones descritas. Si la lista se analiza correctamente, devuelve el nombre de la divisa común. La función SymbolInfoString integrada permite obtener la divisa base y la divisa de cotización de cualquier instrumento financiero: la estudiaremos en el capítulo sobre instrumentos financieros.

string InitSymbols()
{
   SymbolCount = fmin(StringSplit(Instruments, ',', Symbols), BUF_NUM - 1);
   ArrayResize(SymbolsSymbolCount);
   ArrayResize(DirectionSymbolCount);
   ArrayInitialize(Direction0);
   
   string common = NULL// common currency
   
   for(int i = 0i < SymbolCounti++)
   {
      // guarantee the presence of the symbol in the Market Review
      if(!SymbolSelect(Symbols[i], true)) 
      {
         Print("Can't select "Symbols[i]);
         return NULL;
      }
      
      // get the currencies that make up the symbol
      string firstsecond;
      first = SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_BASE);
      second = SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_PROFIT);
    
      // count the number of inclusions of each currency
      if(first != second)
      {
         workCurrencies.inc(first);
         workCurrencies.inc(second);
      }
      else
      {
         workCurrencies.inc(Symbols[i]);
      }
   }
   ...

El bucle realiza un seguimiento de la aparición de cada divisa en todos los instrumentos utilizando una clase de plantilla auxiliar MapArray. Dicho objeto se describe en el indicador a nivel global y requiere la conexión del archivo de encabezado MapArray.mqh.

#include <MQL5Book/MapArray.mqh>
...
// array of pairs [name; number]
// to calculate currency usage statistics
MapArray<string,intworkCurrencies;
...
string InitSymbols()
{
   ...
}

Dado que esta clase desempeña un papel secundario, no se describe en detalle aquí. Puede consultar el código fuente para obtener más detalles. La conclusión es que cuando se llama a su método inc para un nuevo nombre de divisa, se añade al array interno con el valor inicial del contador igual a 1, y si el nombre ya se ha encontrado, el contador se incrementa en 1.

Posteriormente, encontramos la divisa común como aquella cuyo contador es mayor que 1. Con los ajustes correctos, el resto de divisas deberían encontrarse exactamente una vez. He aquí la continuación de la función InitSymbols.

   ...   
   // find the common currency based on currency usage statistics
   for(int i = 0i < workCurrencies.getSize(); i++)
   {
      if(workCurrencies[i] > 1// counter greater than 1
      {
         if(common == NULL)
         {
            common = workCurrencies.getKey(i); // get the name of the i-th currency
         }
         else
         {
            Print("Collision: multiple common symbols");
            return NULL;
         }
      }
   }
   
   if(common == NULLcommon = workCurrencies.getKey(0);
   
   // knowing the common currency, determine the "direction" of each symbol
   for(int i = 0i < SymbolCounti++)
   {
      if(SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_PROFIT) == common)
         Direction[i] = +1;
      else if(SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_BASE) == common)
         Direction[i] = -1;
      else
      {
         Print("Ambiguous symbol direction "Symbols[i], ", defaults used");
         Direction[i] = +1;
      }
   }
   
   return common;
}

Teniendo lista la función InitSymbols, podemos escribir OnInit (con simplificaciones).

int OnInit()
{
   const string common = InitSymbols();
   if(common == NULLreturn INIT_PARAMETERS_INCORRECT;
   
   string base = SymbolInfoString(_SymbolSYMBOL_CURRENCY_BASE);
   string profit = SymbolInfoString(_SymbolSYMBOL_CURRENCY_PROFIT);
   
   // setting up lines by the number of currencies (number of symbols + 1)
   for(int i = 0i <= SymbolCounti++)
   {
      string name = workCurrencies.getKey(i);
      PlotIndexSetString(iPLOT_LABELname);
      PlotIndexSetInteger(iPLOT_DRAW_TYPEDRAW_LINE);
      PlotIndexSetInteger(iPLOT_SHOW_DATAtrue);
      PlotIndexSetInteger(iPLOT_LINE_WIDTH1 + (name == base || name == profit));
   }
  
   // hide extra buffers in the Data Window
   for(int i = SymbolCount + 1i < BUF_NUMi++)
   {
      PlotIndexSetInteger(iPLOT_SHOW_DATAfalse);
   }
  
   // single level at 1.0
   IndicatorSetInteger(INDICATOR_LEVELS1);
   IndicatorSetDouble(INDICATOR_LEVELVALUE01.0);
  
   // Name with parameters
   IndicatorSetString(INDICATOR_SHORTNAME,
      "Unity [" + (string)workCurrencies.getSize() + "]");
  
   // accuracy
   IndicatorSetInteger(INDICATOR_DIGITS5);
   
   return INIT_SUCCEEDED;
}

Ahora vamos a familiarizarnos con el manejador del evento principal OnCalculate.

Es importante señalar que el orden de iteración sobre las barras en el bucle principal se invierte, como en una serie temporal, del presente al pasado. Este enfoque es más conveniente para los indicadores multidivisa, porque la profundidad del historial de los distintos símbolos puede ser diferente, y tiene sentido calcular las barras desde el momento actual hacia atrás, hasta el primer momento en que no hay datos para ninguno de los símbolos. En este caso, la finalización anticipada del bucle no debe tratarse como un error: debemos devolver rates_total para mostrar en el gráfico los valores de las barras más relevantes que ya se han calculado.

No obstante, en esta versión simplificada de IndUnityPercent, no hacemos esto y utilizamos un enfoque más sencillo y rígido: el usuario debe definir la profundidad incondicional de la consulta del historial utilizando el parámetro BarLimit. En otras palabras: para todos los símbolos, debe haber datos hasta la marca de tiempo de la barra con el número BarLimit en el símbolo del gráfico. De lo contrario, el indicador intentará descargar los datos que faltan.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const doubleprice[])
{
   if(prev_calculated == 0)
   {
      buffers.empty(); // delegate total cleanup to the BufferArray class
   }
  
   // main loop in the direction "as in a timeseries" from the present to the past
   const int limit = MathMin(rates_total - prev_calculated + 1BarLimit);
   for(int i = 0i < limiti++)
   {
      if(!calculate(i))
      {
         EventSetTimer(1); // give 1 more second to upload and prepare data
         return 0// let's try to recalculate on the next call
      }
   }
   
   return rates_total;
}

La función Calculate (véase más abajo) calcula los valores de todos los búferes de la barra i-ésima. En caso de que falten datos, devolverá false, e iniciaremos un temporizador para dar tiempo a construir series temporales para todos los instrumentos requeridos. En el manejador del temporizador, enviaremos una petición al terminal para que actualice el gráfico de la forma habitual.

void OnTimer()
{
   EventKillTimer();
   ChartSetSymbolPeriod(0_Symbol_Period);
}

En la función Calculate, primero determinamos el intervalo de fechas de la barra actual y la anterior, sobre el que se calcularán los cambios.

bool Calculate(const int bar)
{
   const datetime time0 = iTime(_Symbol_Periodbar);
   const datetime time1 = iTime(_Symbol_Periodbar + 1);
   ...

Se han necesitado dos fechas para llamar a la siguiente función CopyClose en su versión, donde se indica el intervalo de fechas. En este indicador no podemos utilizar la opción con el número de barras, ya que cualquier símbolo puede tener huecos arbitrarios en las barras, diferentes de los huecos en otros símbolos. Por ejemplo, si hay barras en un símbolo t (actual) y t-1 (anterior), entonces es posible calcular correctamente el cambio Close[t]/Close[t-1]. No obstante, en otro símbolo, la barra t puede estar ausente, y una petición de dos barras devolverá las barras «más cercanas» (en el pasado) a la izquierda, y este pasado puede estar bastante alejado del «presente» (por ejemplo, corresponder a la sesión de trading del día anterior si el símbolo no se negocia las 24 horas del día).

Para evitar que esto ocurra, el indicador solicita cotizaciones estrictamente en el intervalo, y si éste resulta estar vacío para un símbolo concreto, significa que no hay cambios.

Al mismo tiempo, se pueden dar situaciones en las que una consulta de este tipo devuelva más de dos barras, y en este caso, las dos últimas (a la derecha) se toman siempre como las más relevantes. Por ejemplo, cuando se coloca en el gráfico USDRUB,H1, el indicador «verá» que después de la barra de las 17:00 de cada día hábil, hay una barra a las 10:00 del siguiente día hábil. Sin embargo, para los principales pares de divisas Forex, como EURUSD, habrá 16 barras H1 vespertinas, nocturnas y matutinas entre ellas.

bool Calculate(const int bar)
{
   ...
   double w[]; // receiving array of quotes (by bar)
   double v[]; // character changes
   ArrayResize(vSymbolCount);
   
   // find quote changes for each symbol
   for(int j = 0j < SymbolCountj++)
   {
      // try to get at least 2 bars for the j-th symbol,
      // corresponding to two bars of the symbol of the current chart
      int x = CopyClose(Symbols[j], _Periodtime0time1w);
      if(x < 2)
      {
         // if there are no bars, try to get the previous bar from the past
         if(CopyClose(Symbols[j], _Periodtime01w) != 1)
         {
            return false;
         }
         // then duplicate it as no change indication
         // (in principle, it was possible to write any constant 2 times)
         x = 2;
         ArrayResize(w2);
         w[1] = w[0];
      }
   
      // find the reverse course when needed
      if(Direction[j] == -1)
      {
         w[x - 1] = 1.0 / w[x - 1];
         w[x - 2] = 1.0 / w[x - 2];
      }
   
      // calculating changes as a ratio of two values
      v[j] = w[x - 1] / w[x - 2]; // last / previous
   }
   ...

Cuando se reciben los cambios, el algoritmo trabaja según las fórmulas dadas anteriormente y escribe los valores en los búferes de indicadores.

   double sum = 1.0;
   for(int j = 0j < SymbolCountj++)
   {
      sum += v[j];
   }
   
   const double base_0 = (1.0 / sum);
   buffers[0][bar] = base_0 * (SymbolCount + 1);
   for(int j = 1j <= SymbolCountj++)
   {
      buffers[j][bar] = base_0 * v[j - 1] * (SymbolCount + 1);
   }
   
   return true;
}

Vamos a ver cómo funciona el indicador con la configuración por defecto en un conjunto de instrumentos básicos de Forex (en la primera colocación, puede llevar un tiempo notable recibir series temporales si los gráficos no se abrieron para los instrumentos).

Indicador multisímbolo IndUnityPercent con los principales pares de Forex

Indicador multidivisa IndUnityPercent con las principales divisas Forex

La distancia entre las líneas de dos divisas en la ventana del indicador es igual al cambio de la cotización correspondiente en porcentaje (entre dos precios consecutivos Close). De ahí la segunda palabra del nombre del indicador: porcentaje.

En el próximo capítulo sobre el uso programático de los indicadores presentaremos una versión avanzada de IndUnityPercentPro.mq5, en la que las funciones de Copy se sustituirán por la llamada a indicadores integrados iMA, lo que nos permitirá aplicar el suavizado y el cálculo para un tipo arbitrario de precios sin ningún esfuerzo adicional.