English Русский 中文 Deutsch 日本語
preview
Dominando las operaciones con archivos en MQL5: desde E/S básicas hasta la creación de un lector CSV personalizado

Dominando las operaciones con archivos en MQL5: desde E/S básicas hasta la creación de un lector CSV personalizado

MetaTrader 5Indicadores |
66 0
Sahil Bagdi
Sahil Bagdi

Introducción

En el mundo actual del comercio automatizado, los datos lo son todo. Quizás necesite cargar parámetros personalizados para su estrategia, leer una lista de seguimiento de símbolos o integrar datos históricos de fuentes externas. Si trabajas con MetaTrader 5, te alegrará saber que MQL5 facilita mucho el manejo de archivos directamente desde tu código.

Pero seamos sinceros: revisar la documentación para entender las operaciones de los archivos puede resultar un poco abrumador al principio. Por eso, en este artículo, analizaremos los fundamentos de una manera sencilla y paso a paso. Una vez que cubramos los conceptos básicos, como cómo la «zona de pruebas» de MQL5 protege sus archivos, cómo abrir archivos en modo texto o binario y cómo leer y dividir líneas de forma segura, lo pondremos todo en práctica creando una clase sencilla de lector CSV.

¿Por qué archivos CSV? Porque están en todas partes: son sencillos, legibles para los humanos y compatibles con innumerables herramientas. Con un lector CSV, puede importar parámetros externos, listas de símbolos u otros datos personalizados directamente a su Asesor Experto o script, ajustando el comportamiento de su estrategia sin tener que cambiar el código cada vez.

No te abrumaremos con todos los detalles de las funciones de los archivos MQL5, pero te explicaremos lo que necesitas saber. Al final, tendrás un ejemplo claro de cómo abrir un archivo CSV en modo texto, cómo leer sus líneas hasta el final del archivo, cómo dividir cada línea en campos mediante un delimitador elegido, cómo almacenar y recuperar estos campos por nombre de columna o índice, y una comprensión clara de cada uno de ellos.

Este es el plan para este artículo:

  1. Fundamentos de las operaciones con archivos MQL5
  2. Diseño de la clase de lector CSV
  3. Finalización de la implementación de la clase de lectura CSV
  4. Escenarios de prueba y uso
  5. Conclusión


Fundamentos de las operaciones con archivos MQL5

Antes de implementar nuestro lector CSV, veamos más de cerca algunos conceptos básicos sobre el manejo de archivos en MQL5 y los ilustremos con código. Nos centraremos en comprender las restricciones del espacio aislado, los modos de apertura de archivos, la lectura línea por línea y el manejo básico de errores. Ver estos fundamentos en acción hará que sea más fácil construir y depurar nuestro lector CSV más adelante.

Primero, entendamos el sandbox y el acceso restringido a archivos. MQL5 aplica un modelo de seguridad que restringe las operaciones con archivos a ciertos directorios conocidos como sandbox. Normalmente, solo se pueden leer y escribir archivos ubicados en <Carpeta de datos del terminal>/MQL5/Files. Si intenta acceder a archivos fuera de este directorio, la función FileOpen() fallará.

Por ejemplo, si coloca un archivo llamado data.csv en la carpeta MQL5/Files de su terminal MT5, puede abrirlo de la siguiente manera:

int fileHandle = FileOpen("data.csv", FILE_READ|FILE_TXT);
if(fileHandle == INVALID_HANDLE)
  {
   Print("Error: Could not open data.csv. LastError=", _LastError);
   // _LastError can help diagnose if it's a path or permission issue
   return;
  }

// Successfully opened the file, now we can read from it.

Quizás te preguntes qué significan esos códigos de error. Por ejemplo, _LastError = 5004 suele significar algo así como «Archivo no encontrado» o «No se puede abrir el archivo», lo que a menudo se debe a un error tipográfico en el nombre del archivo o a que el archivo no se encuentra dentro de MQL5/Files. Si ve otro código, una rápida consulta en la documentación de MQL5 o en los foros de la comunidad puede ayudarle a descifrar el mensaje. A veces es solo un problema de ruta, otras veces el archivo está bloqueado por otro programa. Si los datos externos son cruciales para su EA, considere añadir un reintento rápido o una impresión detallada del error para poder solucionar los problemas rápidamente.

Tenemos muchas opciones al abrir un archivo. Cuando se llama a FileOpen(), se especifican indicadores para controlar cómo se accede al archivo. Las banderas comunes incluyen:

  • FILE_READ: Abre el archivo para leerlo.
  • FILE_WRITE: Abre el archivo para escribir.
  • FILE_BIN: Modo binario (sin procesamiento de texto).
  • FILE_TXT: Modo texto (gestiona los finales de línea y las conversiones de texto).
  • FILE_CSV: Modo de texto especial que trata el archivo como un CSV.

Para leer un CSV estándar, FILE_READ|FILE_TXT es un excelente punto de partida. El modo texto garantiza que FileReadString() se detendrá en los saltos de línea, lo que simplifica el procesamiento de archivos línea por línea:

int handle = FileOpen("params.txt", FILE_READ|FILE_TXT);
if(handle != INVALID_HANDLE)
  {
   Print("File opened in text mode.");
   // ... read lines here ...
   FileClose(handle);
  }
else
  {
   Print("Failed to open params.txt");
  }

Una vez abierto el archivo en modo texto, leer las líneas es muy sencillo. Utiliza FileReadString() para leer hasta la siguiente línea nueva. Cuando el archivo termina, FileIsEnding() devuelve verdadero. Veamos este bucle:

int handle = FileOpen("list.txt", FILE_READ|FILE_TXT);
if(handle == INVALID_HANDLE)
  {
   Print("Error opening list.txt");
   return;
  }

while(!FileIsEnding(handle))
  {
   string line = FileReadString(handle);
   if(line == "" && _LastError != 0)
     {
      // If empty line and there's an error, break
      Print("Read error or unexpected end of file. _LastError=", _LastError);
      break;
     }
   
   // Process the line
   Print("Line read: ", line);
}

FileClose(handle);

En este fragmento, leemos líneas continuamente hasta llegar al final del archivo. Si se produce un error, se detiene. Se permiten líneas vacías, por lo que si desea omitirlas, solo tiene que marcar if(line=="") continue; . Este enfoque resultará útil al manejar filas CSV.

Ten en cuenta que los archivos de texto no siempre son uniformes. La mayoría utiliza \n o \r\n para los finales de línea, y MQL5 suele gestionarlos correctamente. Aun así, si obtienes un archivo de una fuente inusual, vale la pena comprobar si las líneas se leen correctamente. Si FileReadString() devuelve resultados extraños (como líneas fusionadas), abra el archivo en un editor de texto y confirme su codificación y estilo de salto de línea. Además, tenga en cuenta las líneas extremadamente largas, poco frecuentes en archivos CSV pequeños, pero posibles. Una comprobación de la longitud o un recorte pueden ayudar a garantizar que tu EA no tropiece con formatos inesperados.

Para procesar datos CSV, dividirá cada línea en campos basándose en un delimitador, que suele ser una coma o un punto y coma. La función StringSplit() de MQL5 ayuda a:

string line = "EURUSD;1.2345;Some Comment";
string fields[];
int count = StringSplit(line, ';', fields);

if(count > 0)
  {
   Print("Found ", count, " fields");
   for(int i=0; i<count; i++)
     Print("Field[", i, "] = ", fields[i]);
  }
else
  {
   Print("No fields found in line: ", line);
}

Este código imprime cada campo analizado. Para la lectura de CSV, después de dividir, almacenará estos campos en la memoria para poder acceder a ellos más tarde por índice de columna o nombre.

Aunque StringSplit() funciona muy bien con delimitadores simples, recuerda que los formatos CSV pueden ser complicados. Algunos tienen campos entre comillas o delimitadores escapados, que no estamos tratando aquí. Si tu archivo es sencillo, sin comillas ni trucos sofisticados, StringSplit() es suficiente. Si los campos tienen espacios al final o puntuación extraña, considera usar StringTrim() después de dividirlos. Estas pequeñas comprobaciones mantienen la solidez de su EA, incluso si su fuente de datos añade pequeños errores de formato.

Muchos archivos CSV tienen una fila de encabezado que define los nombres de las columnas. Si _hasHeader es verdadero en nuestro próximo lector CSV, la primera línea leída se dividirá y se almacenará en un mapa hash que asigna nombres de columnas a índices.

Por ejemplo:

// Assume header line: "Symbol;MaxLot;MinSpread"
string header = "Symbol;MaxLot;MinSpread";
string cols[];
int colCount = StringSplit(header, ';', cols);

// Suppose we have a CHashMap<string,uint> Columns;
for(int i=0; i<colCount; i++)
  Columns.Add(cols[i], i);

// Now we can quickly find the index for "MinSpread" or any other column name.
uint idx;
bool found = Columns.TryGetValue("MinSpread", idx);
if(found)
  Print("MinSpread column index: ", idx);
else
  Print("Column 'MinSpread' not found");
Si no hay encabezado, nos basaremos únicamente en los índices numéricos. La primera línea leída será una fila de datos, y las columnas se referenciarán por sus posiciones.


El mapa hash (CHashMap) para los nombres de las columnas es un pequeño detalle que marca una gran diferencia. Sin él, cada vez que necesitaras un índice de columna, tendrías que recorrer los campos del encabezado. Con un mapa hash, TryGetValue() te da el índice de inmediato. Si no se encuentra una columna, puede devolver un valor de error, de forma sencilla y elegante. Si le preocupan las columnas que aparecen dos veces, puede añadir una comprobación rápida al leer el encabezado e imprimir una advertencia si se producen duplicados. Pequeñas mejoras como esa mantienen tu código robusto a medida que tus archivos CSV se vuelven más complejos con el tiempo.

Para el almacenamiento de datos, lo mantendremos sencillo: cada línea analizada (después de dividirla) se convierte en una fila. Usaremos CArrayString para almacenar campos de una sola fila y CArrayObj para almacenar varias filas:

#include <Arrays\ArrayObj.mqh>
#include <Arrays\ArrayString.mqh>

CArrayObj Rows;

// after splitting line into fields:
CArrayString *row = new CArrayString;
for(int i=0; i<count; i++)
  row.Add(fields[i]);

Rows.Add(row);

Más tarde, para recuperar un valor:

// Access row 0, column 1
CArrayString *aRow = Rows.At(0);
string val = aRow.At(1);
Print("Row0 Col1: ", val);
// Access row 0, column 1
CArrayString *aRow = Rows.At(0);
string val = aRow.At(1);
Print("Row0 Col1: ", val);

Debemos asegurarnos de que los índices sean válidos antes de acceder a ellos. 

Ten siempre en cuenta la posibilidad de que falten archivos o columnas. Por ejemplo, si FileOpen() devuelve INVALID_HANDLE, registra un error y regresa. Si el nombre de la columna solicitada no existe, devuelve un valor predeterminado. Nuestra clase lectora CSV final encapsulará estas comprobaciones para que el código principal de su EA permanezca ordenado.

Para combinar todos estos conceptos básicos (reglas del entorno de pruebas, apertura de archivos, lectura de líneas, división de campos y almacenamiento de resultados), disponemos de todos los elementos fundamentales que necesitamos. En las siguientes secciones, diseñaremos e implementaremos nuestra clase de lector CSV paso a paso, utilizando estos conceptos. Si nos centramos ahora en la claridad y la gestión de errores, la implementación posterior será más fluida y fiable.


Diseño de la clase de lector CSV

Ahora que hemos refrescado los fundamentos, describamos la estructura de nuestra clase de lector CSV y comencemos a implementar las partes clave. Crearemos una clase llamada algo así como CSimpleCSVReader que te permitirá:

  1. Abrir un archivo CSV específico en modo de lectura de texto.
  2. Si se solicita, trate la primera línea como un encabezado, almacene los nombres de las columnas y cree un mapa de los nombres de las columnas a los índices.
  3. Lee todas las líneas siguientes en la memoria, dividiendo cada línea en una matriz de cadenas (una por columna).
  4. Proporcionar métodos para consultar datos por índice de columna o nombre.
  5. Devuelve valores predeterminados o de error si falta algo.

Lo haremos paso a paso. En primer lugar, consideremos las estructuras de datos que utilizaremos internamente:

  • Un CHashMap<string,uint> para almacenar la correspondencia entre el nombre de la columna y el índice cuando hay encabezados.
  • Una matriz dinámica de CArrayString* para filas, donde cada CArrayString es una fila de campos.
  • Algunas propiedades almacenadas como _hasHeader, _filename, _separator y quizás _rowCount y _colCount.

El uso de CArrayObj y CArrayString no solo es cómodo, sino que también te ayuda a evitar los dolores de cabeza que supone el redimensionamiento de matrices de bajo nivel. Las matrices nativas son potentes, pero pueden resultar complicadas con conjuntos de datos complejos. Con CArrayString, añadir campos es fácil, y CArrayObj te permite almacenar una lista creciente de filas sin complicaciones. Por otra parte, un mapa hash para los nombres de las columnas evita tener que escanear la línea del encabezado una y otra vez. Es un diseño sencillo y escalable, que te facilita la vida a medida que crece tu CSV o cambian tus necesidades de datos.

Antes de codificar toda la clase, escribamos algunos fragmentos de código básicos para ilustrar cómo abrir un archivo y leer líneas. Más adelante, integraremos estas piezas en el código final de la clase. Abrimos un archivo:

int fileHandle = FileOpen("data.csv", FILE_READ|FILE_TXT);
if(fileHandle == INVALID_HANDLE)
  {
   Print("Error: Could not open file data.csv. Error code=", _LastError);
   return;
  }

// If we reach here, the file is open successfully.

Este fragmento intenta abrir el archivo data.csv desde el directorio MQL5/Files. Si falla, imprime un error y regresa. La variable _LastError puede proporcionar información sobre por qué no se pudo abrir el archivo. Por ejemplo, 5004 significa CANNOT_OPEN_FILE. Ahora leamos el archivo hasta que termine:

string line;
while(!FileIsEnding(fileHandle))
  {
   line = FileReadString(fileHandle);
   if(line == "" && _LastError != 0) // If empty line and error occurred
     {
      Print("Error reading line. Posiblemente sea el final del archivo u otro problema. Error=", _LastError);
      break;
     }

   // Process the line here, e.g., split it into fields
}

Aquí hacemos un bucle hasta que FileIsEnding() devuelva verdadero. Cada iteración lee una línea en línea. Si obtenemos una línea vacía y hay un error, nos detenemos. Si realmente es el final del archivo, saldremos naturalmente del bucle. Ten en cuenta que una línea completamente vacía en el archivo seguiría dando como resultado una cadena vacía, por lo que es posible que desees gestionar esa situación en función de tu formato CSV.

Ahora, supongamos que nuestro CSV utiliza un punto y coma ( ; ) como delimitador. Podemos hacer:

string line = "Symbol;Price;Volume";
string fields[];
int fieldCount = StringSplit(line, ';', fields);

if(fieldCount < 1)
  {
   Print("No fields found in line: ", line);
  }
else
  {
   // fields now contains each piece of data
   for(int i=0; i<fieldCount; i++)
     Print("Field[", i, "] = ", fields[i]);
}

StringSplit() devuelve el número de partes encontradas. Después de esta llamada, los campos contienen cada token separado por ; . Si la línea fuera EURUSD;1,2345;10000, fields[0] sería EURUSD, fields[1] sería 1,2345 y fields[2] sería 10000.

Si _hasHeader es verdadero, la primera línea que leemos es especial. Lo dividiremos y almacenaremos los nombres de las columnas en un CHashMap. Por ejemplo:

#include <Generic\HashMap.mqh>

CHashMap<string,uint> Columns; // columnName -> columnIndex

// Assume line is the header line
string columns[];
int columnCount = StringSplit(line, ';', columns);

for(int i=0; i<columnCount; i++)
  Columns.Add(columns[i], i);

El mapa hash para los nombres de las columnas es un pequeño detalle que reporta grandes beneficios. Sin él, tendrías que recorrer los encabezados de columna cada vez que quisieras un índice. Con un mapa hash, una rápida llamada a TryGetValue() te da el índice, y si no se encuentra una columna, puedes simplemente devolver un valor predeterminado. Si aparecen duplicados o nombres de columnas extraños, puedes detectarlos de antemano. Esta configuración mantiene las búsquedas rápidas y el código limpio, por lo que, aunque el tamaño del CSV se duplique, recuperar los índices de las columnas sigue siendo sencillo.

Ahora Columns asigna cada nombre de columna a su índice. Si más adelante necesitamos el índice para un nombre de columna determinado, podemos hacer lo siguiente:

uint idx;
bool found = Columns.TryGetValue("Volume", idx);
if(found)
  Print("Volume column index = ", idx);
else
  Print("Column 'Volume' not found");

Cada fila de datos debe almacenarse en un objeto CArrayString, y mantendremos una matriz dinámica de punteros a estas filas. Algo así como:

#include <Arrays\ArrayString.mqh>
#include <Arrays\ArrayObj.mqh>

CArrayObj Rows; // holds pointers to CArrayString objects

// After reading and splitting a line into fields:
// (Assume fields[] array is populated)

CArrayString *row = new CArrayString;
for(int i=0; i<ArraySize(fields); i++)
  row.Add(fields[i]);

Rows.Add(row);

Más adelante, para recuperar un valor, haríamos algo como esto:

CArrayString *aRow = Rows.At(0); // get the first row
string value = aRow.At(1);       // get second column
Print("Value at row=0, col=1: ", value);

Por supuesto, siempre debemos comprobar los límites para evitar errores fuera de rango.

Accedamos a las columnas por nombre o índice. Si nuestro CSV tiene un encabezado, podemos usar el mapa de columnas para encontrar índices de columnas por nombre:

string GetValueByName(uint rowNumber, string colName, string errorValue="")
  {
   uint idx;
   if(!Columns.TryGetValue(colName, idx))
     return errorValue; // column not found

   return GetValueByIndex(rowNumber, idx, errorValue);
  }

string GetValueByIndex(uint rowNumber, uint colIndex, string errorValue="")
  {
   if(rowNumber >= Rows.Total())
     return errorValue; // invalid row
   CArrayString *aRow = Rows.At(rowNumber);
   if(colIndex >= (uint)aRow.Total())
     return errorValue; // invalid column index

   return aRow.At(colIndex);
  }

Este pseudocódigo muestra cómo podríamos implementar dos funciones de acceso. GetValueByName usa el mapa hash para convertir el nombre de la columna en un índice y luego llama a GetValueByIndex . GetValueByIndex verifica los límites y devuelve valores o valores predeterminados de error según sea necesario.

Constructor y Destructor: Podemos envolver todo en una clase. El constructor podría simplemente inicializar variables internas y el destructor debería liberar memoria. Por ejemplo:

class CSimpleCSVReader
  {
private:
   bool              _hasHeader;
   string            _separator;
   CHashMap<string,uint> Columns;
   CArrayObj         Rows;

public:
                    CSimpleCSVReader() { _hasHeader = true; _separator=";"; }
                   ~CSimpleCSVReader() { Clear(); }

   void             SetHasHeader(bool hasHeader) { _hasHeader = hasHeader; }
   void             SetSeparator(string sep) { _separator = sep; }

   uint             Load(string filename);
   string           GetValueByName(uint rowNum, string colName, string errorVal="");
   string           GetValueByIndex(uint rowNum, uint colIndex, string errorVal="");

private:
   void             Clear()
                     {
                      for(int i=0; i<Rows.Total(); i++)
                        {
                         CArrayString *row = Rows.At(i);
                         if(row != NULL) delete row;
                        }
                      Rows.Clear();
                      Columns.Clear();
                     }
  };

Este boceto de una clase muestra una posible estructura. Aún no hemos implementado Load(), pero lo haremos pronto. Observe cómo mantenemos un método Clear() para liberar memoria. Después de llamar a delete row; , también debemos usar Rows.Clear() para restablecer la matriz de punteros.

Implementemos ahora el método de carga. Load() abrirá el archivo, leerá la primera línea (posiblemente el encabezado), leerá todas las líneas restantes y las analizará:

uint CSimpleCSVReader::Load(string filename)
  {
   // Clear any previous data
   Clear();

   int fileHandle = FileOpen(filename, FILE_READ|FILE_TXT);
   if(fileHandle == INVALID_HANDLE)
     {
      Print("Error opening file: ", filename, " err=", _LastError);
      return 0;
     }

   if(_hasHeader)
     {
      // read first line as header
      if(!FileIsEnding(fileHandle))
        {
         string headerLine = FileReadString(fileHandle);
         string headerFields[];
         int colCount = StringSplit(headerLine, StringGetCharacter(_separator,0), headerFields);
         for(int i=0; i<colCount; i++)
           Columns.Add(headerFields[i], i);
        }
     }

   uint rowCount=0;
   while(!FileIsEnding(fileHandle))
     {
      string line = FileReadString(fileHandle);
      if(line == "") continue; // skip empty lines

      string fields[];
      int fieldCount = StringSplit(line, StringGetCharacter(_separator,0), fields);
      if(fieldCount<1) continue; // no data?

      CArrayString *row = new CArrayString;
      for(int i=0; i<fieldCount; i++)
        row.Add(fields[i]);
      Rows.Add(row);
      rowCount++;
     }

   FileClose(fileHandle);
   return rowCount;
  }

Esta función Load():

  • Borra datos antiguos.
  • Abre el archivo.
  • Si _hasHeader es verdadero, lee la primera línea como encabezado y llena Columns.
  • Luego lee líneas hasta que finaliza el archivo, dividiéndolas en campos.
  • Para cada línea, crea un CArrayString, lo rellena y lo agrega a Rows.
  • Devuelve el número de filas de datos leídas.

Para resumir todo, ahora tenemos una buena parte de la lógica delineada. En las siguientes secciones, refinaremos y finalizaremos el código, agregaremos los métodos de acceso faltantes y mostraremos la lista final del código completo. También demostraremos ejemplos de uso, como cómo verificar cuántas filas tienes, qué columnas existen y cómo obtener valores de forma segura.

Al recorrer estos fragmentos de código, podrá ver cómo encajan las piezas lógicas. La clase final del lector CSV será autónoma y fácil de integrar: simplemente cree una instancia, llame a Load("myfile.csv"), luego use GetValueByName() o GetValueByIndex() para recuperar la información que necesita.

En la siguiente sección, completaremos toda la implementación de la clase y mostraremos un fragmento de código final listo para que lo copie y adapte. Después de esto, finalizaremos con algunos ejemplos de uso y observaciones finales.


Finalización de la implementación de la clase de lectura CSV

En las secciones anteriores, describimos la estructura de nuestro lector CSV y trabajamos en varias partes del código. Ahora es el momento de ponerlo todo junto en una implementación única y cohesiva. A continuación mostraremos brevemente cómo utilizarlo. En la estructura final del artículo, presentaremos el código completo de una vez aquí, para que tengas una referencia clara.

Integraremos las funciones auxiliares que analizamos (cargar archivos, analizar encabezados, almacenar filas y métodos de acceso) en una sola clase MQL5. A continuación, mostraremos un breve fragmento que demuestra cómo podría utilizar la clase en su EA o script. Recordemos que esta clase:

  • Lee un CSV del directorio MQL5/Files.
  • Si _hasHeader es verdadero, extrae los nombres de las columnas de la primera línea.
  • Las líneas subsiguientes forman filas de datos almacenadas en CArrayString.
  • Puede recuperar valores por nombre de columna (si existe un encabezado) o por índice de columna.

También incluiremos algunas comprobaciones de errores y valores predeterminados. Ahora vamos a presentar el código completo. Tenga en cuenta que este código es un ejemplo ilustrativo y puede requerir pequeños ajustes según su entorno. Suponemos que los archivos HashMap.mqh, ArrayString.mqh y ArrayObj.mqh están disponibles en los directorios de inclusión estándar de MQL5.

Aquí está el listado completo del código del lector CSV:

//+------------------------------------------------------------------+
//|  CSimpleCSVReader.mqh                                            |
//|  A simple CSV reader class in MQL5.                              |
//|  Assumes CSV file is located in MQL5/Files.                      |
//|  By default, uses ';' as the separator and treats first line as  |
//|  header. If no header, columns are accessed by index only.       |
//+------------------------------------------------------------------+
#include <Generic\HashMap.mqh>
#include <Arrays\ArrayObj.mqh>
#include <Arrays\ArrayString.mqh>

class CSimpleCSVReader
  {
private:
   bool                  _hasHeader;
   string                _separator;
   CHashMap<string,uint> Columns;
   CArrayObj             Rows;          // Array of CArrayString*, each representing a data row

public:
                        CSimpleCSVReader()
                          {
                           _hasHeader = true;
                           _separator = ";";
                          }
                       ~CSimpleCSVReader()
                          {
                           Clear();
                          }

   void                 SetHasHeader(bool hasHeader) {_hasHeader = hasHeader;}
   void                 SetSeparator(string sep) {_separator = sep;}

   // Load: Reads the file, returns number of data rows.
   uint                 Load(string filename);

   // GetValue by name or index: returns specified cell value or errorVal if not found
   string               GetValueByName(uint rowNum, string colName, string errorVal="");
   string               GetValueByIndex(uint rowNum, uint colIndex, string errorVal="");

   // Returns the number of data rows (excluding header)
   uint                 RowCount() {return Rows.Total();}

   // Returns the number of columns. If no header, returns column count of first data row
   uint                 ColumnCount()
                         {
                          if(Columns.Count() > 0)
                            return Columns.Count();
                          // If no header, guess column count from first row if available
                          if(Rows.Total()>0)
                            {
                             CArrayString *r = Rows.At(0);
                             return (uint)r.Total();
                            }
                          return 0;
                         }

   // Get column name by index if header exists, otherwise return empty or errorVal
   string               GetColumnName(uint colIndex, string errorVal="")
                         {
                          if(Columns.Count()==0)
                            return errorVal;
                          // Extract keys and values from Columns
                          string keys[];
                          int vals[];
                          Columns.CopyTo(keys, vals);
                          if(colIndex < (uint)ArraySize(keys))
                            return keys[colIndex];
                          return errorVal;
                         }

private:
   void                 Clear()
                         {
                          for(int i=0; i<Rows.Total(); i++)
                            {
                             CArrayString *row = Rows.At(i);
                             if(row != NULL) delete row;
                            }
                          Rows.Clear();
                          Columns.Clear();
                         }
  };

//+------------------------------------------------------------------+
//| Implementation of Load() method                                  |
//+------------------------------------------------------------------+
uint CSimpleCSVReader::Load(string filename)
  {
   Clear(); // Start fresh

   int fileHandle = FileOpen(filename, FILE_READ|FILE_TXT);
   if(fileHandle == INVALID_HANDLE)
     {
      Print("CSVReader: Error opening file: ", filename, " err=", _LastError);
      return 0;
     }

   uint rowCount=0;

   // If hasHeader, read first line as header
   if(_hasHeader && !FileIsEnding(fileHandle))
     {
      string headerLine = FileReadString(fileHandle);
      if(headerLine != "")
        {
         string headerFields[];
         int colCount = StringSplit(headerLine, StringGetCharacter(_separator,0), headerFields);
         for(int i=0; i<colCount; i++)
           Columns.Add(headerFields[i], i);
        }
     }

   while(!FileIsEnding(fileHandle))
     {
      string line = FileReadString(fileHandle);
      if(line == "") continue; // skip empty lines

      string fields[];
      int fieldCount = StringSplit(line, StringGetCharacter(_separator,0), fields);
      if(fieldCount<1) continue; // no data?

      CArrayString *row = new CArrayString;
      for(int i=0; i<fieldCount; i++)
        row.Add(fields[i]);
      Rows.Add(row);
      rowCount++;
     }

   FileClose(fileHandle);
   return rowCount;
  }

//+------------------------------------------------------------------+
//| GetValueByIndex Method                                           |
//+------------------------------------------------------------------+
string CSimpleCSVReader::GetValueByIndex(uint rowNum, uint colIndex, string errorVal="")
  {
   if(rowNum >= Rows.Total())
     return errorVal;
   CArrayString *aRow = Rows.At(rowNum);
   if(aRow == NULL) return errorVal;
   if(colIndex >= (uint)aRow.Total())
     return errorVal;
   string val = aRow.At(colIndex);
   return val;
  }

//+------------------------------------------------------------------+
//| GetValueByName Method                                            |
//+------------------------------------------------------------------+
string CSimpleCSVReader::GetValueByName(uint rowNum, string colName, string errorVal="")
  {
   if(Columns.Count() == 0)
     {
      // No header, can't lookup by name
      return errorVal;
     }

   uint idx;
   bool found = Columns.TryGetValue(colName, idx);
   if(!found) return errorVal;

   return GetValueByIndex(rowNum, idx, errorVal);
  }

//+------------------------------------------------------------------+

Echemos un vistazo más de cerca a Load(). Borra datos antiguos, intenta abrir el archivo y, si _hasHeader es verdadero, lee una línea como encabezado. Luego divide y almacena los nombres de las columnas. Después de eso, recorre el archivo, línea por línea, ignorando las líneas vacías y dividiendo las válidas en campos. Cada conjunto de campos se convierte en una fila CArrayString en Rows. Al final, sabrás exactamente cuántas filas tienes y Columns estará listo para realizar búsquedas basadas en nombres. Este flujo sencillo significa que su EA puede adaptarse fácilmente si el CSV de mañana tiene más filas o un formato ligeramente diferente.

En relación con los métodos GetValueByName() y GetValueByIndex(). Estos métodos de acceso son su interfaz principal con los datos. Son seguros porque siempre comprueban los límites. Si solicita una fila o columna que no existe, obtendrá un valor predeterminado inofensivo en lugar de un bloqueo. Si no hay encabezado, GetValueByName() devuelve un valor de error de forma elegante. De esta manera, aunque falte algo en tu CSV o _hasHeader esté configurado incorrectamente, tu EA no dejará de funcionar. Puede añadir una instrucción Print() rápida si desea registrar estas discrepancias, pero es opcional. La cuestión es que estos métodos mantienen tu flujo de trabajo fluido y sin errores.

Si params.csv tiene este aspecto:

Symbol;MaxLot;MinSpread
EURUSD;0.20;1
GBPUSD;0.10;2

Salida:

Se han cargado 2 filas de datos.
Primera fila: Symbol=EURUSD MaxLot=0.20 MinSpread=1

Y si desea acceder por índice en lugar de por nombre:

// Access second row, second column (MaxLot) by index:
string val = csv.GetValueByIndex(1, 1, "N/A");
Print("Second row, second column:", val);

Esto debería imprimir 0.10, correspondiente al MaxLot del GBPUSD.

¿Qué ocurre si no hay encabezado? Si _hasHeader se establece en falso, omitimos la creación del mapa de columnas. Luego debes confiar en GetValueByIndex() para acceder a los datos. Por ejemplo, si tu CSV no tiene encabezados y cada fila tiene tres campos, sabes que:

  • Columna 0: Símbolo
  • Columna 1: Precio
  • Columna 2: Comentario

Puede llamar directamente a csv.GetValueByIndex(rowNum, 0) para obtener el símbolo.

¿Qué hay del manejo de errores? Nuestro código devuelve valores predeterminados si falta algo, como una columna o fila inexistente. También imprime errores si no se puede abrir el archivo. En la práctica, es posible que desee un registro más robusto. Por ejemplo, si dependes en gran medida de datos externos, considera comprobar rows = csv.Load(«file.csv») y, si rows == 0, gestiona la situación con elegancia. Quizás interrumpas la inicialización de EA o vuelvas a los valores predeterminados internos.

No hemos implementado un manejo de errores extremo para archivos CSV malformados o codificaciones inusuales. Para escenarios más complejos, añada comprobaciones. Si ColumnCount() es cero, tal vez se debería registrar una advertencia. Si no existe una columna necesaria, imprima un mensaje en la pestaña Expertos.

Echemos un vistazo al rendimiento: para archivos CSV pequeños y medianos, este enfoque funciona perfectamente. Si necesita manejar archivos extremadamente grandes, considere estructuras de datos más eficientes o un enfoque de streaming. Sin embargo, para el uso típico de EA, como leer unos cientos o miles de líneas, esto funcionará adecuadamente.

Ahora tenemos un lector CSV completo. En la siguiente (y última) sección, analizaremos brevemente las pruebas, proporcionaremos algunos escenarios de uso y concluiremos con unas observaciones finales. Obtendrá una clase de lector CSV lista para usar que se integra perfectamente con sus EA o scripts MQL5.


Escenarios de prueba y uso

Una vez completada la implementación del lector CSV, es aconsejable confirmar que todo funciona según lo previsto. La prueba es sencilla: cree un pequeño archivo CSV, colóquelo en MQL5/Files y escriba un EA que lo cargue e imprima algunos resultados. A continuación, puede consultar la pestaña Expertos para comprobar si los valores son correctos. Aquí hay algunas sugerencias para la prueba:

  1. Prueba básica con encabezado: Cree un archivo test.csv como:

    Symbol;Spread;Comment
    EURUSD;1;Major Pair
    USDJPY;2;Another Major

    Cárgalo con:

    CSimpleCSVReader csv;
    csv.SetHasHeader(true);
    csv.SetSeparator(";");
    uint rows = csv.Load("test.csv");
    Print("Rows loaded: ", rows);
    Print("EURUSD Spread: ", csv.GetValueByName(0, "Spread", "N/A"));
    Print("USDJPY Comment: ", csv.GetValueByName(1, "Comment", "N/A"));
    

    Comprueba el resultado. Si muestra «Rows loaded: 2», «EURUSD Spread: 1» y «USDJPY Comment: Another Major», entonces está funcionando.

    ¿Qué pasa si el CSV no es perfectamente uniforme? Supongamos que una fila tiene menos columnas de lo esperado. Nuestro enfoque no impone la coherencia. Si a una fila le falta un campo, al solicitar esa columna se devuelve un valor predeterminado. Esto es adecuado si puede manejar datos parciales, pero si necesita un formato estricto, considere verificar el recuento de columnas después de Load(). Para archivos enormes, este método sigue funcionando bien, aunque si estás enviando decenas de miles de líneas, quizá debas empezar a pensar en optimizaciones de rendimiento o en la carga parcial. Para las necesidades cotidianas (CSV pequeños y medianos), esta configuración es más que suficiente.

  2. Prueba sin encabezado: Si establece csv.SetHasHeader(false); y utiliza un archivo sin encabezado:

    EURUSD;1;Major Pair
    USDJPY;2;Another Major
    
    Ahora debe acceder a las columnas por índice:
    string val = csv.GetValueByIndex(0, 0, "N/A"); // should be EURUSD
    Print("Row0 Col0: ", val);
    
    Confirme que el resultado coincide con sus expectativas.

  3. Columnas o filas que faltan: Intente solicitar un nombre de columna que no existe o una fila más allá de los datos cargados. Deberías obtener los valores de error predeterminados que proporcionaste. Por ejemplo:
    string nonExistent = csv.GetValueByName(0, "NonExistentColumn", "MISSING");
    Print("NonExistent: ", nonExistent);
    
    Esto debería imprimir MISSING en lugar de bloquearse.

  4. Archivos más grandes: Si tiene un archivo con más filas, cárguelo y confirme el número de filas. Comprueba que el uso de la memoria y el rendimiento sigan siendo razonables. Este paso ayuda a garantizar que el enfoque sea lo suficientemente sólido para su escenario. 

Ten en cuenta también las codificaciones de caracteres y los símbolos poco habituales. La mayoría de los archivos CSV son ASCII sin formato o UTF-8, que MQL5 maneja muy bien. Si alguna vez aparecen caracteres extraños, puede ser útil convertir primero el archivo a una codificación más compatible. Del mismo modo, si tu CSV tiene espacios en blanco al final o puntuación extraña, recortar los campos después de dividirlos garantiza datos más limpios. Probar ahora estos escenarios «menos bonitos» garantiza que, cuando su EA se ejecute de verdad, no se atasque con un formato de archivo ligeramente diferente o un glifo inesperado.

Escenarios de uso:

  • Parámetros externos:
    Supongamos que tienes un archivo CSV con los parámetros de una estrategia. Cada fila puede definir un símbolo y algunos umbrales. En lugar de codificar estos valores en su EA, puede cargarlos al inicio, iterar sobre las filas y aplicarlos dinámicamente. Cambiar los parámetros es tan fácil como editar el CSV, sin necesidad de recompilar.

  • Gestión de la lista de seguimiento:
    Puede almacenar una lista de símbolos para operar en un archivo CSV. Su EA puede leer esta lista en tiempo de ejecución, lo que le permite añadir o eliminar instrumentos rápidamente sin tocar el código. Por ejemplo, un CSV podría tener:

    Symbol
    EURUSD
    GBPUSD
    XAUUSD
    
    Leer este archivo y recorrer las filas en su EA le permite adaptar los símbolos negociados sobre la marcha.

  • Integración con otras herramientas: Si dispone de un script de Python u otra herramienta que genere análisis CSV, como señales personalizadas o previsiones, puede exportar los datos a CSV y hacer que su EA los importe en MQL5. Esto cierra la brecha entre los diferentes ecosistemas de programación.

Conclusión

Ahora hemos explorado los fundamentos de las operaciones con archivos MQL5, hemos aprendido a leer archivos de texto línea por línea de forma segura, a analizar líneas CSV en campos y a almacenarlos para poder recuperarlos fácilmente por nombres de columna o índices. Al presentar el código completo para un sencillo lector CSV, hemos proporcionado un elemento básico que puede mejorar sus estrategias de trading automatizadas.

Esta clase de lector CSV no es solo un fragmento de código, sino una herramienta práctica que puedes adaptar a tus necesidades. ¿Necesitas un delimitador diferente? Cambiar _separator. ¿No hay encabezado en tu archivo? Establece _hasHeader en falso y confía en los índices. El enfoque es flexible y transparente, lo que le permite integrar datos externos de forma limpia. A medida que continúe desarrollando ideas de trading más complejas, es posible que desee ampliar aún más este lector CSV, añadiendo un manejo de errores más robusto, admitiendo diferentes codificaciones o incluso escribiendo de nuevo en archivos CSV. Por ahora, esta base debería cubrir la mayoría de los escenarios básicos.

Recuerde, contar con datos fiables es fundamental para desarrollar una lógica comercial sólida. Con la capacidad de importar datos externos desde archivos CSV, puede aprovechar una gama más amplia de información sobre el mercado, configuraciones y conjuntos de parámetros, todos ellos controlados dinámicamente por sencillos archivos de texto en lugar de valores codificados, y si sus necesidades se vuelven más complejas, como manejar múltiples delimitadores, ignorar ciertas líneas o admitir campos entre comillas, solo tiene que modificar el código. Esa es la ventaja de tener tu propio lector CSV: es una base sólida que puedes perfeccionar a medida que evolucionan tu estrategia y tus fuentes de datos. Con el tiempo, incluso podrías crear un pequeño conjunto de herramientas de datos a su alrededor, siempre listo para proporcionar a tu EA nuevos conocimientos sin tener que reescribir la lógica central desde cero.

¡Feliz codificación y feliz trading!

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/16614

Archivos adjuntos |
Neurona biológica para la previsión de series temporales financieras Neurona biológica para la previsión de series temporales financieras
Construimos un sistema de neuronas biológicamente correcto para la predicción de series temporales. La introducción de un medio similar al plasma en la arquitectura de una red neuronal ha creado una especie de "mente colectiva", en la que cada neurona influye en el trabajo del sistema no solo a través de conexiones directas, sino también mediante interacciones electromagnéticas de largo alcance. ¿Cómo se comportará el sistema de modelización neural del cerebro en el mercado?
Redes neuronales en el trading: Transformador jerárquico de doble torre (Hidformer) Redes neuronales en el trading: Transformador jerárquico de doble torre (Hidformer)
Hoy le proponemos introducir un framework de transformador jerárquico de dos torres (Hidformer) desarrollado para la previsión de series temporales y el análisis de datos. Los autores del framework propusieron varias mejoras en la arquitectura del Transformer que mejoran la precisión de las predicciones y reducen el consumo de recursos computacionales.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Automatización de estrategias de trading en MQL5 (Parte 2): El sistema Kumo Breakout con Ichimoku y Awesome Oscillator Automatización de estrategias de trading en MQL5 (Parte 2): El sistema Kumo Breakout con Ichimoku y Awesome Oscillator
En este artículo, creamos un Asesor Experto (EA) que automatiza la estrategia Kumo Breakout utilizando el indicador Ichimoku Kinko Hyo y el Awesome Oscillator. Recorremos el proceso de inicialización de los indicadores, detección de condiciones de ruptura y codificación de entradas y salidas automáticas en las operaciones. Además, implementamos trailing stops y lógica de gestión de posiciones para mejorar el rendimiento del EA y su adaptabilidad a las condiciones del mercado.