Organización de acceso a los datos

En esta sección vamos a estudiar las cuestiones relacionadas con la obtención, almacenamiento y solicitud de datos de precios (series temporales).

Recepción de datos del servidor comercial

Antes de que los datos de precios estén disponibles en el terminal MetaTrader 5, es necesario obtener y procesarlos. Para recibir los datos hay que conectarse al terminal comercial MetaTrader 5. Los datos llegan del servidor a petición del terminal en forma de unos bloques de barras de un minuto bien empaquetados.

El mecanismo de dirigirse al servidor para obtener los datos no depende del modo de presentar la solicitud, sea por el usuario navegando por el gráfico o sea de modo de programación en el lenguaje MQL5.

Almacenamiento de datos intermedios

Los datos recibidos del servidor se despaquetan automáticamente y se guardan en el formato intermedio especial HCC. Los datos de cada símbolo se colocan en una carpeta individual directorio_de_terminal\bases\nombre_de_servidor\history\nombre_de_símbolo. Por ejemplo, los datos del símbolo EURUSD recibidos del servidor comercial MetaQuotes-Demo estarán guardados en la carpeta directorio_de_terminal\bases\MetaQuotes-Demo\history\EURUSD\.

Los datos se escriben en los archivos con la extensión .hcc, cada archivo almacena datos de barras de minutos de un año. Por ejemplo, el archivo 2009.hcc de la carpeta EURUSD contiene datos de minutos del símbolo EURUSD del año 2009. Estos archivos se usan para la preparación de datos de precios de todos los períodos y no están destinados para el acceso directo.

Obtención de datos de un período necesario desde los datos intermedios

Los archivos auxiliares en el formato HCC desempeñan el papel de la fuente de información a la hora de construir los datos de precios para los períodos de tiempo solicitados en el formato HC. Los datos en el formato HC son series temporales que están preparadas al máximo para el acceso rápido. Se crean únicamente a la petición de un gráfico o un programa mql5 en el volumen que no supera el valor del parámetro "Max bars in charts" y se guardan en los archivos con la extensión hc para su posterior uso.

Para ahorrar los recursos, los datos relacionados con un período de tiempo se cargan y se almacenan en la memoria operativa sólo si es preciso, en caso si éstos no son requeridos durante un plazo de tiempo considerable se realiza su descarga de la memoria operativa y ellos se guardan en el archivo. Para cada período de tiempo los datos se preparan independientemente de la presencia de datos ya preparados para otros períodos. Las reglas de formación y accesibilidad de datos son iguales para todos los períodos de tiempo. Es decir, a pesar de que la unidad de almacenamiento de datos en el formato HCC sea una barra de un minuto, la disponibilidad de datos en el formato HCC no significa la disponibilidad y accesibilidad de datos del período M1 con el formato HC en el mismo volumen.

La recepción de nuevos datos desde el servidor provoca la renovación automática de datos de precios utilizados en el formato HC de todos los períodos. Esto también lleva al recálculo de todos los indicadores que los usan implícitamente como datos de entrada para el cálculo.

Parámetro "Max bars in chart"

El parámetro "Max bars in charts" limita la cantidad de barras en el formato HC disponible para los gráficos, indicadores y programas mql5. Esta limitación concierne a los datos de todos los períodos de tiempo, y sirve principalmente para ahorrar recursos del ordenador.

Al establecer los valores altos del dicho parámetro, tenemos que recordar que tratándose de un historial bastante profundo de datos de precios para los períodos temporales menores, el consumo de memoria para almacenar series temporales y buffers de indicadores puede ser de centenares de megabytes, llegando al límite de memoria operativa para el terminal de cliente (2GB para las aplicaciones MS Windows de 32 bits).

Los cambios del parámetro "Max bars in charts" tendrán su efecto una vez reiniciado el terminal de cliente. De por sí el cambio de dicho parámetro no implica ni la llamada automática al servidor a por los datos adicionales, ni la formación de barras adicionales de una serie temporal. La solicitud de datos de precios adicionales y la renovación de series temporales se hacen en caso de desplazar el gráfico en la zona de datos que faltan, o bien, solicitando los datos que faltan desde un programa mql5.

El volumen de datos solicitados al servidor corresponde a la cantidad requerida de barras de dicho período, teniendo en cuenta el valor del parámetro "Max bars in charts". La limitación puesta por el parámetro no es tan rigurosa, y en algunas ocasiones el número de barras disponibles en el período temporal puede superar insignificadamente el valor del parámetro corriente.

Disponibilidad de datos

La presencia de datos en el formato HCC, o incluso en el formato HC listo para utilizarse, no siempre supone la disponibilidad absoluta de estos datos para ser mostrados en un gráfico o para utilizarlos en programas mql5.

A la hora de acceder a los datos de precios o a los valores de indicadores desde los programas mql5, hay que recordar que su disponibilidad en un momento dado o desde un momento dado no está garantizada. Esto está relacionado con lo siguiente: con el fin de ahorrar los recursos en MetaTrader 5, no se almacena una copia completa de datos requeridos para un programa mql5, sino se proporciona un acceso directo a la base de datos del terminal.

El historial de precios para todos los períodos temporales se forma de datos comunes en el formato HCC y cualquier renovación de datos desde el servidor supone la renovación de datos para todos los períodos temporales y recálculo de indicadores. A consecuencia de esto, el acceso a los datos puede estar bloqueado, incluso si éstos estaban disponibles hace un momento.

Sincronización de datos del terminal y datos del servidor #

Puesto que un programa mql5 puede dirigirse a los datos por cualquier símbolo y período de tiempo, cabe posibilidad que los datos de la serie temporal requerida todavía no están formados en el terminal o los datos de precios requeridos no están sincronizados con el servidor comercial. En este caso es muy complicado pronosticar el tiempo de espera.

Los algoritmos que usan los ciclos de latencia no es la mejor solución. La única excepción en este caso son los scripts, puesto que ellos no tienen otra elección de algoritmo debido a no disponer del procesamiento de eventos. Para los indicadores personalizados dichos algoritmos, así como otros ciclos de latencia, no se recomiendan en absoluto porque llevan a parar el cálculo de todos los indicadores y otro procesamiento de datos de precios para dicho símbolo.

Para los Asesores Expertos e indicadores personalizados es mejor usar el modelo de eventos de procesamiento. Si durante el manejo del evento OnTick() o OnCalculate() no hemos llegado a obtener todos los datos necesarios de la serie temporal requerida, hay que salir del manejador de eventos, esperando que, al invocar el manejador la próxima vez, obtengamos el acceso a los datos.

Ejemplo de un script para descargar el historial

Vamos a ver un ejemplo - un script ejecuta la solicitud de recibir el historial acerca de un instrumento especificado desde el servidor comercial. Este script sirve para inicializar el instrumento necesario en el gráfico, el período de tiempo no importa, puesto que, y como se decía antes, los datos de precios llegan del servidor en forma de unos datos de minutos empaquetados, de los cuales, luego, se construye una serie temporal predeterminada.

Vamos a organizar todas las acciones de recepción de datos a través de una función independiente CheckLoadHistory(symbol, timeframe, start_date):

int CheckLoadHistory(string symbol,ENUM_TIMEFRAMES period,datetime start_date)
  {
  }

La función CheckLoadHistory() está pensada como una función universal a la que se puede llamar desde un programa cualquiera (Asesor Experto, script o indicador), por tanto hace falta tres parámetros de entrada: nombre del símbolo, período y fecha de inicio a partir de la cual necesitamos el historial de precios.

Vamos a insertar en el código de la función todas las comprobaciones necesarias antes de solicitar el historial que nos falta.  Antes de toda hay que asegurarse que el nombre del símbolo y valor del período son correctos:

   if(symbol==NULL || symbol==""symbol=Symbol();
   if(period==PERIOD_CURRENT)     period=Period();

En el siguiente paso nos persuadimos de que el símbolo especificado esté disponible en la ventana MarketWatch, es decir, el historial para este símbolo va a estar disponible cuando se presenta la solicitud al servidor comercial. Si éste no se encuentra en dicha ventana, hay que añadirlo usando la función SymbolSelect().

   if(!SymbolInfoInteger(symbol,SYMBOL_SELECT))
     {
      if(GetLastError()==ERR_MARKET_UNKNOWN_SYMBOLreturn(-1);
      SymbolSelect(symbol,true);
     }

Ahora hace falta recibir la fecha de inicio del historial ya disponible para el par símbolo/período especificado. Es posible que el valor del parámetro de entrada startdate, pasado a la función CheckLoadHistory(), entre en el intervalo del historial ya disponible, entonces no hace falta presentar ninguna solicitud al servidor comercial. En este momento la función SeriesInfoInteger() con el modificador SERIES_FIRSTDATE sirve para obtener la primera fecha para el símbolo/período.

   SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date);
   if(first_date>0 && first_date<=start_date) return(1);

Otra verificación importante es la comprobación del tipo de programa desde el cual la función es invocada. Acordemos que el envío de la solicitud para actualizar la serie temporal con el mismo período que tiene el indicador que llama esta actualización es muy indeseable. Esta indeseabilidad está condicionada con el hecho de que la actualización de datos históricos se realice en el mismo hilo en el que opera el indicador. Por eso la posibilidad de clinch es alta. Para la comprobación vamos a usar la función MQL5InfoInteger() con el modificador MQL5_PROGRAM_TYPE.

   if(MQL5InfoInteger(MQL5_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol)
      return(-4);

Si hemos pasado todas las comprobaciones con éxito, vamos a hacer el último intento de evitar acudir al servidor comercial. Primero averiguaremos la fecha de inicio para la que estén disponibles los datos de minuto en el formato HCC. Vamos a solicitar este valor usando la función SeriesInfoInteger() con el modificador SERIES_TERMINAL_FIRSTDATE, y volvemos a compararlo con el valor del parámetro start_date.

   if(SeriesInfoInteger(symbol,PERIOD_M1,SERIES_TERMINAL_FIRSTDATE,first_date))
     {
      //--- there is loaded data to build timeseries
      if(first_date>0)
        {
         //--- force timeseries build
         CopyTime(symbol,period,first_date+PeriodSeconds(period),1,times);
         //--- check date
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            if(first_date>0 && first_date<=start_date) return(2);
        }
     }

Si después de todas las comprobaciones el hilo de ejecución sigue estando en el cuerpo de la función CheckLoadHistory(), esto quiere decir que hay necesidad de solicitar los datos de precios que faltan al servidor comercial. Para empezar averiguaremos el valor "Max bars in chart" usando la función TerminalInfoInteger():

  int max_bars=TerminalInfoInteger(TERMINAL_MAXBARS);

Lo vamos a necesitar para no solicitar los datos de más. Luego averiguaremos la primera fecha en el historial del símbolo en el servidor comercial (sin tener en cuenta el período) mediante la función ya conocida SeriesInfoInteger() con el modificador SERIES_SERVER_FIRSTDATE.

   datetime first_server_date=0;
   while(!SeriesInfoInteger(symbol,PERIOD_M1,SERIES_SERVER_FIRSTDATE,first_server_date) && !IsStopped())
      Sleep(5);

Puesto que la solicitud es una operación asincrónica, la función se invoca en el ciclo con un pequeño retraso de 5 milisegundos hasta que la variable first_server_date adquiera un valor, o la ejecución del ciclo sea interrumpida por el usuario (IsStopped(), en este caso devuelve el valor true). Vamos a indicar un valor correcto de la fecha de inicio, desde la cual empezamos a solicitar los datos de precios en el servidor comercial.

   if(first_server_date>start_date) start_date=first_server_date;
   if(first_date>0 && first_date<first_server_date)
      Print("Warning: first server date ",first_server_date,
            " for ",symbol," does not match to first series date ",first_date);

Si de pronto la fecha de inicio first_server_date del servidor resulta ser menos que la fecha de inicio first_date del símbolo en el formato HCC, en el diario de registro aparecerá el aviso correspondiente.

Ahora estamos preparados a solicitar los datos de precios que nos faltan al servidor comercial. Vamos a presentar la solicitud en forma del ciclo y empezaremos a rellenar su cuerpo:

   while(!IsStopped())
     {
      //1. esperar la sincronización entre la serie temporal reconstruida y el historial intermedio en el formato HCC
      //2. recibir el número corriente de barras en esta serie temporal
      //   si la cantidad de barras supera el valor de Max_bars_in_chart, podemos salir, el trabajo está finalizado
      //3. obtenemos la fecha de inicio first_date en la serie temporal reconstruida y la comparamos con el valor 
      //   start_date si first_date es menos que start_date, podemos salir, el trabajo está finalizado
      //4. Solicitamos al servidor comercial nueva parte del historial de 100 barras empezando desde la última 
      //   barra disponible numerada "bars"
     }

Los tres primeros puntos se implementan por medios ya conocidos.

   while(!IsStopped())
     {
      //--- 1.esperamos que se termine el proceso de reconstrucción de la serie temporal
      while(!SeriesInfoInteger(symbol,period,SERIES_SYNCHRONIZED) && !IsStopped())
         Sleep(5);
      //--- 2.preguntamos cuántas barras tenemos disponibles
      int bars=Bars(symbol,period);
      if(bars>0)
        {
         //--- el número de baras es superior a lo que podemos mostrar en el gráfico, salimos
         if(bars>=max_bars) return(-2); 
         //--- 3. averiguamos la fecha de inicio corriente en la serie temporal
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            // la fecha de inicio es más temprana que la solicitada, tarea cumplida
            if(first_date>0 && first_date<=start_date) return(0);
        }
      //4. Solicitamos al servidor comercial nueva parte del historial de 100 barras 
      //   empezando desde la última barra disponible numerada "bars"
     }

Nos queda el último cuarto punto, que es la misma solicitud del historial. No podemos dirigirnos al servidor directamente pero cualquier función-Copy automáticamente inicia el envío de tal solicitud del terminal al servidor comercial si el historial en el formato HCC no es suficiente. Ya que el tiempo de la primera fecha de inicio en la variable first_date es el criterio más fácil y natural para evaluar el grado de ejecución de la solicitud, lo más fácil será usar la función CopyTime().

Cuando llamamos a las funciones que se encargan de copiar cualquier dato desde las series temporales, hay que tener en cuenta que el parámetro start (número de la barra a partir de la cual se inicia el copiado de datos de precios) siempre tiene que estar dentro de los límites del historial del terminal disponible. Si disponemos sólo de 100 barras, no tiene sentido intentar copiar 300 barras empezando de la barra con el índice 500. Tal solicitud se entenderá como errónea y no será procesada, es decir, no se cargará ningún historial desde el servidor comercial.

Precisamente por eso vamos a copiar 100 barras de una vez, empezando de la barra con el índice bars. Esto proporciona la carga fluida del historial desde el servidor comercial. En realidad se cargará un poco más de 100 barras solicitadas, es que el servidor envía el historial con una cantidad de información ligeramente sobrepasada.

   int copied=CopyTime(symbol,period,bars,100,times);

Después de la operación de copiado hay que analizar el número de elementos copiados. Si el intento ha sido fallido, el valor de la variable copied será igual a cero y el valor del contador fail_cnt será aumentado a 1. El trabajo de la función será detenido después de 100 intentos fallidos.

int fail_cnt=0;
...
   int copied=CopyTime(symbol,period,bars,100,times);
   if(copied>0)
     {
      //--- comprobamos los datos
      if(times[0]<=start_date)  return(0);  // el valor copiado es menos, listo
      if(bars+copied>=max_bars) return(-2); // hay más barras que cabe en el gráfico, listo
      fail_cnt=0;
     }
   else
     {
      //--- no más de 100 intentos fallidos seguidos
      fail_cnt++;
      if(fail_cnt>=100) return(-5);
      Sleep(10);
     }
 

De esta manera, en la función no sólo está implementado el correcto procesamiento de la situación corriente en cada momento de ejecución, sino también se devuelve el código de finalización, el que podemos manejar después de invocar la función CheckLoadHistory() con el fin de obtener información adicional. Por ejemplo, de esta manera:

   int res=CheckLoadHistory(InpLoadedSymbol,InpLoadedPeriod,InpStartDate);
   switch(res)
     {
      case -1 : Print("Símbolo desconocido ",InpLoadedSymbol);                                               break;
      case -2 : Print("Cantidad superior de barras solicitadas a la que puede ser mostrada en el gráfico");  break;
      case -3 : Print("Ejecución interrumpida por el usuario");                                              break;
      case -4 : Print("Indicador no debe cargar sus propios datos");                                         break;
      case -5 : Print("Carga fallida");                                                                      break;
      case  0 : Print("Todos los datos están cargados");                                                     break;
      case  1 : Print("Cantidad de datos ya disponibles en la serie temporal es suficiente");                break;
      case  2 : Print("Serie temporal está construida con los datos disponibles en el terminal");            break;
      default : Print("Resultado de ejecución no determinado");
     }

El código entero de la función viene en el ejemplo del script que demuestra el modo correcto de organizar el acceso a cualquier dato con el procesamiento del resultado de solicitud.

Código:

//+------------------------------------------------------------------+
//|                                              TestLoadHistory.mq5 |
//|                         Copyright 2000-2024, MetaQuotes Ltd. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.02"
#property script_show_inputs
//--- input parameters
input string          InpLoadedSymbol="NZDUSD";   // Symbol to be load
input ENUM_TIMEFRAMES InpLoadedPeriod=PERIOD_H1;  // Period to be load
input datetime        InpStartDate=D'2006.01.01'; // Start date
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   Print("Start load",InpLoadedSymbol+","+GetPeriodName(InpLoadedPeriod),"from",InpStartDate);
//---
   int res=CheckLoadHistory(InpLoadedSymbol,InpLoadedPeriod,InpStartDate);
   switch(res)
     {
      case -1 : Print("Unknown symbol ",InpLoadedSymbol);             break;
      case -2 : Print("Requested bars more than max bars in chart"); break;
      case -3 : Print("Program was stopped");                        break;
      case -4 : Print("Indicator shouldn't load its own data");      break;
      case -5 : Print("Load failed");                                break;
      case  0 : Print("Loaded OK");                                  break;
      case  1 : Print("Loaded previously");                          break;
      case  2 : Print("Loaded previously and built");                break;
      default : Print("Unknown result");
     }
//---
   datetime first_date;
   SeriesInfoInteger(InpLoadedSymbol,InpLoadedPeriod,SERIES_FIRSTDATE,first_date);
   int bars=Bars(InpLoadedSymbol,InpLoadedPeriod);
   Print("First date",first_date,"-",bars,"bars");
//---
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CheckLoadHistory(string symbol,ENUM_TIMEFRAMES period,datetime start_date)
  {
   datetime first_date=0;
   datetime times[100];
//--- check symbol & period
   if(symbol==NULL || symbol==""symbol=Symbol();
   if(period==PERIOD_CURRENT)     period=Period();
//--- check if symbol is selected in the MarketWatch
   if(!SymbolInfoInteger(symbol,SYMBOL_SELECT))
     {
      if(GetLastError()==ERR_MARKET_UNKNOWN_SYMBOLreturn(-1);
      SymbolSelect(symbol,true);
     }
//--- check if data is present
   SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date);
   if(first_date>0 && first_date<=start_date) return(1);
//--- don't ask for load of its own data if it is an indicator
   if(MQL5InfoInteger(MQL5_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol)
      return(-4);
//--- second attempt
   if(SeriesInfoInteger(symbol,PERIOD_M1,SERIES_TERMINAL_FIRSTDATE,first_date))
     {
      //--- there is loaded data to build timeseries
      if(first_date>0)
        {
         //--- force timeseries build
         CopyTime(symbol,period,first_date+PeriodSeconds(period),1,times);
         //--- check date
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            if(first_date>0 && first_date<=start_date) return(2);
        }
     }
//--- max bars in chart from terminal options
   int max_bars=TerminalInfoInteger(TERMINAL_MAXBARS);
//--- load symbol history info
   datetime first_server_date=0;
   while(!SeriesInfoInteger(symbol,PERIOD_M1,SERIES_SERVER_FIRSTDATE,first_server_date) && !IsStopped())
      Sleep(5);
//--- fix start date for loading
   if(first_server_date>start_date) start_date=first_server_date;
   if(first_date>0 && first_date<first_server_date)
      Print("Warning: first server date ",first_server_date,
            " for ",symbol," does not match to first series date ",first_date);
//--- load data step by step
   int fail_cnt=0;
   while(!IsStopped())
     {
      //--- wait for timeseries build
      while(!SeriesInfoInteger(symbol,period,SERIES_SYNCHRONIZED) && !IsStopped())
         Sleep(5);
      //--- ask for built bars
      int bars=Bars(symbol,period);
      if(bars>0)
        {
         if(bars>=max_bars) return(-2);
         //--- ask for first date
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            if(first_date>0 && first_date<=start_date) return(0);
        }
      //--- copying of next part forces data loading
      int copied=CopyTime(symbol,period,bars,100,times);
      if(copied>0)
        {
         //--- check for data
         if(times[0]<=start_date)  return(0);
         if(bars+copied>=max_bars) return(-2);
         fail_cnt=0;
        }
      else
        {
         //--- no more than 100 failed attempts
         fail_cnt++;
         if(fail_cnt>=100) return(-5);
         Sleep(10);
        }
     }
//--- stopped
   return(-3);
  }
//+------------------------------------------------------------------+
//| devuelve a la cadena valor del período                           |
//+------------------------------------------------------------------+
string GetPeriodName(ENUM_TIMEFRAMES period)
  {
   if(period==PERIOD_CURRENTperiod=Period();
//---
   switch(period)
     {
      case PERIOD_M1:  return("M1");
      case PERIOD_M2:  return("M2");
      case PERIOD_M3:  return("M3");
      case PERIOD_M4:  return("M4");
      case PERIOD_M5:  return("M5");
      case PERIOD_M6:  return("M6");
      case PERIOD_M10return("M10");
      case PERIOD_M12return("M12");
      case PERIOD_M15return("M15");
      case PERIOD_M20return("M20");
      case PERIOD_M30return("M30");
      case PERIOD_H1:  return("H1");
      case PERIOD_H2:  return("H2");
      case PERIOD_H3:  return("H3");
      case PERIOD_H4:  return("H4");
      case PERIOD_H6:  return("H6");
      case PERIOD_H8:  return("H8");
      case PERIOD_H12return("H12");
      case PERIOD_D1:  return("Daily");
      case PERIOD_W1:  return("Weekly");
      case PERIOD_MN1return("Monthly");
     }
//---
   return("unknown period");
  }