Recetas MQL5 - Servicios
Introducción
Hace relativamente poco tiempo, este tipo de programa como servicio apareció en el terminal comercial MetaTrader 5. Según el desarrollador, los servicios nos permiten crear nuestras propias fuentes de datos de precio para el terminal, pudiendo así transmitir los precios desde sistemas externos en tiempo real, tal como lo hacen los servidores comerciales de los brókeres, y esto está lejos de ser la única posibilidad de los servicios.
En este artículo, analizaremos los matices del trabajo con los servicios y nos familiarizaremos con sus notables propiedades. El material del artículo está más bien enfocado a principiantes. Partiendo de ello, trataremos de escribir el código de tal forma que sea completamente reproducible y se vaya haciendo más complicado de un ejemplo a otro.
1. Daemons en acción
Probablemente no sea un secreto que los servicios en MQL5 resultan similares a los servicios de Windows. Wikipedia da esta definición del servicio de Windows:
En nuestro caso, el entorno externo de los servicios no será el sistema operativo en sí, sino el caparazón del terminal MetaTrader5.
Unas cuantas palabras sobre los daemons.
Un daemon (daemon, dæmon, griego antiguo δαίμων, daemon) es un programa de computadora en sistemas similares a UNIX iniciado por el propio sistema, y que funciona en segundo plano sin interacción directa con el usuario.
En mi opinión, la definición del término captura la esencia del término con mucha precisión:
El término fue acuñado por los programadores del proyecto MAC (inglés)рус. del Instituto Tecnológico de Massachusetts, y se refiere al carácter de un experimento mental, al demonio de Maxwell, que clasifica las moléculas en segundo plano.[1] UNIX y los sistemas similares a UNIX han heredado esta terminología.
El daemon también es un personaje de la mitología griega que realiza tareas que los dioses no quieren asumir. Como se indica en el «Manual del administrador del sistema UNIX», en la antigua Grecia el concepto de "daemon personal" era, en parte, comparable al concepto moderno de "ángel guardián".[2]
Curiosamente, aunque los antiguos griegos no tenían computadoras, las relaciones de las entidades resultaban claras para ellos. Por lo tanto, en el contexto del artículo, intentaremos ocuparnos de aquello que los dioses no emprenden :)
2. Servicios - ¿qué hay en la Documentación?
Antes de profundizar en el tema, le sugerimos echar un vistazo a los materiales de la Documentación y ver cómo el desarrollador describe las capacidades de los servicios.
2.1Tipos de aplicaciones
En la primera página de la Documentación, en la sección Tipos de aplicaciones en MQL5, los servicios se definen como un tipo de programa MQL5:
- Un servicio es un programa que, a diferencia de los indicadores, asesores y scripts, no requiere vincularse a un gráfico para su funcionamiento. Al igual que los scripts, los servicios no gestionan ningún evento que no sea el evento de inicio. Para iniciar un servicio, su código deberá contener la función de manejador OnStart. Los servicios no aceptan ningún otro evento que no sea el Inicio, pero podremos enviar eventos personalizados a los gráficos usando el Gráfico de eventos personalizado. Los servicios se almacenarán en el directorio <directorio_del_terminal>\MQL5\Services.
Tenga en cuenta aquí que los servicios resultan muy similares a los scripts. La diferencia fundamental es que no están vinculados a ninguno de los gráficos.
2.2Ejecución de programas
La sección "Ejecución de programas" contiene un resumen de los programas MQL5:
Programa | Ejecución | Observación |
---|---|---|
Servicio | En un hilo propio; habrá tantos hilos de ejecución como servicios | Un servicio en bucle no puede estropear el funcionamiento de otros programas |
Script | En un hilo propio; habrá tantos hilos de ejecución como scripts | Un script en bucle no puede estropear el funcionamiento de otros programas |
Experto | En un hilo propio; habrá tantos hilos de ejecución como expertos | Un experto en bucle no puede estropear el funcionamiento de otros programas |
Indicador | Un hilo de ejecución para todos los indicadores en un símbolo. Habrá tantos hilos de ejecución como símbolos con indicadores | Un ciclo infinito en un indicador detendrá todos los demás indicadores en ese símbolo |
Es decir, los servicios no se distinguirán de los scripts y los expertos en cuanto al método de activación del hilo de ejecución. Los servicios también serán similares a los scripts y expertos en que la presencia de bloques de código en ciclo no afectará al funcionamiento de otros programas mql5.
2.3Prohibición del uso de funciones en los servicios
El desarrollador ofrece una lista exhaustiva de funciones cuyo uso está prohibido:
- ExpertRemove(),
- EventSetMillisecondTimer(),
- EventSetTimer(),
- EventKillTimer(),
- SetIndexBuffer(),
- IndicatorSetDouble(),
- IndicatorSetInteger(),
- IndicatorSetString(),
- PlotIndexSetDouble(),
- PlotIndexSetInteger(),
- PlotIndexSetString(),
- PlotIndexGetInteger().
Es bastante lógico que los servicios no puedan detener el asesor experto y trabajar con un temporizador, porque manejan solo el evento Start. Tampoco pueden trabajar con las funciones de indicadores personalizados.
2.4Carga y descarga de servicios
Hay varios puntos importantes en esta sección de la Documentación . Vamos a analizar cada uno de ellos.
Los servicios se cargan inmediatamente después de iniciarse el terminal, si se estaban ejecutando cuando al detener el mismo. Los servicios se descargan inmediatamente después de finalizar su funcionamiento.
Esta es una de las propiedades destacables de este tipo de programa como servicio. El servicio no necesitan ser monitoreado: una vez iniciado, realizará sus acciones automáticamente.
Los servicios tienen un único manejador OnStart() en el que puede organizar un ciclo infinito de obtención y procesamiento de datos, por ejemplo, creando y actualizando símbolos personalizados mediante funciones de red.
Así, podemos sacar una conclusión sencilla. Si el servicio debe realizar un conjunto de acciones únicas, entonces no será necesario realizar un ciclo en ningún bloque de código. Si la tarea consiste en la operación constante o regular del servicio, entonces deberemos envolver el bloque de código en un ciclo. Más adelante, analizaremos algunos ejemplos de estas tareas.
A diferencia de los asesores expertos, los indicadores y los scripts, los servicios no están vinculados a un gráfico específico; por ello, se ofrece un mecanismo aparte para iniciar el servicio.
Quizás esta sea la segunda característica destacable del servicio. Para él no existen gráficos sin los que su trabajo resulta imposible.
La creación de un nuevo ejemplar del servicio se realiza desde el Navegador usando el comando "Añadir servicio". Para iniciar, detener y eliminar un ejemplar del servicio, usaremos su menú. Para administrar todos los ejemplares, usaremos el menú del propio servicio.
Esta será la tercera propiedad destacable del servicio. Al tener solo un archivo de programa, podremos ejecutar varios ejemplares de este al mismo tiempo. Esto generalmente se realizará cuando resulte necesario usar diferentes parámetros (variables de entrada).
En general, eso es todo lo que se refiere a la información sobre los servicios en la Documentación.
3. Prototipo del servicio
La Guía de Ayuda que se llama en el terminal al presionar la teclaF1, describe el mecanismo para iniciar y administrar los servicios. Por consiguiente, no nos detendremos en esto ahora.
Ahora crearemos en el editor de código MetaEditor una plantilla de servicio y le asignaremos el nombredEmpty.mq5.
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { //--- } //+------------------------------------------------------------------+
Después de la compilación, veremos el nombre del servicio en el Navegador (Fig.1).
Fig.1 Servicio "dEmpty" en la subventana del Navegador
Después de añadir e iniciar el servicio dEmpty en la subventana del Navegador, obtendremos las siguientes entradas en el Diario de Registro:
CS 0 19:54:18.590 Services service 'dEmpty' started CS 0 19:54:18.592 Services service 'dEmpty' stopped
Los registros muestran que el servicio se ha iniciado y se ha detenido. Como no hay comandos en su código, no habrá ningún cambio en el terminal, y así no notaremos nada tras iniciar este servicio.
Vamos a intentar rellenar la plantilla de servicio con algunos comandos. Así, crearemos el servicio dStart.mq5 y escribiremos las líneas siguientes:
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); } //+------------------------------------------------------------------+
Tras iniciar el servicio, en la pestaña “Experts”, veremos la siguiente entrada:
CS 0 20:04:28.347 dStart Service "dStart" starts at: 2022.11.30 20:04:28.
De esta forma, el servicio dStart nos ha informado sobre su inicio, tras lo cual ha dejado de funcionar.
Ahora expandiremos las capacidades del servicio anterior y llamaremos al nuevo dStartStop.mq5.
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); ::Sleep(1000); now=::TimeLocal(); ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); } //+------------------------------------------------------------------+
El servicio actual ya informa no solo sobre su inicio, sino también sobre la finalización de su actividad.
Tras iniciar el servicio en el diario de registro, veremos las siguientes entradas:
2022.12.01 22:49:10.324 dStartStop Service "dStartStop" starts at: 2022.12.01 22:49:10 2022.12.01 22:49:11.336 dStartStop Service "dStartStop" stops at: 2022.12.01 22:49:11
Resulta fácil ver que la primera y segunda hora difieren en un segundo. Lo que ocurre simplemente es que la función nativa Sleep() se ha activado entre el primer y el último comando.
Ahora ampliaremos las capacidades del servicio actual para que se ejecute hasta que sea detenido forzosamente. Llamaremos al nuevo servicio dStandBy.mq5.
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); do { ::Sleep(1); } while(!::IsStopped()); now=::TimeLocal(); ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); //--- final goodbye for(ushort cnt=0; cnt<5; cnt++) { ::PrintFormat("Count: %hu", cnt+1); ::Sleep(10000); } } //+------------------------------------------------------------------+
Después de salir del ciclo do while, como el programa se ha detenido, el servicio aún escribirá varios valores de contador en el diario de registro. Después de cada entrada de este tipo, se llamará a Sleep() con un intervalo de retraso de 10 segundos.
Las siguientes entradas aparecerán en el diario de registro:
CS 0 23:20:44.478 dStandBy Service "dStandBy" starts at: 2022.12.01 23:20:44 CS 0 23:20:51.144 dStandBy Service "dStandBy" stops at: 2022.12.01 23:20:51 CS 0 23:20:51.144 dStandBy Count: 1 CS 0 23:20:51.159 dStandBy Count: 2 CS 0 23:20:51.175 dStandBy Count: 3 CS 0 23:20:51.191 dStandBy Count: 4 CS 0 23:20:51.207 dStandBy Count: 5
El servicio se ha iniciado a las 23:20:44 y se ha detenido forzosamente a las 23:20:51. También resulta fácil ver que los intervalos entre los valores del contador no superan los 0,02 segundos, aunque para tales intervalos se ha establecido previamente un retraso de 10 segundos.
Como podemos deducir de la Documentación para la función Sleep():
Observación
La función Sleep() no se puede llamar desde indicadores personalizados, ya que los indicadores se ejecutan en el hilo de interfaz y no deberían ralentizarlo. La función contiene la verificación integrada de la bandera de detención del experto cada 0,1 segundos.
Es decir, en nuestro caso, la función Sleep() ha detectado rápidamente que el servicio se ha detenido forzosamente y ha dejado de retrasar la ejecución del programa mql5.
Para ser más exhaustivos, echaremos un vistazo a lo que dice la Documentación sobre el valor retornado de la función de verificación del estado IsStopped():
Valor retornado
Retornará true si la variable del sistema _StopFlag contiene un valor distinto a 0. El valor distinto a cero se escribirá en la variable _StopFlag si se ha obtenido el comando para finalizar la ejecución del programa mql5. En este caso, deberemos finalizar el programa lo antes posible, de lo contrario, el programa se finalizará forzosamente desde el exterior en 3 segundos.
Así, tras una detención forzosa, el servicio tendrá 3 segundos para hacer otra cosa antes de desactivarse por completo. Veamos este momento en la práctica. Ahora añadiremos un cálculo matricial al código del servicio anterior, tras el ciclo; en dicho cálculo se invertirá aproximadamente un minuto. Después veremos si el servicio tiene tiempo para calcular todo tras su parada forzosa. El nuevo servicio tendrá el nombre srvcStandByMatrixMult.mq5.
Después del ciclo de conteo de los valores del contador, deberemos añadir el siguiente bloque al código anterior:
//--- Matrix mult //--- matrix A 1000x2000 int rows_a=1000; int cols_a=2000; //--- matrix B 2000x1000 int rows_b=cols_a; int cols_b=1000; //--- matrix C 1000x1000 int rows_c=rows_a; int cols_c=cols_b; //--- matrix A: size=rows_a*cols_a int size_a=rows_a*cols_a; int size_b=rows_b*cols_b; int size_c=rows_c*cols_c; //--- prepare matrix A double matrix_a[]; ::ArrayResize(matrix_a, rows_a*cols_a); for(int i=0; i<rows_a; i++) for(int j=0; j<cols_a; j++) matrix_a[i*cols_a+j]=(double)(10*::MathRand()/32767); //--- prepare matrix B double matrix_b[]; ::ArrayResize(matrix_b, rows_b*cols_b); for(int i=0; i<rows_b; i++) for(int j=0; j<cols_b; j++) matrix_b[i*cols_b+j]=(double)(10*::MathRand()/32767); //--- CPU: calculate matrix product matrix_a*matrix_b double matrix_c_cpu[]; ulong time_cpu=0; if(!MatrixMult_CPU(matrix_a, matrix_b, matrix_c_cpu, rows_a, cols_a, cols_b, time_cpu)) { ::PrintFormat("Error in calculation on CPU. Error code=%d", ::GetLastError()); return; } ::PrintFormat("time CPU=%d ms", time_cpu);
A continuación, iniciaremos el servicio dStandByMatrixMult y lo detendremos forzosamente después de unos segundos. Las siguientes líneas aparecerán en el diario de registro:
CS 0 15:17:23.493 dStandByMatrixMult Service "dStandByMatrixMult" starts at: 2022.12.02 15:17:23 CS 0 15:18:17.282 dStandByMatrixMult Service "dStandByMatrixMult" stops at: 2022.12.02 15:18:17 CS 0 15:18:17.282 dStandByMatrixMult Count: 1 CS 0 15:18:17.297 dStandByMatrixMult Count: 2 CS 0 15:18:17.313 dStandByMatrixMult Count: 3 CS 0 15:18:17.328 dStandByMatrixMult Count: 4 CS 0 15:18:17.344 dStandByMatrixMult Count: 5 CS 2 15:18:19.771 dStandByMatrixMult Abnormal termination
Podemos ver que el comando para finalizar la ejecución del programa mql5 llegó a las 15:18:17.282, mientras que el servicio en sí fue finalizado forzosamente a las 15:18:19.771. Efectivamente, han transcurrido 2.489 segundos desde el momento de la finalización hasta la parada forzosa del servicio. El hecho de que el servicio se haya detenido forzosamente y, además, con una finalización anormal, ha sido registrado por la entrada «Abnormal termination».
Debido a que, al final del servicio (_StopFlag== true), no quedan más de 3 segundos hasta la parada forzosa, no se recomienda realizar cálculos serios o acciones comerciales para el ciclo interrumpido.
Un ejemplo sencillo: supongamos que en el terminal se está ejecutando algún servicio cuya tarea consiste en cerrar todas las posiciones cuando el terminal está cerrado. Y ahora el terminal se cierra, y el servicio intenta liquidar todas las posiciones activas. Como resultado, el terminal se cerrará y algunas posiciones permanecerán abiertas, cosa que desconoceremos.
4. Ejemplos de uso
Antes de pasar a los ejemplos prácticos, le proponemos especular un poco sobre lo que pueden hacer los servicios en el terminal comercial. Por un lado, podemos introducir casi cualquier código en el servicio (salvo el que está prohibido), pero por otro, probablemente valga la pena, digamos, delimitar los esferas de influencia y proporcionar a los servicios su propio nicho en el entorno del terminal comercial.
En primer lugar, los servicios no deberán duplicar el trabajo de otros programas MQL5 activos: asesores expertos, indicadores, scripts. Supongamos que hay un asesor experto que coloca las órdenes límite de una señal al final de una sesión comercial, y que hay un servicio que también coloca dichas órdenes límite. Como resultado, puede verse afectado el sistema de conteo de órdenes límite en el propio asesor. O, si los números mágicos son distintos, el asesor puede perder de vista las órdenes realizadas por el servicio.
En segundo lugar, deberemos evitar la situación inversa: el conflicto de los servicios con otros programas MQL5. Supongamos que hay un asesor experto que coloca las órdenes límite de una señal al final de una sesión comercial, y que hay un servicio que se encarga de controlar que al final de la jornada bursátil se cierren todas las posiciones y se eliminen las órdenes pendientes. Existe un conflicto de intereses: el asesor colocará las órdenes y el servicio las eliminará de inmediato. Todo esto puede dar como resultado una especie de ataque DDoS en el servidor comercial.
En general, los servicios deberán integrarse armoniosamente en el funcionamiento del terminal comercial, sin oponerse a los programas mql5, e interactuando con ellos para un uso más eficiente de los algoritmos comerciales.
4.1 Eliminación de logs
Supongamos que el servicio tiene la tarea de borrar la carpeta con los logs (registros) generados por uno o más de los asesores expertos en el pasado (ayer, anteayer, etc.) al comenzar un nuevo día comercial.
¿Qué herramientas necesitaremos aquí? Pues necesitaremos operaciones de archivos y la definición de una nueva barra. Podrá leer más información sobre la clase para detectar una nueva barra en el artículo Gestor de evento "Nueva barra".
Ahora vamos a ver las operaciones con archivos. Las operaciones de archivos nativos no funcionarán aquí, porque nos encontraremos con las limitaciones del archivo del sandbox. Según la Documentación:
Por razones de seguridad, el trabajo con archivos estará estrictamente controlado en el lenguaje MQL5. Los archivos con los que se realizan operaciones de archivos usando el lenguaje MQL5 no se podrán ubicar fuera del archivo del sandbox.
Los archivos de registro escritos en el disco por los programas MQL5 se encontrarán en la carpeta %MQL5\Logs. Por suerte, podremos usar WinAPI, que contiene directamente operaciones con archivos.
WinAPI se conecta usando la siguiente directiva:
#include <WinAPI\winapi.mqh>
En la WinAPI del archivo, usaremos ocho funciones:
- FindFirstFileW(),
- FindNextFileW(),
- CopyFileW(),
- GetFileAttributesW(),
- SetFileAttributesW(),
- DeleteFileW(),
- FindClose(),
- GetLastError().
La primera función buscará en la carpeta especificada el primer archivo con el nombre dado. Podremos sustituir el nombre por una máscara. Entonces, para encontrar los archivos de registro en una carpeta, bastará con especificar como nombre la siguiente cadena ".log".
La segunda función proseguirá la búsqueda iniciada por la primera función.
La tercera función copiará un archivo existente a un nuevo archivo.
La cuarta función obtendrá los atributos del sistema de archivos para el archivo o directorio especificado.
La quinta función establecerá dichos atributos.
La sexta función eliminará el archivo con el nombre dado.
La séptima función cerrará el descriptor de búsqueda de archivos.
La octava función recuperará el valor del código del último error.
Veamos el código de servicio dClearTradeLogs.mq5.
//--- include #include <WinAPI\winapi.mqh> #include "Include\CisNewBar.mqh" //--- defines #define ERROR_FILE_NOT_FOUND 0x2 #define ERROR_NO_MORE_FILES 0x12 #define INVALID_FILE_ATTRIBUTES 0xFFFFFFFF #define FILE_ATTRIBUTE_READONLY 0x1 #define FILE_ATTRIBUTE_DIRECTORY 0x10 #define FILE_ATTRIBUTE_ARCHIVE 0x20 //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input string InpDstPath="G:" ; // Destination drive //+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS)); //--- new bar CisNewBar daily_new_bar; daily_new_bar.SetPeriod(PERIOD_D1); daily_new_bar.SetLastBarTime(1); //--- logs path string logs_path=::TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Logs\\"; string mask_path=logs_path+"*.log"; //--- destination folder (if to copy files) string new_folder_name=NULL; uint file_attributes=0; if(::StringLen(InpDstPath)>0) { new_folder_name=InpDstPath+"\\Logs"; //--- check whether a folder exists file_attributes=kernel32::GetFileAttributesW(new_folder_name); bool does_folder_exist=(file_attributes != INVALID_FILE_ATTRIBUTES) && ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0); if(!does_folder_exist) { //--- create a folder int create_res=kernel32::CreateDirectoryW(new_folder_name, 0); if(create_res<1) { ::PrintFormat("Failed CreateDirectoryW() with error: %x", kernel32::GetLastError()); return; } } } //--- main processing loop do { MqlDateTime sToday; ::TimeTradeServer(sToday); sToday.hour=sToday.min=sToday.sec=0; datetime dtToday=::StructToTime(sToday); if(daily_new_bar.isNewBar(dtToday)) { ::PrintFormat("\nToday is: %s", ::TimeToString(dtToday, TIME_DATE)); string todays_log_file_name=::TimeToString(dtToday, TIME_DATE); int replaced=::StringReplace(todays_log_file_name, ".", ""); if(replaced>0) { todays_log_file_name+=".log"; //--- log files FIND_DATAW find_file_data; ::ZeroMemory(find_file_data); HANDLE hFind=kernel32::FindFirstFileW(mask_path, find_file_data); if(hFind==INVALID_HANDLE) { ::PrintFormat("Failed FindFirstFile (hFind) with error: %x", kernel32::GetLastError()); continue; } // List all the files in the directory with some info about them int result=0; uint files_cnt=0; do { string name=""; for(int i=0; i<MAX_PATH; i++) name+=::ShortToString(find_file_data.cFileName[i]); //--- delete any file except today's if(::StringCompare(name, todays_log_file_name)) { string file_name=logs_path+name; //--- if to copy a file before deletion if(::StringLen(new_folder_name)>0) { string new_file_name=new_folder_name+"\\"+name; if(kernel32::CopyFileW(file_name, new_file_name, 0)==0) { ::PrintFormat("Failed CopyFileW() with error: %x", kernel32::GetLastError()); } //--- set READONLY attribute file_attributes=kernel32::GetFileAttributesW(new_file_name); if(file_attributes!=INVALID_FILE_ATTRIBUTES) if(!(file_attributes & FILE_ATTRIBUTE_READONLY)) { file_attributes=kernel32::SetFileAttributesW(new_file_name, file_attributes|FILE_ATTRIBUTE_READONLY); if(!(file_attributes & FILE_ATTRIBUTE_READONLY)) ::PrintFormat("Failed SetFileAttributesW() with error: %x", kernel32::GetLastError()); } } int del_ret=kernel32::DeleteFileW(file_name); if(del_ret>0) files_cnt++; } //--- next file ::ZeroMemory(find_file_data); result= kernel32::FindNextFileW(hFind, find_file_data); } while(result!=0); uint kernel32_last_error=kernel32::GetLastError(); if(kernel32_last_error>0) if(kernel32_last_error!=ERROR_NO_MORE_FILES) ::PrintFormat("Failed FindNextFileW (hFind) with error: %x", kernel32_last_error); ::PrintFormat("Deleted log files: %I32u", files_cnt); int file_close=kernel32::FindClose(hFind); } } ::Sleep(15000); } while(!::IsStopped()); now=::TimeLocal(); ::PrintFormat("Service \"%s\" stops at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS)); } //+------------------------------------------------------------------+
Si en la variable input se establece el disco donde se copiarán los archivos, crearemos una carpeta para almacenar los archivos de registro, verificando previamente la existencia de esta carpeta.
En el ciclo de procesamiento principal, primero comprobaremos si ha aparecido un nuevo día. Luego, también en el ciclo, buscaremos y eliminaremos los archivos de registro, además,omitiendo el archivo de hoy. Si necesitamos copiar un archivo, marcaremos esta posibilidad, y después de copiar, configuraremos el atributo "Solo lectura" para el nuevo archivo.
Luego estableceremos en el ciclo una pausa con una duración de 15 segundos. Esta es probablemente una frecuencia relativamente óptima para determinar un nuevo día.
Entonces, antes de iniciar el servicio, la carpeta %MQL5\Logs se vería así en el Explorador (Fig.2).
Fig.2 Carpeta del explorador "%MQL5\Logs" antes de eliminar los archivos
Después de iniciar el servicio, aparecerán los siguientes mensajes en el diario de registro:
2022.12.05 23:26:59.960 dClearTradeLogs Service "dClearTradeLogs" starts at: 2022.12.05 23:26:59 2022.12.05 23:26:59.960 dClearTradeLogs 2022.12.05 23:26:59.960 dClearTradeLogs Today is: 2022.12.05 2022.12.05 23:26:59.985 dClearTradeLogs Deleted log files: 6
Resulta fácil ver que el servicio no ha escrito nada en el registro sobre la finalización de su trabajo. El problema es que el servicio aún no ha terminado, simplemente se está repitiendo y se ejecutará hasta que se interrumpa.
Fig.3 Carpeta del explorador "%MQL5\Logs" después de eliminar los archivos
Entonces, tras eliminar los logs, solo quedará un archivo en la carpeta especificada (Fig. 3). Naturalmente, también podemos mejorar la eliminación de los archivos y hacerla más flexible. Por ejemplo, antes de eliminar los archivos, podemos copiarlos en otro disco para no perder la información necesaria. En general, la implementación dependerá ya de los requisitos específicos del algoritmo. En el ejemplo actual, los archivos se han copiado en dicha carpeta G:\Logs (Fig.4).
Fig.4 Carpeta del explorador "G:\Logs" después de copiar los archivos
Con esto, damos por concluido el trabajo con los logs. En el siguiente ejemplo, asignaremos al servicio la tarea de mostrar gráficos (charts).
4.2 Gestión de gráficos
Vamos a imaginar que nos encontramos ante la siguiente tarea. Es necesario que los gráficos de aquellos símbolos que se comercien estén abiertos en el terminal, es decir, donde hay posiciones.
Las reglas para abrir los gráficos son muy simples. Si hay una posición abierta para uno de los símbolos, abriremos el gráfico de este símbolo. Si no hay posición, no habrá gráfico. Si hay varias posiciones para un símbolo, solo abriremos un gráfico,
y añadiremos algunos colores más. Si la posición es rentable, el color de fondo del gráfico será azul claro y, si no es rentable, será rosa claro. El beneficio cero se indicará con el color lavanda.
Entonces, para realizar esta tarea, en primer lugar, en el código de servicio, necesitaremos un ciclo en el que monitorearemos el estado de las posiciones y los gráficos. El ciclo ha resultado bastante grande, así que analizaremos su código bloque por bloque.
El ciclo se dividirá en dos bloques.
El primer bloque será el procesamiento de la situación en la que no hay posiciones:
int positions_num=::PositionsTotal(); //--- if there are no positions if(positions_num<1) { // close all the charts CChart temp_chart_obj; temp_chart_obj.FirstChart(); long temp_ch_id=temp_chart_obj.ChartId(); for(int ch_idx=0; ch_idx<MAX_CHARTS && temp_ch_id>-1; ch_idx++) { long ch_id_to_close=temp_ch_id; temp_chart_obj.NextChart(); temp_ch_id=temp_chart_obj.ChartId(); ::ChartClose(ch_id_to_close); } }
En el bloque, deberemos revisar los gráficos abiertos, si los hay, y simplemente cerrarlos. Aquí y más abajo, la clase CChart se usará para trabajar con las propiedades del gráfico de precios.
El segundo bloque será más complejo:
//--- if there are some positions else { //--- collect unique position symbols CHashSet<string> pos_symbols_set; for(int pos_idx=0; pos_idx<positions_num; pos_idx++) { string curr_pos_symbol=::PositionGetSymbol(pos_idx); if(!pos_symbols_set.Contains(curr_pos_symbol)) { if(!pos_symbols_set.Add(curr_pos_symbol)) ::PrintFormat("Failed to add a symbol \"%s\" to the positions set!", curr_pos_symbol); } } string pos_symbols_arr[]; int unique_pos_symbols_num=pos_symbols_set.Count(); if(pos_symbols_set.CopyTo(pos_symbols_arr)!=unique_pos_symbols_num) continue; //--- collect unique chart symbols and close duplicates CHashMap<string, long> ch_symbols_map; CChart map_chart_obj; map_chart_obj.FirstChart(); long map_ch_id=map_chart_obj.ChartId(); for(int ch_idx=0; ch_idx<MAX_CHARTS && map_ch_id>-1; ch_idx++) { string curr_ch_symbol=map_chart_obj.Symbol(); long ch_id_to_close=0; if(!ch_symbols_map.ContainsKey(curr_ch_symbol)) { if(!ch_symbols_map.Add(curr_ch_symbol, map_ch_id)) ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_ch_symbol); } else { //--- if there's a duplicate ch_id_to_close=map_chart_obj.ChartId(); } //--- move to the next chart map_chart_obj.NextChart(); map_ch_id=map_chart_obj.ChartId(); if(ch_id_to_close>0) { ::ChartClose(ch_id_to_close); } } map_chart_obj.Detach(); //--- looking for a chart if there's a position for(int s_pos_idx=0; s_pos_idx<unique_pos_symbols_num; s_pos_idx++) { string curr_pos_symbol=pos_symbols_arr[s_pos_idx]; //--- if there's no chart of the symbol if(!ch_symbols_map.ContainsKey(curr_pos_symbol)) if(::SymbolSelect(curr_pos_symbol, true)) { //--- open a chart of the symbol CChart temp_chart_obj; long temp_ch_id=temp_chart_obj.Open(curr_pos_symbol, PERIOD_H1); if(temp_ch_id<1) ::PrintFormat("Failed to open a chart of the symbol \"%s\"!", curr_pos_symbol); else { if(!ch_symbols_map.Add(curr_pos_symbol, temp_ch_id)) ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_pos_symbol); temp_chart_obj.Detach(); } } } string ch_symbols_arr[]; long ch_ids_arr[]; int unique_ch_symbols_num=ch_symbols_map.Count(); if(ch_symbols_map.CopyTo(ch_symbols_arr, ch_ids_arr)!=unique_ch_symbols_num) continue; //--- looking for a position if there's a chart for(int s_ch_idx=0; s_ch_idx<unique_ch_symbols_num; s_ch_idx++) { string curr_ch_symbol=ch_symbols_arr[s_ch_idx]; long ch_id_to_close=ch_ids_arr[s_ch_idx]; CChart temp_chart_obj; temp_chart_obj.Attach(ch_id_to_close); //--- if there's no position of the symbol if(!pos_symbols_set.Contains(curr_ch_symbol)) { temp_chart_obj.Close(); } else { CPositionInfo curr_pos_info; //--- calculate a position profit double curr_pos_profit=0.; int pos_num=::PositionsTotal(); for(int pos_idx=0; pos_idx<pos_num; pos_idx++) if(curr_pos_info.SelectByIndex(pos_idx)) { string curr_pos_symbol=curr_pos_info.Symbol(); if(!::StringCompare(curr_ch_symbol, curr_pos_symbol)) curr_pos_profit+=curr_pos_info.Profit()+curr_pos_info.Swap(); } //--- apply a color color profit_clr=clrLavender; if(curr_pos_profit>0.) { profit_clr=clrLightSkyBlue; } else if(curr_pos_profit<0.) { profit_clr=clrLightPink; } if(!temp_chart_obj.ColorBackground(profit_clr)) ::PrintFormat("Failed to apply a profit color for the symbol \"%s\"!", curr_ch_symbol); temp_chart_obj.Redraw(); } temp_chart_obj.Detach(); } //--- tile windows (Alt+R) uchar vk=VK_MENU; uchar scan=0; uint flags[]= {0, KEYEVENTF_KEYUP}; ulong extra_info=0; uchar Key='R'; for(int r_idx=0; r_idx<2; r_idx++) { user32::keybd_event(vk, scan, flags[r_idx], extra_info); ::Sleep(10); user32::keybd_event(Key, scan, flags[r_idx], extra_info); } }
Primero, recopilaremos los valores únicos de los símboloscuyas posiciones estén abiertas. Destacaremos que para esta tarea resultan adecuadas las capacidades de la clase CHashSet<T>, que es la implementación de un conjunto de datos dinámicos desordenados de tipo T, sujeta al requisito de que cada valor sea único. Después copiaremos los valores únicos obtenidos en un array de líneas para tener un acceso simplificado a ellas más adelante.
En la siguiente etapa, recopilaremos los valores únicos de los símbolos cuyos gráficos están abiertos, cerrando ya de paso los gráficos duplicados, si los hubiera. Supongamos que hay abiertos 2 gráficos de EURUSD. Luego dejaremos solo un gráfico y cerraremos el otro. Aquí ya se implicará un ejemplar de la clase CHashMap<TKey,TValue>, que es la implementación de una tabla hash dinámica cuyos datos se almacenan como pares clave-valor no ordenados, cumpliendo el requisito de que la clave sea única.
Ahora solo queda resolver dos ciclos. En el primero, iteraremos por el array de símbolos de las posiciones abiertas y comprobaremos si hay un gráfico para él. Si no existe, lo abriremos. En el segundo ciclo, iteraremos por el array de símbolos de los gráficos abiertos y comprobaremos si la posición abierta corresponde a cada símbolo. Digamos que el gráfico está abierto para el símbolo USDJPY, pero no hay ninguna posición para él. Luego se cerrará el gráfico USDJPY . En el mismo ciclo, calcularemos el beneficio de la posición para establecer el color de fondo, tal como se determinó al comienzo de la tarea. Para acceder a las propiedades de la posición y obtener sus valores se ha usado la clase de la Biblioteca EstándarCPositionInfo.
Bueno, al final del bloque, daremos algunos retoques: añadiremos las ventanas de los gráficos con un mosaico. Para ello, recurriremos a WinAPI, es decir, precisamente a la funciónkeybd_event() que simula la pulsación de una tecla.
Eso es todo. Solo nos quedará iniciar el servicio dActivePositionsCharts.
4.3 Símbolo personalizado, cotizaciones
Una de las ventajas del servicio es que puede funcionar en segundo plano, sin utilizar el gráfico de precios. Como ejemplo, en esta sección mostraremos cómo se puede usar el servicio para crear un símbolo personalizado y su historia de ticks, además de generar nuevos ticks.
El índice del dólar estadounidense actuará como símbolo personalizado.
4.3.1 Índice del dólar, composición
El índice del dólar estadounidense es un índice sintético que muestra el valor del USD frente a una cesta de otras seis divisas:
- euro (57,6%);
- yen japonés (13,6%);
- libra esterlina (11,9%);
- dólar canadiense (9,1%);
- corona sueca (4,2%);
- franco suizo (3,6%).
La fórmula con la que se calcula el índice es, con un factor de corrección, el promedio geométrico ponderado de las tasas de cambio del dólar frente a estas divisas:
USDX = 50.14348112 * EURUSD-0.576 * USDJPY0.136 * GBPUSD-0.119 * USDCAD0.091 * USDSEK0.042 * USDCHF0.036
Partiendo de la fórmula, digamos que la cotización del par se elevará a la potencia negativa cuando el dólar en la cotización sea la divisa cotizada, y que la cotización del par se elevará a la potencia positiva cuando el dólar en la cotización sea la divisa básica.
La cesta de divisas se puede mostrar esquemáticamente de la siguiente manera (Fig. 5).
Fig.5Cesta de divisas del índice del dólar (DXY)
El índice del dólar estadounidense es el activo básico de los futurosnegociados en la Bolsa Intercontinental (ICE). Los futuros sobre los índices se calculan aproximadamente cada 15 segundos. Los precios para el cálculo se toman al precio Bid más alto y al precio Ask más bajo en la profundidad de mercado del par de divisas incluido en el índice.
4.3.2 Índice del dólar, servicio
Ya tenemos todo lo necesario para los cálculos, así que podemos comenzar a escribir el código del servicio. Pero primero, debemos subrayar que el servicio funcionará por etapas. En la primera etapa formará la historia de ticks y barras para los sintéticos, mientras que en la segunda etapa procesará los nuevos ticks. Obviamente, la primera etapa estará relacionada con el pasado y la segunda, con el presente.
Vamos a crear una plantilla de programa (servicio) MQL5 llamada dDxySymbol.mq5.
Como variables input definiremos las siguientes:
input datetime InpStartDay=D'01.10.2022'; // Start date input int InpDxyDigits=3; // Index digits
La primera definirá el inicio de la historia de cotizaciones que intentaremos conseguir para crear nuestro símbolo, es decir, descargaremos la historia de cotizaciones a partir del 1 de octubre de 2022.
La segunda establecerá la precisión de la cotización del símbolo.
Entonces, para comenzar a trabajar con el índice, deberemos crear un símbolo personalizado: la base para mostrar los sintéticos. DXY será el nombre del símbolo del índice. El recurso tiene muchos materiales sobre símbolos personalizados. Recurriremos a la clase CiCustomSymbol, que se definió en el artículo Recetas MQL5 – Prueba de estrés de una estrategia comercial con ayuda de símbolos personalizados.
Aquí estará el bloque de código donde se implementará la creación de sintéticos de DXY:
//--- create a custom symbol string custom_symbol="DXY", custom_group="Dollar Index"; CiCustomSymbol custom_symbol_obj; const uint batch_size = 1e6; const bool is_selected = true; int code = custom_symbol_obj.Create(custom_symbol, custom_group, NULL, batch_size, is_selected); ::PrintFormat("Custom symbol \"%s\", code: %d", custom_symbol, code); if(code < 0) return;
Destacaremos que si el símbolo DXY no se ha creado antes y no se encuentra en la lista de símbolos personalizados del terminal, el método CiCustomSymbol::Create() retornará el código 1. Si el símbolo DXY ya está entre los símbolos, obtendremos el código 0. Si no es posible crear un símbolo, obtendremos un error, el código -1. Si se produce un error al crear un símbolo personalizado, el servicio finalizará su funcionamiento.
Después de crear el sintético, estableceremos varias propiedades para él.
//--- Integer properties //--- sector ENUM_SYMBOL_SECTOR symbol_sector = SECTOR_CURRENCY; if(!custom_symbol_obj.SetProperty(SYMBOL_SECTOR, symbol_sector)) { ::PrintFormat("Failed to set a sector for the custom symbol \"%s\"", custom_symbol); return; } //--- background color color symbol_background_clr = clrKhaki; if(!custom_symbol_obj.SetProperty(SYMBOL_BACKGROUND_COLOR, symbol_background_clr)) { ::PrintFormat("Failed to set a background color for the custom symbol \"%s\"", custom_symbol); return; } //--- chart mode ENUM_SYMBOL_CHART_MODE symbol_ch_mode=SYMBOL_CHART_MODE_BID; if(!custom_symbol_obj.SetProperty(SYMBOL_CHART_MODE, symbol_ch_mode)) { ::PrintFormat("Failed to set a chart mode for the custom symbol \"%s\"", custom_symbol); return; } //--- digits if(!custom_symbol_obj.SetProperty(SYMBOL_DIGITS, InpDxyDigits)) { ::PrintFormat("Failed to set digits for the custom symbol \"%s\"", custom_symbol); return; } //--- trade mode ENUM_SYMBOL_TRADE_MODE symbol_trade_mode = SYMBOL_TRADE_MODE_DISABLED; if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_MODE, symbol_trade_mode)) { ::PrintFormat("Failed to disable trade for the custom symbol \"%s\"", custom_symbol); return; }
A las propiedades del tipo ENUM_SYMBOL_INFO_INTEGER pertenecerán las siguientes:
- SYMBOL_SECTOR,
- SYMBOL_BACKGROUND_COLOR,
- SYMBOL_CHART_MODE,
- SYMBOL_DIGITS,
- SYMBOL_TRADE_MODE.
La última propiedad será responsable del modo comercial. El sintético se desactivará del comercio, por lo que la propiedad se establecerá en SYMBOL_TRADE_MODE_DISABLED. Si necesitamos verificar alguna estrategia según un símbolo en el Simulador, la propiedad deberá estar habilitada (SYMBOL_TRADE_MODE_FULL).
//--- Double properties //--- point double symbol_point = 1./::MathPow(10, InpDxyDigits); if(!custom_symbol_obj.SetProperty(SYMBOL_POINT, symbol_point)) { ::PrintFormat("Failed to to set a point value for the custom symbol \"%s\"", custom_symbol); return; } //--- tick size double symbol_tick_size = symbol_point; if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_TICK_SIZE, symbol_tick_size)) { ::PrintFormat("Failed to to set a tick size for the custom symbol \"%s\"", custom_symbol); return; }
A las propiedades de tipo ENUM_SYMBOL_INFO_DOUBLE pertenecerán las siguientes:
- SYMBOL_POINT,
- SYMBOL_TRADE_TICK_SIZE.
//--- String properties //--- category string symbol_category="Currency indices"; if(!custom_symbol_obj.SetProperty(SYMBOL_CATEGORY, symbol_category)) { ::PrintFormat("Failed to to set a category for the custom symbol \"%s\"", custom_symbol); return; } //--- country string symbol_country= "US"; if(!custom_symbol_obj.SetProperty(SYMBOL_COUNTRY, symbol_country)) { ::PrintFormat("Failed to to set a country for the custom symbol \"%s\"", custom_symbol); return; } //--- description string symbol_description= "Synthetic US Dollar Index"; if(!custom_symbol_obj.SetProperty(SYMBOL_DESCRIPTION, symbol_description)) { ::PrintFormat("Failed to to set a description for the custom symbol \"%s\"", custom_symbol); return; } //--- exchange string symbol_exchange= "ICE"; if(!custom_symbol_obj.SetProperty(SYMBOL_EXCHANGE, symbol_exchange)) { ::PrintFormat("Failed to to set an exchange for the custom symbol \"%s\"", custom_symbol); return; } //--- page string symbol_page = "https://www.ice.com/forex/usdx"; if(!custom_symbol_obj.SetProperty(SYMBOL_PAGE, symbol_page)) { ::PrintFormat("Failed to to set a page for the custom symbol \"%s\"", custom_symbol); return; } //--- path string symbol_path="Custom\\"+custom_group+"\\"+custom_symbol; if(!custom_symbol_obj.SetProperty(SYMBOL_PATH, symbol_path)) { ::PrintFormat("Failed to to set a path for the custom symbol \"%s\"", custom_symbol); return; }
A las propiedades de tipo ENUM_SYMBOL_INFO_STRING pertenecerán las siguientes:
- SYMBOL_CATEGORY,
- SYMBOL_COUNTRY,
- SYMBOL_DESCRIPTION,
- SYMBOL_EXCHANGE,
- SYMBOL_PAGE,
- SYMBOL_PATH.
La última propiedad será responsable de la ruta en el árbol de símbolos. Incluso al crear un sintético, se ha especificado el grupo de símbolos y el nombre de símbolo. Por lo tanto, esta propiedad no se podrá establecer, será idéntica.
Obviamente, todavía sería posible establecer la fórmula para el sintético directamente y no sufrir la acumulación de ticks. Pero entonces, por un lado, perderíamos el sentido del ejemplo, y por otro lado, el precio del índice se calcularía periódicamente. En el ejemplo actual, el periodo de conteo es de 10 segundos.
Ahora pasaremos al siguiente bloque, a saber, la comprobación de la existencia de una historia comercial. Aquí resolveremos dos tareas: la verificación de la historia de barras y la carga de ticks. Comprobaremos las barras de la siguiente manera:
//--- check quotes history CBaseSymbol base_symbols[BASE_SYMBOLS_NUM]; const string symbol_names[]= { "EURUSD", "USDJPY", "GBPUSD", "USDCAD", "USDSEK", "USDCHF" }; ENUM_TIMEFRAMES curr_tf=PERIOD_M1; ::Print("\nChecking of quotes history is running..."); for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; string curr_symbol_name=symbol_names[s_idx]; if(ptr_base_symbol.Init(curr_symbol_name, curr_tf, InpStartDay)) { ::PrintFormat("\n Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name); ulong start_cnt=::GetTickCount64(); int check_load_code=ptr_base_symbol.CheckLoadHistory(); ::PrintFormat(" Checking code: %I32d", check_load_code); ulong time_elapsed_ms=::GetTickCount64()-start_cnt; ::PrintFormat(" Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC); if(check_load_code<0) { ::PrintFormat("Failed to load quotes history for the symbol \"%s\"", curr_symbol_name); return; } } }
Tendremos 6 símbolos que necesitaremos recorrer, y de los que deberemos procesar sus cotizaciones. Para realizar dicho trabajo de forma más cómoda, hemos creado la clase CBaseSymbol.
//+------------------------------------------------------------------+ //| Class CBaseSymbol | //+------------------------------------------------------------------+ class CBaseSymbol : public CObject { //--- === Data members === --- private: CSymbolInfo m_symbol; ENUM_TIMEFRAMES m_tf; matrix m_ticks_mx; datetime m_start_date; ulong m_last_idx; //--- === Methods === --- public: //--- constructor/destructor void CBaseSymbol(void); void ~CBaseSymbol(void) {}; //--- bool Init(const string _symbol, const ENUM_TIMEFRAMES _tf, datetime start_date); int CheckLoadHistory(void); bool LoadTicks(const datetime _stop_date, const uint _flags); matrix GetTicks(void) const { return m_ticks_mx; }; bool SearchTickLessOrEqual(const double _dbl_time, vector &_res_row); bool CopyLastTick(vector &_res_row); };
La clase se ocupará de la historia de barras y ticks, que es una tarea extremadamente importante, de lo contrario no habrá material para crear sintéticos.
Luego cargaremos los ticks:
//--- try to load ticks ::Print("\nLoading of ticks is running..."); now=::TimeCurrent(); uint flags=COPY_TICKS_INFO | COPY_TICKS_TIME_MS | COPY_TICKS_BID | COPY_TICKS_ASK; double first_tick_dbl_time=0.; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; string curr_symbol_name=symbol_names[s_idx]; ::PrintFormat("\n Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name); ulong start_cnt=::GetTickCount64(); ::ResetLastError(); if(!ptr_base_symbol.LoadTicks(now, flags)) { ::PrintFormat("Failed to load ticks for the symbol \"%s\" , error: %d", curr_symbol_name, ::GetLastError()); return; } ulong time_elapsed_ms=::GetTickCount64()-start_cnt; ::PrintFormat(" Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC); //--- looking for the 1st tick matrix ticks_mx=ptr_base_symbol.GetTicks(); double tick_dbl_time=ticks_mx[0][0]; if(tick_dbl_time>first_tick_dbl_time) first_tick_dbl_time=tick_dbl_time; }
La función nativa matrix::CopyTicksRange() se usaba para cargar los ticks. Es cómoda porque permite cargar solo aquellas columnas en la estructura de ticks que estén definidas por banderas. Y el tema del ahorro de recursos resultará sumamente relevante al solicitar millones de ticks.
COPY_TICKS_INFO = 1, // ticks caused by Bid and/or Ask changes COPY_TICKS_TRADE = 2, // ticks caused by Last and Volume changes COPY_TICKS_ALL = 3, // all ticks that have changes COPY_TICKS_TIME_MS = 1<<8, // time in milliseconds COPY_TICKS_BID = 1<<9, // Bid price COPY_TICKS_ASK = 1<<10, // Ask price COPY_TICKS_LAST = 1<<11, // Last price COPY_TICKS_VOLUME = 1<<12, // volume COPY_TICKS_FLAGS = 1<<13, // tick flags
Las etapas de verificación de la historia y la carga de ticks en el registro se describirán en cuanto al costo de tiempo.
CS 0 12:01:11.802 dDxySymbol Checking of quotes history is running... CS 0 12:01:11.802 dDxySymbol CS 0 12:01:11.802 dDxySymbol Symbol #1: "EURUSD" CS 0 12:01:14.476 dDxySymbol Checking code: 1 CS 0 12:01:14.476 dDxySymbol Time elapsed: 2.688 sec CS 0 12:01:14.476 dDxySymbol CS 0 12:01:14.476 dDxySymbol Symbol #2: "USDJPY" CS 0 12:01:17.148 dDxySymbol Checking code: 1 CS 0 12:01:17.148 dDxySymbol Time elapsed: 2.672 sec CS 0 12:01:17.148 dDxySymbol CS 0 12:01:17.148 dDxySymbol Symbol #3: "GBPUSD" CS 0 12:01:19.068 dDxySymbol Checking code: 1 CS 0 12:01:19.068 dDxySymbol Time elapsed: 1.922 sec CS 0 12:01:19.068 dDxySymbol CS 0 12:01:19.068 dDxySymbol Symbol #4: "USDCAD" CS 0 12:01:21.209 dDxySymbol Checking code: 1 CS 0 12:01:21.209 dDxySymbol Time elapsed: 2.140 sec CS 0 12:01:21.209 dDxySymbol CS 0 12:01:21.209 dDxySymbol Symbol #5: "USDSEK" CS 0 12:01:22.631 dDxySymbol Checking code: 1 CS 0 12:01:22.631 dDxySymbol Time elapsed: 1.422 sec CS 0 12:01:22.631 dDxySymbol CS 0 12:01:22.631 dDxySymbol Symbol #6: "USDCHF" CS 0 12:01:24.162 dDxySymbol Checking code: 1 CS 0 12:01:24.162 dDxySymbol Time elapsed: 1.531 sec CS 0 12:01:24.162 dDxySymbol CS 0 12:01:24.162 dDxySymbol Loading of ticks is running... CS 0 12:01:24.162 dDxySymbol CS 0 12:01:24.162 dDxySymbol Symbol #1: "EURUSD" CS 0 12:02:27.204 dDxySymbol Time elapsed: 63.032 sec CS 0 12:02:27.492 dDxySymbol CS 0 12:02:27.492 dDxySymbol Symbol #2: "USDJPY" CS 0 12:02:32.587 dDxySymbol Time elapsed: 5.094 sec CS 0 12:02:32.938 dDxySymbol CS 0 12:02:32.938 dDxySymbol Symbol #3: "GBPUSD" CS 0 12:02:37.675 dDxySymbol Time elapsed: 4.734 sec CS 0 12:02:38.285 dDxySymbol CS 0 12:02:38.285 dDxySymbol Symbol #4: "USDCAD" CS 0 12:02:43.223 dDxySymbol Time elapsed: 4.937 sec CS 0 12:02:43.624 dDxySymbol CS 0 12:02:43.624 dDxySymbol Symbol #5: "USDSEK" CS 0 12:03:18.484 dDxySymbol Time elapsed: 34.860 sec CS 0 12:03:19.596 dDxySymbol CS 0 12:03:19.596 dDxySymbol Symbol #6: "USDCHF" CS 0 12:03:24.317 dDxySymbol Time elapsed: 4.719 sec
Después de obtener los ticks, formaremos una historia de ticks para el DXY sintético. Este proceso se realizará en el siguiente bloque:
//--- create a custom symbol ticks history ::Print("\nCustom symbol ticks history is being formed..."); long first_tick_time_sec=(long)(first_tick_dbl_time/MS_IN_SEC); long first_tick_time_ms=(long)first_tick_dbl_time%(long)MS_IN_SEC; ::PrintFormat(" First tick time: %s.%d", ::TimeToString((datetime)first_tick_time_sec, TIME_DATE|TIME_SECONDS), first_tick_time_ms); double active_tick_dbl_time=first_tick_dbl_time; double now_dbl_time=MS_IN_SEC*now; uint ticks_cnt=0; uint arr_size=0.5e8; MqlTick ticks_arr[]; ::ArrayResize(ticks_arr, arr_size); ::ZeroMemory(ticks_arr); matrix base_prices_mx=matrix::Zeros(BASE_SYMBOLS_NUM, 2); do { //--- collect base symbols ticks bool all_ticks_ok=true; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; vector tick_prices_vc; bool to_break_loop=false; if(!ptr_base_symbol.SearchTickLessOrEqual(active_tick_dbl_time, tick_prices_vc)) to_break_loop=true; else { if(!base_prices_mx.Row(tick_prices_vc, s_idx)) to_break_loop=true; } if(to_break_loop) { all_ticks_ok=false; break; } } //--- calculate index prices if(all_ticks_ok) { MqlTick last_ind_tick; CalcIndexPrices(active_tick_dbl_time, base_prices_mx, last_ind_tick); arr_size=ticks_arr.Size(); if(ticks_cnt>=arr_size) { uint new_size=(uint)(arr_size+0.1*arr_size); if(::ArrayResize(ticks_arr, new_size)!=new_size) continue; } ticks_arr[ticks_cnt]=last_ind_tick; ticks_cnt++; } active_tick_dbl_time+=TICK_PAUSE; } while(active_tick_dbl_time<now_dbl_time); ::ArrayResize(ticks_arr, ticks_cnt); int ticks_replaced=custom_symbol_obj.TicksReplace(ticks_arr, true);
Luego estableceremos el punto temporal (active_tick_dbl_time) al que, al final del ciclo, añadiremos 10 segundos. Esta es una especie de marca temporal (time stamp) para obtener las marcas para todos los símbolos que componen el Índice.
Por consiguiente, la búsqueda del tick deseado en cada símbolo se basará en un punto específico en el pasado. El método CBaseSymbol::SearchTickLessOrEqual() retornará el tick que ha llegado no más tarde que el valor active_tick_dbl_time.
Cuando obtengamos los ticks de cada componente del Índice, los precios de los ticks ya estarán en el array base_prices_mx.
La función CalcIndexPrices() retornará el valor listo del tick del índice en un momento temporal.
Cuando los ticks hayan sido creados, la base de datos de ticks se actualizará utilizando el método CiCustomSymbol::TicksReplace().
Con esto damos por completado el trabajo con el pasado. En el siguiente bloque, el servicio solo se ocupará entonces del presente:
//--- main processing loop ::Print("\nA new tick processing is active..."); do { ::ZeroMemory(base_prices_mx); //--- collect base symbols ticks bool all_ticks_ok=true; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; vector tick_prices_vc; bool to_break_loop=false; if(!ptr_base_symbol.CopyLastTick(tick_prices_vc)) to_break_loop=true; else { if(!base_prices_mx.Row(tick_prices_vc, s_idx)) to_break_loop=true; } if(to_break_loop) { all_ticks_ok=false; break; } } //--- calculate index prices if(all_ticks_ok) { MqlTick last_ind_tick, ticks_to_add[1]; now=::TimeCurrent(); now_dbl_time=MS_IN_SEC*now; CalcIndexPrices(now_dbl_time, base_prices_mx, last_ind_tick); ticks_to_add[0]=last_ind_tick; int ticks_added=custom_symbol_obj.TicksAdd(ticks_to_add, true); } ::Sleep(TICK_PAUSE); } while(!::IsStopped());
La tarea del bloque será similar a la del bloque anterior, solo que un poco más sencilla. Cada 10 segundos, deberá obtener los datos de ticks en los símbolos y calcular los precios del índice. Querríamos señalar que en el Índice, el precio Bid se calculará según los precios Bid de todos los símbolos, mientras que el precio Ask se calculará según los precios Ask de todos los símbolos, respectivamente.
Después de iniciar el servicio dDxySymbol, tras un tiempo, podremos abrir el gráfico del símbolo DXY personalizado (Fig. 6).
Fig.6 Gráfico del símbolo personalizado DXY con días festivos
En el gráfico, los sábados están resaltados con segmentos verticales rojos. Resulta que en la historia de sábados y domingos, el servicio continúa calculando los ticks, lo cual probablemente no sea del todo correcto. Deberemos complementar el código de servicio con un límite de tiempo (días de la semana). Asignaremos esta tarea a la función CheckDayOfWeek().
Ahora el gráfico sintético se verá así (Fig. 7). Parece que el error ha sido solucionado.
Fig.7 Gráfico del símbolo personalizado DXY sin días festivos
Con esto daremos por completado el trabajo con el serviciodDxySymbol.
Conclusión
El artículo ha presentado algunas características de un programa mql5 como servicio. Este tipo de programas mql5 se distingue en que no tiene un gráfico vinculante, sino que funciona de forma independiente. Enfatizaremos que la naturaleza de los servicios es tal que pueden entrar en conflicto con otros asesores expertos, scripts y probablemente, en menor medida, con indicadores. Por lo tanto, la delimitación de los derechos y obligaciones de los programas de servicio en el entorno MetaTrader5 recaerá sobre los hombros del desarrollador.
El archivo contiene fuentes que se podrán colocar en la carpeta %MQL5\Services.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/11826
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso