Principios de las operaciones de base de datos en MQL5

Las bases de datos almacenan información en forma de tablas. La obtención, modificación y adición de nuevos datos se realiza mediante consultas en lenguaje SQL. Describiremos sus particularidades en las secciones siguientes. Mientras tanto, utilicemos el script DatabaseRead.mq5, que no tiene nada que ver con el trading, y veamos cómo crear una base de datos sencilla y obtener información de ella. Todas las funciones mencionadas aquí se describirán en detalle más adelante. Ahora es importante imaginar los principios generales.

La creación y cierre de una base de datos incorporada mediante funciones DatabaseOpen/DatabaseClose integradas se realiza de forma similar a como se hace con los archivos, ya que también creamos un descriptor para la base de datos, lo comprobamos y lo cerramos al final.

void OnStart()
{
   string filename = "company.sqlite";
   // create or open a database
   int db = DatabaseOpen(filenameDATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(db == INVALID_HANDLE)
   {
      Print("DB: "filename" open failed with code "_LastError);
      return;
   }
   ...// further work with the database
   // close the database
   DatabaseClose(db);
}

Después de abrir la base de datos, nos aseguraremos de que no haya ninguna tabla en ella con el nombre que necesitamos. Si la tabla ya existe, al intentar insertar en ella los mismos datos que en nuestro ejemplo, se producirá un error, por lo que utilizaremos la función DatabaseTableExists.

La eliminación y creación de una tabla se realiza mediante consultas que se envían a la base de datos con dos llamadas a la función DatabaseExecute y acompañadas de una comprobación de errores.

   ...
   // if the table COMPANY exists, then delete it
   if(DatabaseTableExists(db"COMPANY"))
   {
      if(!DatabaseExecute(db"DROP TABLE COMPANY"))
      {
         Print("Failed to drop table COMPANY with code "_LastError);
         DatabaseClose(db);
         return;
      }
   }
   // creating table COMPANY 
   if(!DatabaseExecute(db"CREATE TABLE COMPANY("
     "ID      INT     PRIMARY KEY NOT NULL,"
     "NAME    TEXT    NOT NULL,"
     "AGE     INT     NOT NULL,"
     "ADDRESS CHAR(50),"
     "SALARY  REAL );"))
   {
      Print("DB: "filename" create table failed with code "_LastError);
      DatabaseClose(db);
      return;
   }
   ...

Vamos a explicar la esencia de las consultas SQL. En la tabla EMPRESA tenemos solo 5 campos: ID de registro, nombre, edad, dirección y salario. Aquí, el campo ID es una clave, es decir, un índice único. Los índices permiten identificar unívocamente cada registro y pueden utilizarse en todas las tablas para vincularlas entre sí. Esto es similar a cómo el ID de posición vincula todas las operaciones y órdenes que pertenecen a una posición en particular.

Ahora necesita llenar la tabla con datos, lo que se hace mediante la consulta «INSERT»:

   // insert data into table
   if(!DatabaseExecute(db,
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (1,'Paul',32,'California',25000.00); "
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (2,'Allen',25,'Texas',15000.00); "
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (3,'Teddy',23,'Norway',20000.00);"
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (4,'Mark',25,'Rich-Mond',65000.00);"))
   {
      Print("DB: "filename" insert failed with code "_LastError);
      DatabaseClose(db);
      return;
   }
   ...

Aquí se añaden 4 registros a la tabla EMPRESA; para cada registro hay una lista de campos, y se indican los valores que se escribirán en estos campos. Los registros se insertan mediante consultas «INSERT...» separadas, que se combinan en una sola línea, mediante un carácter delimitador especial ';', pero podríamos insertar cada registro en la tabla con una llamada separada a DatabaseExecute.

Como al final del script la base de datos se guardará en el archivo «empresa.sqlite», la próxima vez que se ejecute intentaríamos escribir los mismos datos en la tabla EMPRESA con el mismo ID. Esto provocaría un error, razón por la cual previamente borramos la tabla para empezar de cero cada vez que se ejecutara el script.

Ahora obtenemos todos los registros de la tabla EMPRESA con el campo SALARY > 15000. Para ello se utiliza la función DatabasePrepare, que «compila» el texto de la solicitud y devuelve su controlador para su posterior uso en las funciones DatabaseRead o DatabaseReadBind.

   // prepare a request with a descriptor
   int request = DatabasePrepare(db"SELECT * FROM COMPANY WHERE SALARY>15000");
   if(request == INVALID_HANDLE)
   {
      Print("DB: "filename" request failed with code "_LastError);
      DatabaseClose(db);
      return;
   }
   ...

Una vez que la solicitud se ha creado correctamente, necesitamos obtener los resultados de su ejecución. Esto puede hacerse utilizando la función DatabaseRead, que en la primera llamada ejecutará la consulta y saltará al primer registro de los resultados. En cada llamada posterior, leerá el siguiente registro hasta llegar al final. En este caso, devolverá false, lo que significa «no hay más registros».

   // printing all records with salary over 15000
   int idage;
   string nameaddress;
   double salary;
   Print("Persons with salary > 15000:");
   for(int i = 0DatabaseRead(request); i++)
   {
      // read the values of each field from the received record by its number
      if(DatabaseColumnInteger(request0id) && DatabaseColumnText(request1name) &&
         DatabaseColumnInteger(request2age) && DatabaseColumnText(request3address) &&
         DatabaseColumnDouble(request4salary))
         Print(i":  "id" "name" "age" "address" "salary);
      else
      {
         Print(i": DatabaseRead() failed with code "_LastError);
         DatabaseFinalize(request);
         DatabaseClose(db);
         return;
      }
   }
   // deleting handle after use
   DatabaseFinalize(request);

El resultado de la ejecución será:

Persons with salary > 15000:
0:  1 Paul 32 California 25000.0
1:  3 Teddy 23 Norway 20000.0
2:  4 Mark 25 Rich-Mond  65000.0

La función DatabaseRead permite recorrer todos los registros del resultado de la consulta y, a continuación, obtener información completa sobre cada columna de la tabla resultante a través de las funciones DatabaseColumn. Estas funciones están diseñadas para trabajar de forma universal con los resultados de cualquier consulta, pero el coste es un código redundante.

Si se conoce de antemano la estructura de los resultados de la consulta, es mejor utilizar la función DatabaseReadBind, que permite leer todo el registro de una vez en una estructura. Podemos rehacer así el ejemplo anterior y presentarlo con un nuevo nombre DatabaseReadBind.mq5. En primer lugar, declaremos la estructura Person:

struct Person
{
   int    id;
   string name;
   int    age;
   string address;
   double salary;
};

A continuación, restaremos cada registro de los resultados de la consulta con DatabaseReadBind(request, person) en un bucle mientras la función devuelva true:

   Person person;
   Print("Persons with salary > 15000:");
   for(int i = 0DatabaseReadBind(requestperson); i++)
      Print(i":  "person.id" "person.name" "person.age,
         " "person.address" "person.salary);
   DatabaseFinalize(request);

De este modo, obtenemos inmediatamente los valores de todos los campos del registro actual y no necesitamos leerlos por separado.

Este ejemplo introductorio se ha extraído del artículo SQLite: trabajo nativo con bases de datos en SQL en MQL5, donde, además, se contemplan varias opciones de aplicación de la base de datos para operadores de trading. En concreto, puede encontrar allí la restauración del historial de posiciones de las operaciones, el análisis de un informe de trading en términos de estrategias, símbolos de trabajo u horas de trading preferidas, así como técnicas para trabajar con los resultados de la optimización.

Para dominar este material pueden ser necesarios algunos conocimientos básicos de SQL, por lo que los trataremos brevemente en las siguientes secciones.