- Principales características de los indicadores
- Evento indicador principal: OnCalculate
- Dos tipos de indicadores: para la ventana principal y para la subventana
- Ajuste del número de buffers y gráficos
- Asignación de un array como buffer: SetIndexBuffer
- Configuración de plot: PlotIndexSetInteger
- Reglas de asignación de buffers y gráficos
- Aplicación de directivas para personalizar plots
- Configuración de nombres de plots
- Visualización de las carencias de datos (elementos vacíos)
- Indicadores de subventanas independientes: tamaños y niveles
- Propiedades generales de los indicadores: precisión del título y del valor
- Coloreado de gráficos por elementos
- Omitir dibujo en barras iniciales
- Esperar datos y gestionar la visibilidad (DRAW_NONE)
- Indicadores multidivisa y multitemporal
- Seguimiento de formación de barras
- Comprobación de indicadores
- Limitaciones y ventajas de los indicadores
- Crear un borrador de indicador en el Asistente MQL
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
|
Los arrays de búferes se describen mediante nombres de tipo de precio.
double open[];
|
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
|
Se implementa una función sencilla para traducir ENUM_CHART_MODE a ENUM_DRAW_TYPE.
ENUM_DRAW_TYPE Mode2Style(const ENUM_CHART_MODE m)
|
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;
|
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 index, double &buffer[],
|
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 index, const string name, const int style,
|
La configuración inicial de un único gráfico (con índice 0) se realiza mediante nuevas funciones en el manejador OnInit.
int OnInit()
|
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_total, const int prev_calculated,
|
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
|
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_total, const int prev_calculated,
|
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_total, const int prev_calculated,
|
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(time, true); // go from present to past
|
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 asset, const ENUM_TIMEFRAMES period,
|
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(asset, period) >= TerminalInfoInteger(TERMINAL_MAXBARS))
|
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];
|
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
|
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
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
|
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(xi, x0)2))), i = 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
|
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[];
|
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
|
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
|
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_NUM, true); |
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";
|
Al iniciar el programa, analice la lista de símbolos y forme un array Symbols independiente de tamaño SymbolCount.
string Symbols[];
|
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()
|
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>
|
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.
...
|
Teniendo lista la función InitSymbols, podemos escribir OnInit (con simplificaciones).
int OnInit()
|
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,
|
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()
|
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)
|
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)
|
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;
|
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 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.