Escritura y lectura de variables (archivos de texto)

Los archivos de texto tienen su propio conjunto de funciones para el almacenamiento atómico (elemento por elemento) y para la lectura de datos. Es ligeramente diferente de los archivos binarios establecidos en la sección anterior. También hay que tener en cuenta que no existen funciones analógicas para escribir/leer una estructura o un array de estructuras en un archivo de texto. Si intenta utilizar cualquiera de estas funciones con un archivo de texto, no tendrán ningún efecto, sino que emitirán un código de error interno 5011 (FILE_NOTBIN).

Como ya sabemos, los archivos de texto en MQL5 tienen dos formas: texto plano y texto en formato CSV. El modo correspondiente, FILE_TXT o FILE_CSV, se establece cuando se abre el archivo y no se puede cambiar sin cerrar y volver a adquirir el manejador. La diferencia entre ellos sólo aparece al leer archivos. Ambos modos se graban de la misma manera.

En el modo TXT, cada llamada a la función de lectura (cualquiera de las funciones que veremos en esta sección) encuentra la siguiente nueva línea en el archivo (un carácter '\n' o un par de '\r\n') y procesa todo hasta ella. El objetivo del tratamiento es convertir el texto del archivo en un valor de un tipo específico correspondiente a la función llamada. En el caso más sencillo, si se llama a la función FileReadString no se realiza ningún procesamiento (la cadena se devuelve «tal cual»).

En el modo CSV, cada vez que se llama a la función de lectura, el texto del archivo se divide de manera lógica, no sólo por nuevas líneas, sino también por un delimitador adicional especificado al abrir el archivo. El resto del procesamiento del fragmento desde la posición actual del archivo hasta el delimitador más cercano es similar.

En otras palabras: la lectura del texto y la transferencia de la posición interna dentro del archivo se realiza en fragmentos de delimitador a delimitador, donde delimitador significa no sólo el carácter delimiter de la lista de parámetros FileOpen, sino también una nueva línea ('\n', '\r\n'), así como el principio y el final del archivo.

El delimitador adicional tiene el mismo efecto al escribir texto en archivos FILE_TXT y FILE_CSV, pero sólo cuando se utiliza la función FileWrite: inserta automáticamente este carácter entre los elementos grabados. El separador de funciones FileWriteString se ignora.

Veamos las descripciones formales de las funciones y, a continuación, un ejemplo en FileTxtCsv.mq5.

uint FileWrite(int handle, ...)

La función pertenece a la categoría de funciones que toman un número variable de parámetros. Estos parámetros se indican en el prototipo de la función con una elipsis. Sólo se admiten los tipos de datos integrados. Para escribir estructuras u objetos de clase, debe «desreferenciar» sus elementos y pasarlos individualmente.

La función escribe todos los argumentos pasados después del primero en un archivo de texto con el descriptor handle. Los argumentos se separan por comas, como en una lista de argumentos normal. El número de argumentos que se envían al archivo no puede ser superior a 63.

En la salida, los datos numéricos se convierten a formato de texto según las reglas de la conversión estándar a (string). Los valores o el tipo double tienen salida a 16 dígitos significativos, ya sea en formato tradicional o en formato de exponente científico (se elige la opción más compacta). Los datos del tipo float se muestran con una precisión de 7 dígitos significativos. Para mostrar números reales con una precisión diferente o en un formato especificado explícitamente, utilice la función DoubleToString (véase De números a cadenas y viceversa).

Los valores del tipo datetime se imprimen en el formato «AAAA.MM.DD hh:mm:ss» (véase Fecha y hora).

Un color estándar (de la lista de colores web) se muestra como un nombre, un color no estándar se muestra como un triple de valores de componentes RGB (véase Color), separados por comas (nota: la coma es el carácter separador más común en CSV).

En el caso de las enumeraciones se muestra un número entero que denota el elemento en lugar de su identificador (nombre). Por ejemplo, al escribir VIERNES (de ENUM_DAY_OF_WEEK, véase Enumeraciones) obtenemos el número 5 en el archivo.

Los valores del tipo bool se muestran como cadenas «true» o «false».

Si al abrir el archivo se ha especificado un carácter delimitador distinto de 0, éste se insertará entre dos líneas adyacentes resultantes de la conversión de los argumentos correspondientes.

Una vez que todos los argumentos se escriben en el archivo, se añade un terminador de línea '\r\n'.

La función devuelve el número de bytes escritos, o 0 en caso de error.

uint FileWriteString(int handle, const string text, int length = -1)

La función escribe el parámetro de cadena text en un archivo de texto con el descriptor handle. El parámetro length sólo es aplicable a archivos binarios y se ignora en este contexto (la línea se escribe completa).

La función FileWriteString también puede trabajar con archivos binarios. Esta aplicación de la función se describe en el apartado anterior.

Los separadores (entre elementos de una línea) y las nuevas líneas deben ser insertados o añadidos por el programador.

La función devuelve el número de bytes escritos (en modo FILE_UNICODE será 2 veces la longitud de la cadena en caracteres) o 0 en caso de error.

string FileReadString(int handle, int length = -1)

La función lee una cadena hasta el siguiente delimitador de un archivo con el descriptor handle (carácter delimitador en un archivo CSV, carácter de salto de línea en cualquier archivo, o hasta el final del archivo). El parámetro length sólo se aplica a los archivos binarios y se ignora en este contexto.

La cadena resultante se puede convertir en un valor del tipo requerido utilizando normas de reducción estándar o mediante funciones de conversión. Como alternativa, se pueden utilizar funciones de lectura especializadas, como FileReadBool, FileReadDatetime, FileReadNumber, que se describen a continuación.

En caso de error, se devolverá una cadena vacía. El código de error puede encontrarse por medio de la variable _LastError o de la función GetLastError. En concreto, cuando se alcance el final del archivo, el código de error será 5027 (FILE_ENDOFFILE).

bool FileReadBool(int handle)

La función lee un fragmento de un archivo CSV hasta el siguiente delimitador, o hasta el final de la línea y lo convierte en un valor del tipo bool. Si el fragmento contiene el texto «true» (ya sea en minúsculas, mayúsculas o una combinación de ambas, como en «True»), o un número distinto de cero, obtenemos true. En otros casos, obtenemos false.

La palabra «true» debe ocupar todo el elemento de lectura. Aun cuando la cadena empiece por «true» y tenga una continuación (por ejemplo, «True Volume»), obtendremos false.

datetime FileReadDatetime(int handle)

La función lee en un archivo CSV una cadena de uno de los siguientes formatos: «AAAA.MM.DD hh:mm:ss», «AAAA.MM.DD» o «hh:mm:ss», y lo convierte en un valor del tipo datetime. Si el fragmento no contiene una representación textual válida de la fecha y/o la hora, la función devolverá cero o una hora «rara», dependiendo de los caracteres que pueda interpretar como fragmentos de fecha y hora. Para cadenas vacías o no numéricas, obtenemos la fecha actual con hora cero.

Se puede conseguir una lectura más flexible de la fecha y la hora (con más formatos admitidos) combinando dos funciones: StringToTime(FileReadString(handle)). Para obtener más información sobre StringToTime, consulte Fecha y hora.

double FileReadNumber(int handle)

La función lee un fragmento del archivo CSV hasta el siguiente delimitador o hasta el final de la línea, y lo convierte en un valor de tipo double según las normas estándar de conversión de tipos.

Tenga en cuenta que double puede perder la precisión de valores muy grandes, lo que puede afectar a la lectura de números grandes de los tipos long/ulong (el valor a partir del cual se distorsionan los enteros dentro de double es 9007199254740992; se ofrece un ejemplo de este fenómeno en la sección Uniones).

Las funciones analizadas en la sección anterior, incluidas FileReadDouble, FileReadFloat, FileReadInteger, FileReadLong y FileReadStruct, no pueden aplicarse a archivos de texto.

El script FileTxtCsv.mq5 demuestra cómo trabajar con archivos de texto. La última vez cargamos las cotizaciones en un archivo binario. Ahora vamos a hacerlo en formatos TXT y CSV.

Básicamente, MetaTrader 5 permite exportar e importar cotizaciones en formato CSV desde el cuadro de diálogo «Símbolos». Sin embargo, a efectos educativos, reproduciremos este proceso. Además, la aplicación informática le permite desviarse del formato exacto que se genera por defecto. A continuación se muestra un fragmento del historial XAUUSD H1 exportado de la forma estándar.

<DATE> » <TIME> » <OPEN> » <HIGH> » <LOW> » <CLOSE> » <TICKVOL> » <VOL> » <SPREAD>
2021.01.04 » 01:00:00 » 1909.07 » 1914.93 » 1907.72 » 1913.10 » 4230 » 0 » 5
2021.01.04 » 02:00:00 » 1913.04 » 1913.64 » 1909.90 » 1913.41 » 2694 » 0 » 5
2021.01.04 » 03:00:00 » 1913.41 » 1918.71 » 1912.16 » 1916.61 » 6520 » 0 » 5
2021.01.04 » 04:00:00 » 1916.60 » 1921.89 » 1915.49 » 1921.79 » 3944 » 0 » 5
2021.01.04 » 05:00:00 » 1921.79 » 1925.26 » 1920.82 » 1923.19 » 3293 » 0 » 5
2021.01.04 » 06:00:00 » 1923.20 » 1923.71 » 1920.24 » 1922.67 » 2146 » 0 » 5
2021.01.04 » 07:00:00 » 1922.66 » 1922.99 » 1918.93 » 1921.66 » 3141 » 0 » 5
2021.01.04 » 08:00:00 » 1921.66 » 1925.60 » 1921.47 » 1922.99 » 3752 » 0 » 5
2021.01.04 » 09:00:00 » 1922.99 » 1925.54 » 1922.47 » 1924.80 » 2895 » 0 » 5
2021.01.04 » 10:00:00 » 1924.85 » 1935.16 » 1924.59 » 1932.07 » 6132 » 0 » 5

Aquí, en concreto, puede que no estemos satisfechos con el carácter separador por defecto (tabulador, denotado como «»), el orden de las columnas o el hecho de que la fecha y la hora estén divididas en dos campos.

En nuestro script, elegiremos la coma como separador y generaremos las columnas en el orden de los campos de la estructura MqlRates. La descarga y posterior lectura de prueba se realizará en los modos FILE_TXT y FILE_CSV.

const string txtfile = "MQL5Book/atomic.txt";
const string csvfile = "MQL5Book/atomic.csv";
const short delimiter = ',';

Las cotizaciones se solicitarán al principio de la función OnStart de la forma habitual:

void OnStart()
{
   MqlRates rates[];   
   int n = PRTF(CopyRates(_Symbol_Period010rates)); // 10

Especificaremos los nombres de las columnas del array por separado, y también los combinaremos utilizando la función de ayuda StringCombine. Los títulos separados son necesarios porque los combinamos en un título común utilizando un carácter delimitador seleccionable (una solución alternativa podría basarse en StringReplace). Le animamos a que trabaje con el código fuente StringCombine de forma independiente: realiza la operación inversa con respecto al StringSplit integrado.

   const string columns[] = {"DateTime""Open""High""Low""Close"
                             "Ticks""Spread""True"};
   const string caption = StringCombine(columnsdelimiter) + "\r\n";

La última columna debería haberse llamado «Volume», pero utilizaremos su ejemplo para comprobar el rendimiento de la función FileReadBool. Puede suponer que el nombre actual implica «True Volume» (pero tal cadena no se interpretaría como true).

A continuación vamos a abrir dos archivos en los modos FILE_TXT y FILE_CSV, y a escribir en ellos el encabezado preparado.

   int fh1 = PRTF(FileOpen(txtfileFILE_TXT | FILE_ANSI | FILE_WRITEdelimiter));//1
   int fh2 = PRTF(FileOpen(csvfileFILE_CSV | FILE_ANSI | FILE_WRITEdelimiter));//2
  
   PRTF(FileWriteString(fh1caption)); // 48
   PRTF(FileWriteString(fh2caption)); // 48

Como la función FileWriteString no añade automáticamente una nueva línea, hemos añadido «\r\n» a la variable caption.

   for(int i = 0i < n; ++i)
   {
      FileWrite(fh1rates[i].time
         rates[i].openrates[i].highrates[i].lowrates[i].close
         rates[i].tick_volumerates[i].spreadrates[i].real_volume);
      FileWrite(fh2rates[i].time
         rates[i].openrates[i].highrates[i].lowrates[i].close
         rates[i].tick_volumerates[i].spreadrates[i].real_volume);
   }
   
   FileClose(fh1);
   FileClose(fh2);

La escritura de campos de estructura desde el array rates se realiza de la misma manera, llamando a FileWrite en un bucle para cada uno de los dos archivos. Recuerde que la función FileWrite inserta automáticamente un carácter delimitador entre los argumentos y añade «\r\n» al final de la cadena. Por supuesto, era posible convertir independientemente todos los valores de salida en cadenas y enviarlos a un archivo utilizando FileWriteString, pero entonces tendríamos que ocuparnos nosotros mismos de los separadores y las nuevas líneas. En algunos casos no son necesarios, por ejemplo, si está escribiendo en formato JSON de forma compacta (esencialmente en una línea gigante).

Así, en la fase de grabación, ambos archivos se gestionaron de la misma manera y resultaron ser iguales. He aquí un ejemplo de su contenido para XAUUSD,H1 (sus resultados pueden variar):

DateTime,Open,High,Low,Close,Ticks,Spread,True
2021.08.19 12:00:00,1785.3,1789.76,1784.75,1789.06,4831,5,0
2021.08.19 13:00:00,1789.06,1790.02,1787.61,1789.06,3393,5,0
2021.08.19 14:00:00,1789.08,1789.95,1786.78,1786.89,3536,5,0
2021.08.19 15:00:00,1786.78,1789.86,1783.73,1788.82,6840,5,0
2021.08.19 16:00:00,1788.82,1792.44,1782.04,1784.02,9514,5,0
2021.08.19 17:00:00,1784.04,1784.27,1777.14,1780.57,8526,5,0
2021.08.19 18:00:00,1780.55,1784.02,1780.05,1783.07,5271,6,0
2021.08.19 19:00:00,1783.06,1783.15,1780.73,1782.59,3571,7,0
2021.08.19 20:00:00,1782.61,1782.96,1780.16,1780.78,3236,10,0
2021.08.19 21:00:00,1780.79,1780.9,1778.54,1778.65,1017,13,0

Las diferencias a la hora de trabajar con estos archivos empezarán a aparecer en la fase de lectura.

Vamos a abrir un archivo de texto para leerlo y «escaneémoslo» utilizando la función FileReadString en un bucle, hasta que devuelva una cadena vacía (es decir, hasta el final del archivo).

   string read;
   fh1 = PRTF(FileOpen(txtfileFILE_TXT | FILE_ANSI | FILE_READdelimiter)); // 1
   Print("===== Reading TXT");
   do
   {
      read = PRTF(FileReadString(fh1));
   }
   while(StringLen(read) > 0);

El registro mostrará algo como esto:

===== Reading TXT
FileReadString(fh1)=DateTime,Open,High,Low,Close,Ticks,Spread,True / ok
FileReadString(fh1)=2021.08.19 12:00:00,1785.3,1789.76,1784.75,1789.06,4831,5,0 / ok
FileReadString(fh1)=2021.08.19 13:00:00,1789.06,1790.02,1787.61,1789.06,3393,5,0 / ok
FileReadString(fh1)=2021.08.19 14:00:00,1789.08,1789.95,1786.78,1786.89,3536,5,0 / ok
FileReadString(fh1)=2021.08.19 15:00:00,1786.78,1789.86,1783.73,1788.82,6840,5,0 / ok
FileReadString(fh1)=2021.08.19 16:00:00,1788.82,1792.44,1782.04,1784.02,9514,5,0 / ok
FileReadString(fh1)=2021.08.19 17:00:00,1784.04,1784.27,1777.14,1780.57,8526,5,0 / ok
FileReadString(fh1)=2021.08.19 18:00:00,1780.55,1784.02,1780.05,1783.07,5271,6,0 / ok
FileReadString(fh1)=2021.08.19 19:00:00,1783.06,1783.15,1780.73,1782.59,3571,7,0 / ok
FileReadString(fh1)=2021.08.19 20:00:00,1782.61,1782.96,1780.16,1780.78,3236,10,0 / ok
FileReadString(fh1)=2021.08.19 21:00:00,1780.79,1780.9,1778.54,1778.65,1017,13,0 / ok
FileReadString(fh1)= / FILE_ENDOFFILE(5027)

Cada llamada de FileReadString lee la línea completa (hasta '\r\n') en el modo FILE_TXT. Para separarlo en elementos, debemos aplicar un tratamiento adicional. Opcionalmente, podemos utilizar el modo FILE_CSV.

Hagamos lo mismo con el archivo CSV.

   fh2 = PRTF(FileOpen(csvfileFILE_CSV | FILE_ANSI | FILE_READdelimiter)); // 2
   Print("===== Reading CSV");
   do
   {
      read = PRTF(FileReadString(fh2));
   }
   while(StringLen(read) > 0);

Esta vez habrá muchas más entradas en el registro:

===== Reading CSV
FileReadString(fh2)=DateTime / ok
FileReadString(fh2)=Open / ok
FileReadString(fh2)=High / ok
FileReadString(fh2)=Low / ok
FileReadString(fh2)=Close / ok
FileReadString(fh2)=Ticks / ok
FileReadString(fh2)=Spread / ok
FileReadString(fh2)=True / ok
FileReadString(fh2)=2021.08.19 12:00:00 / ok
FileReadString(fh2)=1785.3 / ok
FileReadString(fh2)=1789.76 / ok
FileReadString(fh2)=1784.75 / ok
FileReadString(fh2)=1789.06 / ok
FileReadString(fh2)=4831 / ok
FileReadString(fh2)=5 / ok
FileReadString(fh2)=0 / ok
...
FileReadString(fh2)=2021.08.19 21:00:00 / ok
FileReadString(fh2)=1780.79 / ok
FileReadString(fh2)=1780.9 / ok
FileReadString(fh2)=1778.54 / ok
FileReadString(fh2)=1778.65 / ok
FileReadString(fh2)=1017 / ok
FileReadString(fh2)=13 / ok
FileReadString(fh2)=0 / ok
FileReadString(fh2)= / FILE_ENDOFFILE(5027)

La cuestión es que la función FileReadString en el modo FILE_CSV tiene en cuenta el carácter delimitador y divide las cadenas en elementos. Cada llamada a FileReadString devuelve un único valor (celda) de una tabla CSV. Obviamente, las cadenas resultantes deben convertirse posteriormente a los tipos adecuados.

Este problema puede resolverse de forma generalizada utilizando las funciones especializadas FileReadDatetime, FileReadNumber, FileReadBool. Sin embargo, en cualquier caso, el desarrollador debe llevar un registro del número de la columna legible actual y determinar su significado práctico. En el tercer paso de la prueba se ofrece un ejemplo de un algoritmo de este tipo. Utiliza el mismo archivo CSV (para simplificar, lo cerramos al final de cada paso y lo abrimos al principio del siguiente).

Para simplificar la asignación del siguiente campo en la estructura MqlRates por el número de columna, hemos creado una estructura hija MqlRates que contiene un método de plantilla set:

struct MqlRatesM : public MqlRates
{
   template<typename T>
   void set(int fieldT v)
   {
      switch(field)
      {
         case 0this.time = (datetime)vbreak;
         case 1this.open = (double)vbreak;
         case 2this.high = (double)vbreak;
         case 3this.low = (double)vbreak;
         case 4this.close = (double)vbreak;
         case 5this.tick_volume = (long)vbreak;
         case 6this.spread = (int)vbreak;
         case 7this.real_volume = (long)vbreak;
      }
   }
};

En la función OnStart hemos descrito un array de una estructura de este tipo, donde añadiremos los valores entrantes. El array se requiere para simplificar el registro con ArrayPrint (no hay ninguna función ya preparada en MQL5 para imprimir una estructura por sí misma).

   Print("===== Reading CSV (alternative)");
   MqlRatesM r[1];
   int count = 0;
   int column = 0;
   const int maxColumn = ArraySize(columns);

La variable count que cuenta los registros era necesaria no sólo para las estadísticas, sino también como medio para omitir la primera línea, que contiene encabezados y no datos. El número de columna actual se registra en la variable column. Su valor máximo no debe superar el número de columnas maxColumn.

Ahora sólo tenemos que abrir el archivo y leer elementos del mismo en un bucle utilizando varias funciones hasta que se produzca un error; en concreto, un error esperado como 5027 (FILE_ENDOFFILE), es decir, que se llegue al final del archivo.

Cuando el número de columna es 0, aplicamos la función FileReadDatetime. Para otras columnas, utilice FileReadNumber. La excepción es el caso de la primera línea con encabezados: para ello llamamos a la función FileReadBool a fin de demostrar cómo reaccionaría ante el encabezado «True» añadido deliberadamente a la última columna.

   fh2 = PRTF(FileOpen(csvfileFILE_CSV | FILE_ANSI | FILE_READdelimiter)); // 1
   do
   {
      if(column)
      {
         if(count == 1// demo for FileReadBool on the 1st record with headers
         {
            r[0].set(columnPRTF(FileReadBool(fh2)));
         }
         else
         {
            r[0].set(columnFileReadNumber(fh2));
         }
      }
      else // 0th column is the date and time
      {
         ++count;
         if(count >1// the structure from the previous line is ready
         {
            ArrayPrint(r_DigitsNULL010);
         }
         r[0].time = FileReadDatetime(fh2);
      }
      column = (column + 1) % maxColumn;
   }
   while(_LastError == 0); // exit when end of file 5027 is reached (FILE_ENDOFFILE)
   
   // printing the last structure
   if(column == maxColumn - 1)
   {
      ArrayPrint(r_DigitsNULL010);
   }

Esto es lo que se registra:

===== Reading CSV (alternative)
FileOpen(csvfile,FILE_CSV|FILE_ANSI|FILE_READ,delimiter)=1 / ok
FileReadBool(fh2)=false / ok
FileReadBool(fh2)=false / ok
FileReadBool(fh2)=false / ok
FileReadBool(fh2)=false / ok
FileReadBool(fh2)=false / ok
FileReadBool(fh2)=false / ok
FileReadBool(fh2)=true / ok
2021.08.19 00:00:00   0.00   0.00  0.00    0.00          0     0       1
2021.08.19 12:00:00 1785.30 1789.76 1784.75 1789.06       4831     5       0
2021.08.19 13:00:00 1789.06 1790.02 1787.61 1789.06       3393     5       0
2021.08.19 14:00:00 1789.08 1789.95 1786.78 1786.89       3536     5       0
2021.08.19 15:00:00 1786.78 1789.86 1783.73 1788.82       6840     5       0
2021.08.19 16:00:00 1788.82 1792.44 1782.04 1784.02       9514     5       0
2021.08.19 17:00:00 1784.04 1784.27 1777.14 1780.57       8526     5       0
2021.08.19 18:00:00 1780.55 1784.02 1780.05 1783.07       5271     6       0
2021.08.19 19:00:00 1783.06 1783.15 1780.73 1782.59       3571     7       0
2021.08.19 20:00:00 1782.61 1782.96 1780.16 1780.78       3236    10       0
2021.08.19 21:00:00 1780.79 1780.90 1778.54 1778.65       1017    13       0

Como ve, de todos los encabezados, sólo el último se convierte al valor true, y todos los anteriores son false.

El contenido de las estructuras leídas es el mismo que el de los datos originales.