- Conceptos básicos del calendario
- Obtener la lista y las descripciones de los países disponibles
- Consultar tipos de eventos por país y moneda
- Obtener descripciones de eventos por ID
- Obtener registros de eventos por país o moneda
- Obtener registros de eventos de un tipo específico
- Leer registros de sucesos por ID
- Seguimiento de los cambios de eventos por país o moneda
- Seguimiento de los cambios de eventos por tipo
- Filtrar eventos por múltiples condiciones
- Transferir la base de datos de calendarios al probador
- Operar con el calendario
Transferir la base de datos de calendarios al probador
El calendario sólo está disponible para los programas MQL en línea, por lo que simular las estrategias de trading de noticias plantea algunas dificultades. Una de las soluciones consiste en crear de forma independiente una determinada imagen del calendario, es decir, la caché, y luego utilizarla dentro del probador. Las tecnologías de almacenamiento en caché pueden ser diferentes, como archivos o una base de datos SQLite incrustada. En esta sección mostraremos una implementación utilizando un archivo.
En cualquier caso, cuando utilice la caché de calendario, recuerde que corresponde a un punto concreto en el tiempo X. En todos los eventos «antiguos» (informes financieros) que ocurrieron antes de X, los valores reales ya están establecidos, y en los posteriores (en el «futuro», relativos a X) no hay valores reales, y no los habrá hasta que aparezca una nueva copia más reciente de la caché. En otras palabras: no tiene sentido probar indicadores y Asesores Expertos a la derecha de X. En cuanto a los que están a la izquierda de X, debe evitar mirar hacia adelante, es decir, no leer los indicadores actuales hasta el momento de la publicación de cada noticia específica.
¡Atención! Cuando se solicitan datos de calendario en el terminal, la hora de todos los eventos se notifica teniendo en cuenta la zona horaria actual del servidor, incluida una posible corrección por el horario de «verano» (por regla general, esto significa aumentar las marcas de tiempo en 1 hora). Esto sincroniza la publicación de noticias con los tiempos de cotización en línea. Sin embargo, los cambios de reloj pasados (hace medio año, un año o más) sólo se muestran en las cotizaciones, pero no en los eventos del calendario. Toda la base de datos del calendario se lee a través de MQL5 según la zona horaria actual del servidor. Debido a esto, cualquier archivo de calendario creado contendrá las marcas de tiempo correctas para aquellos eventos que ocurrieron con el mismo modo DST (activado o desactivado) que estaba activo en el momento del almacenamiento. Para los eventos en semestres «opuestos» se requiere realizar un ajuste de forma independiente durante una hora después de leer el archivo. En los ejemplos siguientes, se omite esta situación.
Llamemos a la clase de caché CalendarCache y pongámosla en un archivo llamado CalendarCache.mqh. Tendremos que guardar las 3 tablas de la base del calendario en el archivo (MqlCalendarCountry, MqlCalendarEvent, MqlCalendarValue). MQL5 proporciona las funciones FileWriteArray y FileReadArray (véase Escritura y lectura de arrays) que pueden escribir y leer directamente arrays de estructuras simples en archivos. Sin embargo, 2 de cada 3 estructuras en nuestro caso no son simples, porque tienen campos de cadena. Por lo tanto, necesitamos un mecanismo para almacenar cadenas por separado, similar al que ya utilizamos en la clase CalendarFilter (había un array de cadenas stringCache, y el índice de la cadena deseada de este array se indicaba en los filtros).
Para evitar que falten cadenas de diferentes estructuras de «calendario» en un «diccionario», prepararemos una clase de plantilla StringRef: el parámetro de tipo T será cualquiera de las estructuras MqlCalendar. Esto nos dará una caché de cadenas separada para los países, y una caché de cadenas separada para los tipos de eventos.
template<typename T>
|
Las cadenas se almacenan en el array cache utilizando operator=, y se extraen de ella utilizando operator[] (con un índice ficticio que siempre se omite). Cada objeto almacena sólo el índice de la cadena en el array. El array cache se declara estático, por lo que acumulará todos los campos de cadena de una estructura T. Quien lo desee puede cambiar el método de almacenamiento en caché de forma que cada campo de la estructura tenga su propio array, pero esto no es importante para nosotros.
La escritura de un array en un archivo y la lectura desde un archivo se realizan mediante un par de métodos estáticos save y load: ambos toman un manejador de archivo como parámetro.
Teniendo en cuenta la clase StringRef, vamos a describir estructuras que duplican las estructuras de calendario estándar que utilizan objetos StringRef en lugar de campos de cadena. Por ejemplo, para MqlCalendarCountry obtenemos MqlCalendarCountryRef. Las estructuras estándar y modificadas se copian entre sí de forma similar mediante los operadores sobrecargados '=' y '[]'.
struct MqlCalendarCountryRef
|
Observe que los operadores de asignación del primer método tienen la sobrecarga '=' de StringRef, debido a lo cual todas las líneas caen dentro del array StringRef<MqlCalendarCountry>::cache. En el segundo método, las llamadas al operador '[]' obtienen de forma invisible la dirección de la cadena y devuelven de StringRef directamente la cadena almacenada en esa dirección en el array cache.
La estructura MqlCalendarEventRef se define de forma similar, pero sólo hay 3 campos en ella (source_url, event_code, name) que requieren sustituir el tipo string por StringRef<MqlCalendarEvent>. La estructura MqlCalendarValue no requiere tales transformaciones, ya que no contiene campos de cadena.
Con esto concluyen las etapas preparatorias, y se puede proceder a la clase principal de caché CalendarCache.
Por consideraciones generales, así como por compatibilidad con la clase CalendarFilter ya desarrollada, vamos a describir los campos de la caché que especifican el contexto (país o divisa), el intervalo de fechas para los eventos almacenados y el momento de generación de la caché (tiempo X, variable t).
class CalendarCache
|
En realidad, no tiene mucho sentido establecer restricciones al crear una caché a partir de un calendario. Una caché completa es probablemente más práctica, ya que su tamaño no es crítico al tratarse de unas dos docenas de megabytes hasta mediados de 2022 (esto incluye datos históricos desde 2007 con eventos previstos hasta 2024). No obstante, las restricciones pueden ser útiles para programas de demostración con una funcionalidad artificialmente reducida.
Es obvio que se deben proporcionar arrays de estructuras de calendario en la caché para almacenar todos los datos.
MqlCalendarValue values[];
|
Inicialmente, estos se rellenan desde la base de datos de calendarios mediante el método update.
bool update()
|
El campo t es una señal de la salud de la caché, con el tiempo de llenado de los arrays.
El objeto de caché lleno puede escribirse en un archivo utilizando el método save. Al principio del archivo hay un encabezado CALENDAR_CACHE_HEADER: esta es la cadena «MQL5 Calendar Cache\r\nv.1.0\r\n», que le permite asegurarse de que el formato es correcto cuando se lee. A continuación, el método guarda las variables context, from, to y t, así como el array values, «tal cual». Antes del array propiamente dicho, anotamos su tamaño para restablecerlo al leer.
bool save(string filename = NULL)
|
Con los arrays events y countries vienen nuestras estructuras de envoltorio con el sufijo «Ref». El método de ayuda store convierte el array events en un array de estructuras simples erefs, en la que las cadenas se sustituyen por números en el diccionario de cadenas StringRef<MqlCalendarEvent>. Estas estructuras simples pueden escribirse ya en un archivo de la forma habitual, pero para su posterior lectura es necesario guardar también todas las líneas del diccionario (llamando a StringRef<MqlCalendarEvent> ::save(handle)). Las estructuras de los países se convierten y guardan en un archivo del mismo modo.
MqlCalendarEventRef erefs[];
|
El método store antes mencionado es bastante sencillo: en él, en un bucle sobre los elementos, se ejecuta un operador de asignación sobrecargado en las estructuras MqlCalendarEventRef o MqlCalendarCountryRef.
template<typename T1,typename T2>
|
Para cargar el archivo recibido en el objeto caché se escribe un método espejo load. Lee los datos del archivo en variables y arrays en el mismo orden, realizando simultáneamente transformaciones inversas de los campos de cadena para tipos de eventos y países.
bool load(const string filename)
|
El método auxiliar restore utiliza la sobrecarga del operador '[]' en un bucle sobre los elementos de las estructuras MqlCalendarEventRef o MqlCalendarCountryRef para obtener la línea en sí por número de línea y asignarla a una estructura estándar MqlCalendarEvent o MqlCalendarCountry.
template<typename T1,typename T2>
|
En esta fase podríamos escribir ya un indicador de prueba sencillo basado en la clase CalendarCache, ejecutarlo en un gráfico en línea y guardarlo en un archivo con la caché del calendario. A continuación, el archivo podría cargarse desde la copia del indicador en el probador, y podría recibirse el conjunto completo de eventos. Sin embargo, esto no es suficiente para desarrollos prácticos.
Y es que, para acceder rápidamente a los datos, es necesario proporcionar indexing, un concepto bien conocido en programación, que tocaremos más adelante, en el capítulo dedicado a las bases de datos. En teoría, podríamos utilizar el motor SQLite integrado para almacenar la caché, y entonces obtendríamos índices «gratis», pero hablaremos de ello más adelante.
El sentido de la indexación es fácil de entender si imaginamos cómo implementar eficazmente análogos de las funciones de calendario estándar en nuestra caché. Por ejemplo, el ID del evento se pasa en la función CalendarValueById. La enumeración directa de los registros en el array values llevaría mucho tiempo. Por lo tanto, es necesario complementar el array con alguna «estructura de datos» que nos permita optimizar la búsqueda. «Estructura de datos» va entre comillas porque no se trata del significado del lenguaje de programación (struct), sino en general de la arquitectura de construcción de datos. Puede constar de diferentes partes y basarse en distintos principios organizativos. Por supuesto, los datos adicionales requerirán memoria, pero cambiar memoria por velocidad es un enfoque habitual en programación.
La solución más sencilla para la indexación es un array bidimensional separado, ordenado de forma ascendente para que se pueda buscar rápidamente utilizando la función ArrayBsearch. Para la segunda dimensión bastan dos elementos: los valores con índices [i][0], por los que se realiza la ordenación, contienen identificadores, y los valores [i][1] contienen las posiciones ordinales en el array de estructuras.
Otro concepto utilizado con frecuencia es hashing, que es una transformación de los valores iniciales en algunas claves (hashes, enteros) de tal manera que proporcione el mínimo número de colisiones (coincidencias de claves para datos iniciales diferentes). La propiedad fundamental de las claves es una distribución aleatoria casi uniforme de sus valores, por lo que pueden utilizarse como índices en arrays preasignados. Calcular una función hash para un único elemento de los datos originales es un proceso rápido que en realidad proporciona la dirección del elemento en sí. Por ejemplo, las conocidas estructuras de datos de mapa hash siguen este principio.
Si los dos valores originales obtienen el mismo hash (aunque esto es poco frecuente), se alinean en una lista por su clave y se realiza una búsqueda secuencial dentro de la lista. Sin embargo, como las funciones hash se eligen para que el número de coincidencias sea pequeño, la búsqueda suele dar con el objetivo en cuanto se calcula el hash.
Para la demostración, utilizaremos ambos enfoques en la clase CalendarCache: hashing y búsqueda binaria.
El paquete de MetaTrader 5 incluye un conjunto de clases para crear mapas hash (MQL5/Include/Generic/HashMap.mqh), pero nosotros nos las arreglaremos con nuestra propia implementación más sencilla, en la que sólo se mantiene el principio de utilizar la función hash.
Esquema de indexación de datos mediante hashing
En nuestro caso, basta con hacer un hash sólo de los identificadores de los objetos de calendario. La función hash que elijamos tendrá que convertir el identificador en un índice dentro de un array especial: la posición del identificador en el array de estructuras de «calendario» se almacenará en una celda con este índice. Para países, tipos de eventos y noticias específicas, se asigna según su propio array.
int id4country[];
|
Sus elementos almacenarán el número de secuencia de la entrada en el array correspondiente (countries, events, values).
Para cada uno de los arrays de «redirección» deben asignarse al menos 2 veces más elementos que el número de estructuras correspondientes en la base de datos (y en la caché) del calendario. Gracias a esta redundancia, minimizamos el número de colisiones hash. Se cree que la mayor eficacia se consigue cuando se elige un tamaño igual a un número primo. Por lo tanto, la clase tiene un método estático size2prime que devuelve el tamaño recomendado del array de «cestas» hash (uno de id4 -arrays) según el número de elementos de los datos de origen.
static int size2prime(const int size)
|
Todo el proceso de hashing del calendario se describe en el método hash. Veamos su inicio utilizando el ejemplo de un array de estructuras countries, y los otros dos arrays se tratan de forma similar.
Así que obtenemos el tamaño de índice «plano» recomendado id4country a partir del tamaño del array countries llamando a size2prime. Inicialmente, el array de índices se rellena con el valor -1, es decir, todos sus elementos están libres. Más adelante en el bucle a través de los países, es necesario calcular el hash para cada siguiente identificador de país y usándolo encontrar un índice libre en el array id4country. Este es el trabajo para el método de ayuda place.
bool hash()
|
La función hash dentro de place es la expresión (MathSwap(id) ^ 0xEFCDAB8967452301) % n, donde id es nuestro identificador, y n es el tamaño del array de índices. Así, el resultado de los cálculos se reduce siempre a un índice válido dentro de array[]. El principio de elección de una función hash es un tema aparte que queda fuera del alcance de este libro.
int place(const ulong id, const int index, int &array[])
|
Si la celda en la posición p del array de índices no está ocupada (igual a -1), escribimos inmediatamente la dirección de ubicación de la estructura de calendario en el elemento [p]. Si la celda ya está ocupada, intentamos seleccionar la siguiente utilizando la fórmula p = (p + attempt) % n, donde attempt es un contador de intentos (esta es nuestra versión camuflada de la lista de elementos con un hash coincidente). Si el número de intentos fallidos alcanza una décima parte de los datos originales, la indexación fallará, pero esto es prácticamente imposible con nuestro tamaño de array de índices sobredimensionado y la naturaleza conocida de los datos con hash (identificadores únicos).
Como resultado del hashing del array de estructuras, obtenemos un array de índices relleno (hay espacios libres en él, pero así es como está pensado), a través del cual podemos encontrar la ubicación de la estructura correspondiente en el array de estructuras por el identificador del elemento calendario. Para ello se utiliza el método find, cuyo significado es opuesto al de place.
template<typename S>
|
Veamos cómo se utiliza en la práctica. Las funciones estándar del calendario incluyen CalendarCountryById y CalendarEventById. Cuando necesite probar un programa MQL en el probador, no podrá acceder directamente a ellos, pero podrá cargar la caché del calendario en el objeto CalendarCache y, por lo tanto, deberá tener métodos similares.
bool calendarCountryById(ulong country_id, MqlCalendarCountry &cnt)
|
Utilizan el método find y los arrays de índices id4country y id4event.
Pero éstas no son las características más deseadas del calendario. Mucho más a menudo, un programa MQL con una estrategia de noticias necesita las funciones CalendarValueHistory, CalendarValueHistoryByEvent, CalendarValueLast o CalendarValueLastByEvent. Permiten acceder rápidamente a las entradas de la agenda por hora, país o divisa.
Por lo tanto, la clase CalendarCache debería proporcionar métodos similares. Aquí utilizaremos el segundo método de «indexación»: mediante una búsqueda binaria en un array ordenado.
Para implementar los métodos anteriores, vamos a añadir otros 4 arrays bidimensionales a la clase para establecer una correspondencia entre las noticias y el tipo de evento, las noticias y el país, las noticias y la divisa, así como las noticias y la hora de su publicación.
ulong value2event[][2]; // [0] - event_id, [1] - value_id
|
En el primer elemento de cada fila, es decir, bajo los índices [i][0] se registrará un ID de evento, país, divisa u hora, respectivamente. En el segundo elemento de la serie, bajo los índices [i][1] se colocarán los ID de noticias concretas. Después de llenar todos los arrays una vez, se ordenan utilizando ArraySort en los valores [i][0]. A continuación, podemos buscar por ID, por ejemplo, en event_id, todas las noticias de este tipo en el array value2event: la función ArrayBsearch devolverá el número del primer elemento coincidente, seguido de otros con el mismo event_id hasta que se encuentre un identificador distinto. El orden en la segunda «columna» no está definido (puede ser cualquiera).
Búsqueda rápida de estructuras relacionadas basada en la clasificación
Esta operación de unión mutua de estructuras de distintos tipos se lleva a cabo en el método bind. El tamaño de cada array «vinculante» es el mismo que el del array de noticias. Al recorrer todas las noticias en un bucle, utilizamos arrays de índices ya preparadas y el método find para un direccionamiento rápido.
bool bind()
|
En el caso de las divisas, se toma como identificador un número especial obtenido de la cadena mediante la función currencyId.
static ulong currencyId(const string s)
|
Ahora podemos presentar finalmente el constructor completo de la clase CalendarCache.
CalendarCache(const string _context = NULL,
|
Cuando se ejecuta en un gráfico en línea, el objeto creado con los parámetros predeterminados recopilará toda la información del calendario (update), la indexará (hash) y vinculará las tablas (bind). Si algo va mal en cualquiera de las etapas, el signo de error será 0 en la variable t. Si tiene éxito, el valor de la función TimeTradeServer permanecerá allí (recuerde, se coloca dentro de update). Este objeto listo para usar puede exportarse a un archivo utilizando el método save descrito anteriormente.
Cuando se lanza en el probador, el objeto debe crearse con una combinación especial de parámetros from y to (from > to); en este caso, el programa considerará la cadena context como un nombre de archivo y cargará el estado del calendario desde él. La forma más fácil de hacerlo es la siguiente:
CalendarCache calca("filename.cal", true); |
Dentro del método load también llamaremos a hash y bind para poner el objeto en estado de funcionamiento.
bool load(const string filename)
|
Utilizando la función CalendarValueLast como ejemplo, mostramos una implementación equivalente del método calendarValueLast (con exactamente el mismo prototipo). La caché utilizará la hora actual del «servidor» como identificador de cambios, a falta de una API de software abierta para leer la tabla de cambios del calendario en línea. Hipotéticamente, podríamos utilizar la información sobre los identificadores de cambio guardados por el servicio CalendarChangeSaver.mq5, pero este enfoque requiere una recopilación de estadísticas a largo plazo antes de poder empezar la simulación. Por lo tanto, el tiempo de «servidor» generado por el probador se acepta como un sustituto bastante adecuado.
Cuando el programa MQL solicita cambios por primera vez con un identificador nulo, simplemente devolvemos el valor de TimeTradeServer.
int calendarValueLast(ulong &change, MqlCalendarValue &result[],
|
Si el identificador de cambio ya es distinto de cero, continuamos con la rama principal del algoritmo.
En función del contenido de los parámetros code y currency, encontramos los identificadores del país y la divisa. Por defecto, es 0, lo que significa que busca todos los cambios.
ulong country_id = 0;
|
Más adelante, utilizando el recuento de tiempo transmitido change como inicio de la búsqueda, encontramos todas las noticias en value2time hasta el nuevo valor actual TimeTradeServer. Dentro del bucle utilizamos el método find para buscar el índice de la estructura MqlCalendarValue correspondiente en el array values y, si es necesario, comparar el país y la divisa del tipo de evento asociado a los deseados. Todas las noticias que cumplen los criterios se escriben en el array de salida result.
const ulong past = change;
|
Los métodos calendarValueHistory, calendarValueHistoryByEvent y calendarValueLastByEvent se implementan de acuerdo con un principio similar (este último en realidad delega todo el trabajo en el método calendarValueLast comentado anteriormente). El código fuente completo se encuentra en el archivo adjunto CalendarCache.mqh.
Basándose en la clase de caché, es lógico crear una clase derivada CalendarFilter, que, al procesar las peticiones, accedería a la caché en lugar de al calendario.
La solución final se encuentra en el archivo CalendarFilterCached.mqh. Debido a que la API de caché se diseñó sobre la base de la API estándar, la integración se reduce únicamente a reenviar las llamadas de filtro al objeto de caché (puntero automático cache).
class CalendarFilterCached: public CalendarFilter
|
Para probar el calendario en el probador, vamos a crear una nueva versión del indicador CalendarMonitor.mq5 CalendarMonitorCached.mq5.
Las principales diferencias son las siguientes.
Suponemos que se creará o ya se ha creado algún archivo de caché con el nombre «xyz.cal» (en la carpeta MQL5/Files) y por lo tanto lo conectaremos al programa MQL con la directiva tester_file.
#property tester_file "xyz.cal" |
Esta directiva garantiza la transferencia de la caché a cualquier agente, incluidos los distribuidos (que, sin embargo, es más relevante para los Asesores Expertos, en lugar de un indicador). Se puede crear un archivo de caché con este (u otro nombre) utilizando una nueva variable de entrada CalendarCacheFile. Si el usuario cambia el nombre por defecto por otro, entonces para trabajar en el probador necesitará corregir la directiva (¡requiere recompilación!) o transferir el archivo a la carpeta compartida de terminales (esta característica está admitida en la clase de caché, pero se queda «tras el telón»); no obstante, dicho archivo ya no está disponible para los agentes remotos.
input string CalendarCacheFile = "xyz.cal"; |
El objeto CalendarFilter se describe ahora como un puntero automático, porque dependiendo de dónde se ejecute el indicador, puede utilizar la clase original CalendarFilter así como la clase derivada CalendarFilterCached.
AutoPtr<CalendarFilter> fptr;
|
Al principio de OnInit hay un nuevo fragmento que se encarga de generar la caché y de leerla.
int OnInit()
|
Si se ha leído el archivo de caché, obtendremos el objeto terminado CalendarCache, que se pasa al constructor CalendarFilterCached. De lo contrario, el programa comprueba si se está ejecutando en el probador o en línea. La ausencia de caché en el probador es un caso fatal. En un gráfico regular, el programa crea un nuevo objeto basado en los datos del calendario integrado y lo guarda en la caché con el nombre especificado. Pero si el nombre del archivo se deja vacío, el indicador funcionará exactamente igual que el original: directamente con el calendario.
Vamos a ejecutar el indicador en el gráfico EURUSD. Se advertirá al usuario de que no se ha encontrado el archivo especificado y se ha intentado guardarlo. Siempre que el calendario esté activado en la configuración del terminal, deberíamos obtener aproximadamente las siguientes líneas en el registro. A continuación encontrará una versión con información de diagnóstico detallada. Los detalles pueden desactivarse comentando la directiva en el código fuente #define LOGGING.
Loading calendar cache xyz.cal
|
Ahora podemos elegir el indicador CalendarMonitorCached.mq5 en el probador y ver en dinámica, basándonos en el historial, cómo cambia la tabla de noticias.
Indicador de noticias con caché de calendario en el probador
La presencia de la caché de calendario le permite probar estrategias de trading en las noticias. Lo haremos en la próxima sección.