English Русский 中文 Deutsch 日本語 Português
preview
Recetas MQL5 - Servicios

Recetas MQL5 - Servicios

MetaTrader 5Ejemplos | 22 marzo 2023, 15:23
622 0
Denis Kirichenko
Denis Kirichenko

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:

El servicio de Windows (eng. Windows Service) es una aplicación ejecutada automáticamente (si está configurada para ello) por el sistema al iniciarse el sistema operativo Windowsy se ejecuta independientemente del estado del usuario. Tiene algo en común con el concepto daemons en Unix.

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:

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).


Servicio "dEmpty"

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:

  1. FindFirstFileW(),
  2. FindNextFileW(),
  3. CopyFileW(),
  4. GetFileAttributesW(),
  5. SetFileAttributesW(),
  6. DeleteFileW(),
  7. FindClose(),
  8. 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).

Carpeta del explorador "%MQL5\Logs" antes de eliminar los archivos

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.

Carpeta del explorador "%MQL5\Logs" después de eliminar los archivos

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).

Carpeta del explorador "G:\Logs" después de copiar los archivos

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:

  1. euro (57,6%);
  2. yen japonés (13,6%);
  3. libra esterlina (11,9%);
  4. dólar canadiense (9,1%);
  5. corona sueca (4,2%);
  6. 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).



Cesta de divisas del índice dólar (DXY)

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.
Como hemos determinado previamente que el símbolo no será comercial, existen pocas propiedades double.

//--- 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

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.

Gráfico del símbolo personalizado DXY sin días festivos

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

Archivos adjuntos |
code.zip (23.7 KB)
DoEasy. Elementos de control (Parte 29): Control auxiliar "ScrollBar" DoEasy. Elementos de control (Parte 29): Control auxiliar "ScrollBar"
En este artículo, comenzaremos a desarrollar el control auxiliar ScrollBar y sus objetos derivados: las barras de desplazamiento verticales y horizontales. ScrollBar (barra de desplazamiento) se usa para desplazar el contenido del formulario si va más allá del contenedor. Por lo general, las barras de desplazamiento se encuentran en la parte inferior y derecha del formulario. La barra horizontal en la parte inferior desplaza el contenido hacia la izquierda y hacia la derecha, mientras que la barra vertical desplaza el contenido hacia arriba y hacia abajo.
Algoritmos de optimización de la población: Algoritmo de optimización de cuco (Cuckoo Optimization Algorithm — COA) Algoritmos de optimización de la población: Algoritmo de optimización de cuco (Cuckoo Optimization Algorithm — COA)
El siguiente algoritmo que analizaremos será la optimización de la búsqueda de cuco usando los vuelos de Levy. Este es uno de los últimos algoritmos de optimización, así como el nuevo líder en la clasificación.
Algoritmos de optimización de la población: Búsqueda de bancos de peces (Fish School Search — FSS) Algoritmos de optimización de la población: Búsqueda de bancos de peces (Fish School Search — FSS)
La búsqueda de bancos de peces (FSS) es un nuevo algoritmo de optimización moderno inspirado en el comportamiento de los peces en un banco, la mayoría de los cuales, hasta el 80%, nadan en una comunidad organizada de parientes. Se ha demostrado que las asociaciones de peces juegan un papel importante a la hora de buscar alimento y protegerse contra los depredadores de forma eficiente.
DoEasy. Elementos de control (Parte 28): Estilos de barra en el control «ProgressBar» DoEasy. Elementos de control (Parte 28): Estilos de barra en el control «ProgressBar»
El artículo desarrollará los estilos de visualización y el texto de descripción para la barra de progreso del control ProgressBar.