SQLite: trabajo nativo con bases de datos en SQL en MQL5

20 febrero 2020, 13:35
MetaQuotes
0
720

Contenido


Trading algorítmico moderno en MetaTrader 5

MQL5 es una solución ideal para el trading algorítmico, dado que es muy semejante al lenguaje С++ tanto por su sintaxis, como por su velocidad de cálculo. La plataforma MetaTrader 5 ofrece a los tráders un lenguaje especializado y moderno para la escritura de robots comerciales e indicadores técnicos, que permite salir del marco de las tareas comerciales: aquí usted podrá crear sistemas analíticos con cualquier nivel de dificultad.

Además de funciones comerciales asincrónicas y bibliotecas matemáticas, los tráders tienen a su disposición funciones de red, la posibilidad de importar datos a Python, cálculos paralelos en OpenCL, soporte nativo de bibliotecas .NET con importación "inteligente" de funciones, integración con MS Visual Studio y visualización de datos con ayuda de DirectX. Estamos hablando de las herramientas imprescindibles para cualquier tráder algorítmico moderno, que permiten resolver tareas de lo más diverso sin salir de la plataforma MetaTrader 5.


Funciones para trabajar con las bases de datos

El desarrollo de estrategias comerciales está relacionado con el procesamiento de grandes volúmenes de datos. Hoy en día, ya no es suficiente con escribir un algoritmo comercial en forma de programa MQL5 rápido y fiable. Para lograr resultados fiables, el tráder también debe llevar a cabo un enorme número de pruebas y optimizaciones con las herramientas más variadas, guardar los resultados, procesarlos, analizarlos y decidir qué hacer a continuación.

Ahora, usted podrá trabajar directamente en MQL5 con las bases de datos en SQLite, un motor sencillo y popular. Los resultados de las pruebas en el sitio web de los desarrolladores muestran una alta velocidad de ejecución de las solicitudes SQL: en la mayoría de tareas, el motor ha superado a PostgreSQL y MySQL. Nosotros, por nuestra parte, hemos comparado la velocidad de ejecución de estas pruebas en MQL5 y LLVM 9.0.0, insertando después los resultados en un recuadro. Los resultados de la ejecución se dan en milisengundos, cuanto menor sea, mejor.

Nombre
Descripción
 LLVM  
MQL5
Test 1
 1000 INSERTs
11572
8488
Test 2
 25000 INSERTs in a transaction
59
60
Test 3
 25000 INSERTs into an indexed table
102
105
Test 4
 100 SELECTs without an index
142
150
Test 5
 100 SELECTs on a string comparison
391
390
Test 6
 Creating an index
43
33
Test 7
 5000 SELECTs with an index
385
307
Test 8
 1000 UPDATEs without an index
58
54
Test 9
 25000 UPDATEs with an index
161
165
Test 10
 25000 text UPDATEs with an index
124
120
Test 11  INSERTs from a SELECT
84
84
Test 12
 DELETE without an index
25
74
Test 13
 DELETE with an index
70
72
Test 14  A big INSERT after a big DELETE
62
66
Test 15  A big DELETE followed by many small INSERTs
33
33
Test 16  DROP TABLE: finished
42
40

Podrá encontrar los detalles de las pruebas en milisegundos en el archivo SqLiteTest.zip adjunto. Estas son las características técnicas de la computadora en la que se han realizado las mediciones: Windows 10 x64, Intel Xeon E5-2690 v3 @ 2.60GHz. 

Los resutados preliminares muestran que usted puede confiar en obtener el máximo rendimiento al trabajar con bases de datos en MQL5. Aquellos que nunca antes se han encontrado con SQL, se verán gratamente sorprendidos: multitud de tareas en el lenguaje de solicitudes estructuradas se resuelven de forma rápida y elegante, sin necesidad de escribir complejos ciclos y muestras.


Ejemplo de solicitud simple

Las bases de datos guardan información en forma de recuadros; la obtención/modificación de los mismos, así como la adición de nuevos datos, se realiza con la ayuda de solicitudes en el lenguaje SQL. Vamos a mostrar un ejemplo en el que se crea una bases de datos, de la que luego se obtiene información.

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   string filename="company.sqlite";
//--- Creamos o abrimos la base de datos en la carpeta general del terminal
   int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE |DATABASE_OPEN_COMMON);
   if(db==INVALID_HANDLE)
     {
      Print("DB: ", filename, " open failed with code ", GetLastError());
      return;
     }
... trabajando con la base de datos


//--- cerramos la base de datos
   DatabaseClose(db);
  }

La creación y el cierre de bases de datos es similar al trabajo con archivos: de la misma forma, creamos un puntero a la base de datos, lo comprobamos y cerramos al final.

A continuación, comprobamos la presencia de recuadros; si ya existe un recuadro, al intentar insertar en él los mismos datos que en el ejemplo, obtendremos error.

//--- si el recuadro COMPANY existe, lo eliminamos
   if(DatabaseTableExists(db, "COMPANY"))
     {
      //--- eliminamos el recuadro
      if(!DatabaseExecute(db, "DROP TABLE COMPANY"))
        {
         Print("Failed to drop table COMPANY with code ", GetLastError());
         DatabaseClose(db);
         return;
        }
     }
//--- eliminamos el recuadro 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 ", GetLastError());
      DatabaseClose(db);
      return;
     }

La eliminación y creación del recuadro se realizará con la ayuda de solicitudes: siempre deberemos comprobar el resultado de la ejecución. En el recuadro COMPANY, tenemos un total de 5 campos: la ID de la entrada, el nombre, la edad, la dirección y el sueldo. Además, el campo de la ID es la clave, es decir, un índice único. Los índices permiten determinar de forma unívoca cada entrada, y pueden usarse en diferentes recuadros para conectarlos entre sí. Es similar a la manera en que la ID de una posición conecta entre sí todas las transacciones y órdenes relacionadas con una posición concreta.

Ahora, debemos rellenar el recuadro con los datos, y esto se hace con la ayuda de la solicitud "INSERT":

//--- insertamos los datos en el recuadro 
   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 ", GetLastError());
      DatabaseClose(db);
      return;
     }

Vemos que al recuadro COMPANY se añaden 4 entradas; para cada una de ellas se indica el orden de los campos y los valores que se anotarán en dichos campos. Cada entrada se inserta con una solicitud "INSERT...." aparte, unida a otras en una sola solicitud. Es decir, podríamos insertar cada entrada en el recuadro con una llamada DatabaseExecute() aparte.

Dado que al finalizar el funcionamiento del script, la base se guardará en el archivo company.sqlite, en su siguiente inicio, nosotros trataríamos de anotar los mismos datos en el recuadro COMPANY con la misma ID. Esto provocaría un error, precisamente por ello, hemos eliminado el recuadro primero, para comenzar el trabajo desde cero con cada inicio del script.

Ahora, obtendremos todas las entradas desde el recuadro COMPANY, donde el campo SALARY>15000. Esto se hace con la ayuda de la funciónDatabasePrepare(), que compila el texto de la solicitud y retorna el manejador a ella para su posterior uso en DatabaseRead() o DatabaseReadBind().

//--- creamos una solicitud y obtenemos el manejador a ella
   int request=DatabasePrepare(db, "SELECT * FROM COMPANY WHERE SALARY>15000");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }

Después de que la solicitud se haya creado con éxito, deberemos obtener los resultados de su ejecución. Lo haremos con la ayuda de DatabaseRead(), que, al darse la primera llamada, ejecutará la solicitud y pasará a la primera entrada en los resultados. Con cada llamada siguiente, simplemente leerá la próxima entrada hasta que llegue al final. En este caso, retornará false, lo cual significa "no hay más entradas".

//--- imprimimos todas las entradas con un sueldo superior a 15000
   int    id, age;
   string name, address;
   double salary;
   Print("Persons with salary > 15000:");
   for(int i=0; DatabaseRead(request); i++)
     {
      //--- leemos los valores de cada campo de la entrada recibida
      if(DatabaseColumnInteger(request, 0, id) && DatabaseColumnText(request, 1, name) &&
         DatabaseColumnInteger(request, 2, age) && DatabaseColumnText(request, 3, address) && DatabaseColumnDouble(request, 4, salary))
         Print(i, ":  ", id, " ", name, " ", age, " ", address, " ", salary);
      else
        {
         Print(i, ": DatabaseRead() failed with code ", GetLastError());
         DatabaseFinalize(request);
         DatabaseClose(db);
         return;
        }
     }
//--- eliminamos la solicitud después del uso
   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

El código completo del ejemplo se encuentra en el archivo DatabaseRead.mq5.

Depuración de solicitudes SQL en el MetaEditor

Todas las funciones para trabajar con la base de datos retornan el código de error si la llamada no tiene éxito. El trabajo con ellas no da problemas si respetamos 4 sencillas normas:

  1. todos los manejadores de las solicitudes deberán ser eliminados después de utilizarse con la ayuda de DatabaseFinalize();
  2. antes de finalizar su funcionamiento, la base de datos deberá cerrarse con la ayuda de DatabaseClose();
  3. los resultados de la ejecución de las solicitudes deberán comprobarse;
  4. en caso de error, primero deberemos eliminar la solicitud, y después cerrar la base de datos.

En este caso, lo más complicado será comprender en qué consiste el error, si la solicitud no se ha creado. El MetaEditor permite abrir archivos *.sqlite y trabajar con ellos con la ayuda de solicitudes SQL. Vamos a mostrar cómo se hace, usando de ejemplo el archivo company.sqlite que hemos creado:

1. Abrimos en el acrpeta general de los terminales el archivo company.sqlite.

2. Después de abrir la base de datos, veremos en el Navegador el recuadro COMPANY, sobre el que clicaremos dos veces.

3. En la barra de estado, se creará automáticamente la solicitud "SELECT * FROM COMPANY".

4. La solicitud se ejecuta automáticamente; asimismo, es posible ejecutarla con la tecla F9 o pulsando el botón Execute.

5. Veamos el resultado de la ejecución de la solicitud.

6. Si algo ha salido mal, los errores se mostrarán en el Diario del editor.


Con la ayuda de las solicitudes SQL, podemos obtener las estadísticas sobre los campos del recuadro, por ejemplo, la suma y la media. Creamos las solicitudes y comprobamos que funcionen.

Trabajando con solicitudes en el MetaEditor

Ahora, podemos trasladar estas solicitudes al código MQL5:

   Print("Some statistics:");
//--- preparando una nueva solicitud sobre la suma de los sueldos
   request=DatabasePrepare(db, "SELECT SUM(SALARY) FROM COMPANY");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }
   while(DatabaseRead(request))
     {
      double total_salary;
      DatabaseColumnDouble(request, 0, total_salary);
      Print("Total salary=", total_salary);
     }
//--- eliminamos la solicitud después del uso
   DatabaseFinalize(request);
 
//--- preparando una nueva solicitud sobre el sueldo medio
   request=DatabasePrepare(db, "SELECT AVG(SALARY) FROM COMPANY");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      ResetLastError();
      DatabaseClose(db);
      return;
     }
   while(DatabaseRead(request))
     {
      double aver_salary;
      DatabaseColumnDouble(request, 0, aver_salary);
      Print("Average salary=", aver_salary);
     }
//--- eliminamos la solicitud después del uso
   DatabaseFinalize(request);

Y comparamos los resultados de la ejecución:

Some statistics:
Total salary=125000.0
Average salary=31250.0


Lectura automática de los resultados de las solicitudes en la estructura con la ayuda de DatabaseReadBind()

La función DatabaseRead() permite iterar por todas las entradas del resultado de la solicitud y obtener a continuación información completa sobre cada columna en el recuadro obtenido:

Estas funciones permiten trabajar de forma universal con los resultados de cualquier solicitud, pero el precio a pagar por ello será un código voluminoso. Si la estructura de los resultados de la solicitud se conoce de antemano, los mejor será utilizar la función DatabaseReadBind(), que permite leer de golpe toda la entrada en la estructura. Podemos rehacer el ejemplo anterior de la forma siguiente: primero, delclaramos la estructura Person:

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

A continuación, procedemos a la lectura de cada entrada de los resultados de la solicitud con la ayuda de DatabaseReadBind(request, person):

//--- mostramos los resultados obtenidos para la solicitud
   Person person;
   Print("Persons with salary > 15000:");
   for(int i=0; DatabaseReadBind(request, person); i++)
      Print(i, ":  ", person.id, " ", person.name, " ", person.age, " ", person.address, " ", person.salary);
//--- eliminamos la solicitud después del uso
   DatabaseFinalize(request);

De esta forma, obtenemos directamente de la entrada actual los valores de todos los campos, no teniendo que leerlos por separado.


Acelerando las transacciones con el envoltorio en DatabaseTransactionBegin()/DatabaseTransactionCommit() 

Si al trabajar con un recuadro necesitamos ejecutar los comandos masivos INSERT, UPDATE o DELETE, será mejor hacerlo con ayuda de las transacciones. Al ejecutar una transacción, la base de datos primero se bloquea(DatabaseTransactionBegin); a continuación, se ejecutan los comando masivos de modificación, y después se guardan (DatabaseTransactionCommit) o bien se cancelan en caso de error (DatabaseTransactionRollback).

En la descripción de la función DatabasePrepare, se muestra un ejemplo de uso de las transacciones:

//--- variables auxiliares
   ulong    deal_ticket;         // ticket de la transacción
   long     order_ticket;        // ticket de la orden según la que se ha realizado la transacción
   long     position_ticket;     // ID de la posición a la que pertenece la transacción
   datetime time;                // hora de realización de la transacción
   long     type ;               // tipo de transacción
   long     entry ;              // dirección de la transacción
   string   symbol;              // de qué símbolo se ha realizado la transacción
   double   volume;              // volumen de la operación
   double   price;               // precio
   double   profit;              // resultado financiero
   double   swap;                // swap
   double   commission;          // comisión
   long     magic;               // Magic number (ID del asesor)
   long     reason;              // motivo o fuente de la ejecución de la transacción
//--- iteramos por todas las transacciones y las introducimos en la base de datos
   bool failed=false;
   int deals=HistoryDealsTotal();
//--- bloqueamos la base de datos antes de ejecutar la transacción
   DatabaseTransactionBegin(database);
   for(int i=0; i<deals; i++)
     {
      deal_ticket=    HistoryDealGetTicket(i);
      order_ticket=   HistoryDealGetInteger(deal_ticket, DEAL_ORDER);
      position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID);
      time= (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);
      type=           HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
      entry=          HistoryDealGetInteger(deal_ticket, DEAL_ENTRY);
      symbol=         HistoryDealGetString(deal_ticket, DEAL_SYMBOL);
      volume=         HistoryDealGetDouble(deal_ticket, DEAL_VOLUME);
      price=          HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
      profit=         HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
      swap=           HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
      commission=     HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);
      magic=          HistoryDealGetInteger(deal_ticket, DEAL_MAGIC);
      reason=         HistoryDealGetInteger(deal_ticket, DEAL_REASON);
      //--- introducimos en el recuadro cada transacción a través de una solicitud
      string request_text=StringFormat("INSERT INTO DEALS (ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)"
                                       "VALUES (%d, %d, %d, %d, %d, %d, '%s', %G, %G, %G, %G, %G, %d, %d)",
                                       deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason);
      if(!DatabaseExecute(database, request_text))
        {
         PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError());
         PrintFormat("i=%d: deal #%d  %s", i, deal_ticket, symbol);
         failed=true;
         break;
        }
     }
//--- comprobamos si se producen errores al ejecutar las transacciones
   if(failed)
     {
      //--- deshacemos todas las transacciones y desbloqueamos la base de datos
      DatabaseTransactionRollback(database);
      PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError());
      return(false);
     }
//--- todas las transacciones se han realizado con éxito:registramos los cambios y desbloqueamos la base de datos
   DatabaseTransactionCommit(database);

Gracias al uso de las transacciones, usted podrá realizar las operaciones masivas de modificación de recuadros cientos de veces más rápido, como se muestra en el ejemplo DatabaseTransactionBegin:

Resultado:
   La historia comercial ha calculado el número de transacciones: 2737 
   Transations WITH    DatabaseTransactionBegin/DatabaseTransactionCommit: time=48.5 milliseconds
   Transations WITHOUT DatabaseTransactionBegin/DatabaseTransactionCommit: time=25818.9 milliseconds
   El uso de DatabaseTransactionBegin/DatabaseTransactionCommit nos ha dado una velocidad 532.8 veces mayor


Trabajando con las transacciones de la historia comercial

La fuerza de las solicitudes SQL reside en que usted puede clasificar, seleccionar y modificar los datos fuente sin necesidad de escribir código. Vamos a continuar analizando el ejemplo de la descripción de la función DatabasePrepare: ahí se muestra cómo obtener trades a partir de las transacciones utilizando una solicitud. Los trades contiene información sobre las fechas de entrada y salida de la posición, los precios de entrada y salida, el símbolo, la dirección y el volumen. Si nos fijamos en la estructura de la transacción, podremos ver que las transacciones de entrada y salida están vinculadas por un identificador de posición común. De esta forma, si tenemos un sistema comercial sencillo en nuestra cuenta con registro de posiciones de cobertura, podremos vincular fácilmente dos posiciones en un solo trade. Esto se consigue con la ayuda de esta solicitud:

//--- rellenamos a través de una solicitud SQL el recuadro TRADES basado en los datos de DEALS
   ulong start=GetMicrosecondCount();
   if(DatabaseTableExists(db, "DEALS"))
     {
      //--- rellenamos el recuadro TRADES
      if(!DatabaseExecute(db, "INSERT INTO TRADES(TIME_IN,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) "
                          "SELECT "
                          "   d1.time as time_in,"
                          "   d1.position_id as ticket,"
                          "   d1.type as type,"
                          "   d1.volume as volume,"
                          "   d1.symbol as symbol,"
                          "   d1.price as price_in,"
                          "   d2.time as time_out,"
                          "   d2.price as price_out,"
                          "   d1.commission+d2.commission as commission,"
                          "   d2.swap as swap,"
                          "   d2.profit as profit "
                          "FROM DEALS d1 "
                          "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id "
                          "WHERE d1.entry=0 AND d2.entry=1"))
        {
         Print("DB: fillng the TRADES table failed with code ", GetLastError());
         return;
        }
     }
   ulong transaction_time=GetMicrosecondCount()-start;

Aquí se toma el recuadro DEALS existente y, usando una combinación interna a través de INNER JOIN, se crean las entradas de las transacciones con idéntico DEAL_POSITION_ID. Resultado del funcionamiento del ejemplo de DatabasePrepare en la cuenta comercial:

Resultado:
   La historia comercial ha calculado el número de transacciones: 2741 
   Las primeras 10 transacciones:
       [ticket] [order_ticket] [position_ticket]              [time] [type] [entry] [symbol] [volume]   [price]   [profit] [swap] [commission] [magic] [reason]
   [0] 34429573              0                 0 2019.09.05 22:39:59      2       0 ""        0.00000   0.00000 2000.00000 0.0000      0.00000       0        0
   [1] 34432127       51447238          51447238 2019.09.06 06:00:03      0       0 "USDCAD"  0.10000   1.32320    0.00000 0.0000     -0.16000     500        3
   [2] 34432128       51447239          51447239 2019.09.06 06:00:03      1       0 "USDCHF"  0.10000   0.98697    0.00000 0.0000     -0.16000     500        3
   [3] 34432450       51447565          51447565 2019.09.06 07:00:00      0       0 "EURUSD"  0.10000   1.10348    0.00000 0.0000     -0.18000     400        3
   [4] 34432456       51447571          51447571 2019.09.06 07:00:00      1       0 "AUDUSD"  0.10000   0.68203    0.00000 0.0000     -0.11000     400        3
   [5] 34432879       51448053          51448053 2019.09.06 08:00:00      1       0 "USDCHF"  0.10000   0.98701    0.00000 0.0000     -0.16000     600        3
   [6] 34432888       51448064          51448064 2019.09.06 08:00:00      0       0 "USDJPY"  0.10000 106.96200    0.00000 0.0000     -0.16000     600        3
   [7] 34435147       51450470          51450470 2019.09.06 10:30:00      1       0 "EURUSD"  0.10000   1.10399    0.00000 0.0000     -0.18000     100        3
   [8] 34435152       51450476          51450476 2019.09.06 10:30:00      0       0 "GBPUSD"  0.10000   1.23038    0.00000 0.0000     -0.20000     100        3
   [9] 34435154       51450479          51450479 2019.09.06 10:30:00      1       0 "EURJPY"  0.10000 118.12000    0.00000 0.0000     -0.18000     200        3
 
   Los primeros 10 trades:
                 [time_in] [ticket] [type] [volume] [symbol] [price_in]          [time_out] [price_out] [commission]   [swap]  [profit]
   [0] 2019.09.06 06:00:03 51447238      0  0.10000 "USDCAD"    1.32320 2019.09.06 18:00:00     1.31761     -0.32000  0.00000 -42.43000
   [1] 2019.09.06 06:00:03 51447239      1  0.10000 "USDCHF"    0.98697 2019.09.06 18:00:00     0.98641     -0.32000  0.00000   5.68000
   [2] 2019.09.06 07:00:00 51447565      0  0.10000 "EURUSD"    1.10348 2019.09.09 03:30:00     1.10217     -0.36000 -1.31000 -13.10000
   [3] 2019.09.06 07:00:00 51447571      1  0.10000 "AUDUSD"    0.68203 2019.09.09 03:30:00     0.68419     -0.22000  0.03000 -21.60000
   [4] 2019.09.06 08:00:00 51448053      1  0.10000 "USDCHF"    0.98701 2019.09.06 18:00:01     0.98640     -0.32000  0.00000   6.18000
   [5] 2019.09.06 08:00:00 51448064      0  0.10000 "USDJPY"  106.96200 2019.09.06 18:00:01   106.77000     -0.32000  0.00000 -17.98000
   [6] 2019.09.06 10:30:00 51450470      1  0.10000 "EURUSD"    1.10399 2019.09.06 14:30:00     1.10242     -0.36000  0.00000  15.70000
   [7] 2019.09.06 10:30:00 51450476      0  0.10000 "GBPUSD"    1.23038 2019.09.06 14:30:00     1.23040     -0.40000  0.00000   0.20000
   [8] 2019.09.06 10:30:00 51450479      1  0.10000 "EURJPY"  118.12000 2019.09.06 14:30:00   117.94100     -0.36000  0.00000  16.73000
   [9] 2019.09.06 10:30:00 51450480      0  0.10000 "GBPJPY"  131.65300 2019.09.06 14:30:01   131.62500     -0.40000  0.00000  -2.62000
   Se han invertido 12.51 milisegundos en rellenar el recuadro TRADES

Inicie este script en su cuenta comercial con registro de cobertura y compare los resultados con las posiciones en la historia. Si antes usted quizá no tenía conocimientos o tiempo suficientes para codificar los ciclos y obtener este resultado, ahora, podrá hacerlo con una sola solicitud SQL. Podrá ver el resultado del funcionamiento del script en el MetaEditor: abra el archivo adjunto trades.sqlite.


Análisis del portafolio desde el punto de vista de la estrategia

En los resultados del funcionamiento del script de DatabasePrepare mostrados más arriba, podemos ver que el comercio se realiza con varias parejas de divisas. Pero, además, podemos ver en la columna [magic] valores de 100 a 600. Esto significa que en la cuenta comercial están comerciando varias estrategias, cada una de las cuales tiene su propio Magic Number para identificar nuevas transacciones.

Con la ayuda de la solicitud SQL, podemos analizar el comercio desde el punto de vista de los valores magic:

//--- obtenemos la estadística comercial desde el punto de vista de los asesores según el Magic Number
   request=DatabasePrepare(db, "SELECT r.*,"
                           "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                           "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                           "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                           "   r.gross_profit/r.win_trades as average_profit,"
                           "   r.gross_loss/r.loss_trades as average_loss,"
                           "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                           "FROM "
                           "   ("
                           "   SELECT MAGIC,"
                           "   sum(case when entry =1 then 1 else 0 end) as trades,"
                           "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                           "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                           "   sum(swap) as total_swap,"
                           "   sum(commission) as total_commission,"
                           "   sum(profit) as total_profit,"
                           "   sum(profit+swap+commission) as net_profit,"
                           "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                           "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                           "   FROM DEALS "
                           "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                           "   GROUP BY MAGIC"
                           "   ) as r");

Resultado:

Trade statistics by Magic Number
    [magic] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[0]     100      242     2584.80000  -2110.00000          -33.36000    -93.53000      474.80000    347.91000          143            99           1.96198      59.09091       40.90909         18.07552      -21.31313         1.22502
[1]     200      254     3021.92000  -2834.50000          -29.45000    -98.22000      187.42000     59.75000          140           114           0.73787      55.11811       44.88189         21.58514      -24.86404         1.06612
[2]     300      250     2489.08000  -2381.57000          -34.37000    -96.58000      107.51000    -23.44000          134           116           0.43004      53.60000       46.40000         18.57522      -20.53078         1.04514
[3]     400      224     1272.50000  -1283.00000          -24.43000    -64.80000      -10.50000    -99.73000          131            93          -0.04687      58.48214       41.51786          9.71374      -13.79570         0.99182
[4]     500      198     1141.23000  -1051.91000          -27.66000    -63.36000       89.32000     -1.70000          116            82           0.45111      58.58586       41.41414          9.83819      -12.82817         1.08491
[5]     600      214     1317.10000  -1396.03000          -34.12000    -68.48000      -78.93000   -181.53000          116            98          -0.36883      54.20561       45.79439         11.35431      -14.24520         0.94346

Podemos ver que 4 de las 6 estrategias han dado beneficios. Y para cada una de las estrategias, hemos obtenido los siguientes índices estadísticos:

  • trades — número de trades por estrategia,
  • gross_profit — beneficio total por estrategia (suma de todos los valores positivos de profit),
  • gross_loss — pérdidas totales por estrategia (suma de todos los valores negativos de profit),
  • total_commission — suma de todas las comisiones de los trades de la estrategia,
  • total_swap — suma de todos los swaps de los trades de la estrategia,
  • total_profit — suma de gross_profit  y gross_loss,
  • net_profit — suma (gross_profit  + gross_loss + total_commission + total_swap),
  • win_trades — número de trades, donde profit>0,
  • loss_trades— número de trades, donde profit<0,
  • expected_payoff — esperanza matemática del trade sin tener encuenta swaps y comisiones = net_profit/trades,
  • win_percent — porcentaje de trades ganadores,
  • loss_percent — porcentaje de trades perdedores,
  • average_profit — ganancia media = gross_profit/win_trades,
  • average_loss — pérdida media = gross_loss /loss_trades,
  • profit_factor — Factor de beneficio = gross_profit/gross_loss.

Esta estadística para calcular el beneficio y las pérdidas no tiene en cuenta los swaps y comisiones que han sido cargados sobre la posición. Esto permite ver estas retenciones de forma limpia. Puede suceder que la propia estrategia dé un beneficio pequeño, pero, debido a los swaps y comisiones, no resulte rentable.


Análisis de transacciones según el símbolo

Podemos analizar el comercio desde el punto de vista de los símbolos. Para ello, realizamos la siguiente solicitud:

//--- obtenemos la estadística comercial desde el punto de vista de los símbolos
   int request=DatabasePrepare(db, "SELECT r.*,"
                               "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                               "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                               "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                               "   r.gross_profit/r.win_trades as average_profit,"
                               "   r.gross_loss/r.loss_trades as average_loss,"
                               "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                               "FROM "
                               "   ("
                               "   SELECT SYMBOL,"
                               "   sum(case when entry =1 then 1 else 0 end) as trades,"
                               "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                               "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                               "   sum(swap) as total_swap,"
                               "   sum(commission) as total_commission,"
                               "   sum(profit) as total_profit,"
                               "   sum(profit+swap+commission) as net_profit,"
                               "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                               "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                               "   FROM DEALS "
                               "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                               "   GROUP BY SYMBOL"
                               "   ) as r");

Resultado:

Trade statistics by Symbol
      [name] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[0] "AUDUSD"      112      503.20000   -568.00000           -8.83000    -24.64000      -64.80000    -98.27000           70            42          -0.57857      62.50000       37.50000          7.18857      -13.52381         0.88592
[1] "EURCHF"      125      607.71000   -956.85000          -11.77000    -45.02000     -349.14000   -405.93000           54            71          -2.79312      43.20000       56.80000         11.25389      -13.47676         0.63512
[2] "EURJPY"      127     1078.49000  -1057.83000          -10.61000    -45.76000       20.66000    -35.71000           64            63           0.16268      50.39370       49.60630         16.85141      -16.79095         1.01953
[3] "EURUSD"      233     1685.60000  -1386.80000          -41.00000    -83.76000      298.80000    174.04000          127           106           1.28240      54.50644       45.49356         13.27244      -13.08302         1.21546
[4] "GBPCHF"      125     1881.37000  -1424.72000          -22.60000    -51.56000      456.65000    382.49000           80            45           3.65320      64.00000       36.00000         23.51712      -31.66044         1.32052
[5] "GBPJPY"      127     1943.43000  -1776.67000          -18.84000    -52.46000      166.76000     95.46000           76            51           1.31307      59.84252       40.15748         25.57145      -34.83667         1.09386
[6] "GBPUSD"      121     1668.50000  -1438.20000           -7.96000    -49.93000      230.30000    172.41000           77            44           1.90331      63.63636       36.36364         21.66883      -32.68636         1.16013
[7] "USDCAD"       99      405.28000   -475.47000           -8.68000    -31.68000      -70.19000   -110.55000           51            48          -0.70899      51.51515       48.48485          7.94667       -9.90563         0.85238
[8] "USDCHF"      206     1588.32000  -1241.83000          -17.98000    -65.92000      346.49000    262.59000          131            75           1.68199      63.59223       36.40777         12.12458      -16.55773         1.27902
[9] "USDJPY"      107      464.73000   -730.64000          -35.12000    -34.24000     -265.91000   -335.27000           50            57          -2.48514      46.72897       53.27103          9.29460      -12.81825         0.63606

La estadística indica que solo en 5 símbolos de 10 se ha obtenido beneficio neto (net_profit>0), aunque en 6 de 10 el factor de beneficio ha sido positivo (profit_factor>1). Se trata precisamente de uno de esos casos en los que los swaps y comisiones convierten una estrategia en EURJPY en perdedora.


Análisis de transacciones según la hora de entrada

Incluso cuando estamos comerciando con una sola estrategia y en un solo símbolo, puede resultar útil el análisis según la hora de entrada en el mercado. Esto se consigue con la siguiente solicitud SQL:

//--- obtenemos la estadística comercial desde el punto de vista de la hora de entrada en el mercado
   request=DatabasePrepare(db, "SELECT r.*,"
                           "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                           "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                           "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                           "   r.gross_profit/r.win_trades as average_profit,"
                           "   r.gross_loss/r.loss_trades as average_loss,"
                           "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                           "FROM "
                           "   ("
                           "   SELECT HOUR_IN,"
                           "   count() as trades,"
                           "   sum(volume) as volume,"
                           "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                           "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                           "   sum(profit) as net_profit,"
                           "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                           "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                           "   FROM TRADES "
                           "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                           "   GROUP BY HOUR_IN"
                           "   ) as r");

Resultado:

Trade statistics by entry hour
     [hour_in] [trades] [volume] [gross_profit] [gross_loss] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[ 0]         0       50  5.00000      336.51000   -747.47000   -410.96000           21            29          -8.21920      42.00000       58.00000         16.02429      -25.77483         0.45020
[ 1]         1       20  2.00000      102.56000    -57.20000     45.36000           12             8           2.26800      60.00000       40.00000          8.54667       -7.15000         1.79301
[ 2]         2        6  0.60000       38.55000    -14.60000     23.95000            5             1           3.99167      83.33333       16.66667          7.71000      -14.60000         2.64041
[ 3]         3       38  3.80000      173.84000   -200.15000    -26.31000           22            16          -0.69237      57.89474       42.10526          7.90182      -12.50938         0.86855
[ 4]         4       60  6.00000      361.44000   -389.40000    -27.96000           27            33          -0.46600      45.00000       55.00000         13.38667      -11.80000         0.92820
[ 5]         5       32  3.20000      157.43000   -179.89000    -22.46000           20            12          -0.70187      62.50000       37.50000          7.87150      -14.99083         0.87515
[ 6]         6       18  1.80000       95.59000   -162.33000    -66.74000           11             7          -3.70778      61.11111       38.88889          8.69000      -23.19000         0.58886
[ 7]         7       14  1.40000       38.48000   -134.30000    -95.82000            9             5          -6.84429      64.28571       35.71429          4.27556      -26.86000         0.28652
[ 8]         8       42  4.20000      368.48000   -322.30000     46.18000           24            18           1.09952      57.14286       42.85714         15.35333      -17.90556         1.14328
[ 9]         9      118 11.80000     1121.62000   -875.21000    246.41000           72            46           2.08822      61.01695       38.98305         15.57806      -19.02630         1.28154
[10]        10      206 20.60000     2280.59000  -2021.80000    258.79000          115            91           1.25626      55.82524       44.17476         19.83122      -22.21758         1.12800
[11]        11      138 13.80000     1377.02000   -994.18000    382.84000           84            54           2.77420      60.86957       39.13043         16.39310      -18.41074         1.38508
[12]        12      152 15.20000     1247.56000  -1463.80000   -216.24000           84            68          -1.42263      55.26316       44.73684         14.85190      -21.52647         0.85227
[13]        13       64  6.40000      778.27000   -516.22000    262.05000           36            28           4.09453      56.25000       43.75000         21.61861      -18.43643         1.50763
[14]        14       62  6.20000      536.93000   -427.47000    109.46000           38            24           1.76548      61.29032       38.70968         14.12974      -17.81125         1.25606
[15]        15       50  5.00000      699.92000   -413.00000    286.92000           28            22           5.73840      56.00000       44.00000         24.99714      -18.77273         1.69472
[16]        16       88  8.80000      778.55000   -514.00000    264.55000           51            37           3.00625      57.95455       42.04545         15.26569      -13.89189         1.51469
[17]        17       76  7.60000      533.92000  -1019.46000   -485.54000           44            32          -6.38868      57.89474       42.10526         12.13455      -31.85813         0.52373
[18]        18       52  5.20000      237.17000   -246.78000     -9.61000           24            28          -0.18481      46.15385       53.84615          9.88208       -8.81357         0.96106
[19]        19       52  5.20000      407.67000   -150.36000    257.31000           30            22           4.94827      57.69231       42.30769         13.58900       -6.83455         2.71129
[20]        20       18  1.80000       65.92000    -89.09000    -23.17000            9             9          -1.28722      50.00000       50.00000          7.32444       -9.89889         0.73993
[21]        21       10  1.00000       41.86000    -32.38000      9.48000            7             3           0.94800      70.00000       30.00000          5.98000      -10.79333         1.29277
[22]        22       14  1.40000       45.55000    -83.72000    -38.17000            6             8          -2.72643      42.85714       57.14286          7.59167      -10.46500         0.54408
[23]        23        2  0.20000        1.20000     -1.90000     -0.70000            1             1          -0.35000      50.00000       50.00000          1.20000       -1.90000         0.63158

Podemos ver claramente que el mayor número de trades se realiza en el intervalo de las 9 a las 16 inclusive. El comercio a otras horas da menos trades, y por lo general no resulta rentable. Podrá encontrar el código fuente completo con los 3 tipos de solicitud en el ejemplo de la función DatabaseExecute().


Muestra cómoda de datos en el diario de asesores en DatabasePrint()

En los ejemplos anteriores, para mostrar los resultados, necesitamos leer cada entrada en la estructura e imprimir la entradas una a una. No siempre resulta cómodo crear una estructura solo para ver los valores del recuadro o el resultado de una solicitud. Por eso, especialemnte con ese cometido, se ha añadido la función DatabasePrint():

long  DatabasePrint(
   int     database,          // manejador de la base de datos obtenido en DatabaseOpen
   string  table_or_sql,      // recuadro o solicitud SQL
   uint    flags              // combinación de banderas
   );

Con su ayuda, podremos imprimir fácilmente no solo un recuadro existente, sino también los resultados de la ejecución de la solicitud, que se pueden representar en forma de recuadro. Por ejemplo, vamos a mostrar los valores del recuadro DEALS con la ayuda de la solicitud:

   DatabasePrint(db,"SELECT * from DEALS",0);

Resultado (se muestran las primeras 10 líneas del recuadro):

  #|       ID ORDER_ID POSITION_ID       TIME TYPE ENTRY SYMBOL VOLUME   PRICE  PROFIT  SWAP COMMISSION MAGIC REASON
---+----------------------------------------------------------------------------------------------------------------
  1| 34429573        0           0 1567723199    2     0           0.0     0.0  2000.0   0.0        0.0     0      0 
  2| 34432127 51447238    51447238 1567749603    0     0 USDCAD    0.1  1.3232     0.0   0.0      -0.16   500      3 
  3| 34432128 51447239    51447239 1567749603    1     0 USDCHF    0.1 0.98697     0.0   0.0      -0.16   500      3 
  4| 34432450 51447565    51447565 1567753200    0     0 EURUSD    0.1 1.10348     0.0   0.0      -0.18   400      3 
  5| 34432456 51447571    51447571 1567753200    1     0 AUDUSD    0.1 0.68203     0.0   0.0      -0.11   400      3 
  6| 34432879 51448053    51448053 1567756800    1     0 USDCHF    0.1 0.98701     0.0   0.0      -0.16   600      3 
  7| 34432888 51448064    51448064 1567756800    0     0 USDJPY    0.1 106.962     0.0   0.0      -0.16   600      3 
  8| 34435147 51450470    51450470 1567765800    1     0 EURUSD    0.1 1.10399     0.0   0.0      -0.18   100      3 
  9| 34435152 51450476    51450476 1567765800    0     0 GBPUSD    0.1 1.23038     0.0   0.0       -0.2   100      3 
 10| 34435154 51450479    51450479 1567765800    1     0 EURJPY    0.1  118.12     0.0   0.0      -0.18   200      3 


Importación y exportación de datos

Asimismo, para facilitar la importación y exportación de datos, se han añadido las funciones DatabaseImport() y DatabaseExport(). Estas funciones permiten trabajar con archivos CSV y los datos guardados en un fichero ZIP.

DatabaseImport() importa los datos al recuadro indicado, si no existe el recuadro con el nombre indicado, este se creará automáticamente. El nombre y el tipo de los campos en el recuadro creado serán reconocidos automáticamente usando como base los datos contenidos en el archivo. 

DatabaseExport() permite guardar en un archivo el recuadro o los resultados de la solicitud. Si se exportan los resultados de una solicitud, la solicitud SQL deberá comenzar con "SELECT" o "select". En otras palabras, una solicitud SQL no puede modificar la base de datos, en caso contrario, DatabaseExport() se finalizará con error.

Podrá ver la descripción completa en la Documentación MQL5.


Guardando los resultados de la optimización en la base de datos

Las funciones para trabajar con las bases de datos también se pueden utilizar para procesar los resultados de la optimización. Usando como ejemplo el asesor "MACD Sample" del paquete estándar, vamos a mostrar cómo obtener los resultados de simulación con la ayuda de frames y después guardar los valores de todos los criterios de optimización en un solo archivo. Para ello, crearemos la clase CDatabaseFrames, en la que definiremos el método OnTester() para enviar la estadística comercial:

//+------------------------------------------------------------------+
//| Tester function - envía en un frame la estadística comercial       |
//+------------------------------------------------------------------+
void               CDatabaseFrames::OnTester(const double OnTesterValue)
  {
//--- matriz stats[] para enviar los datos en un frame
   double stats[16];
//--- para visualizar mejor las estadísticas sobre los trades, las colocamos en variables aparte
   int    trades=(int)TesterStatistics(STAT_TRADES);
   double win_trades_percent=0;
   if(trades>0)
      win_trades_percent=TesterStatistics(STAT_PROFIT_TRADES)*100./trades;
//--- rellenamos la matriz con los resultados de la simulación
   stats[0]=trades;                                       // número de trades
   stats[1]=win_trades_percent;                           // porcentaje de trades rentables
   stats[2]=TesterStatistics(STAT_PROFIT);                // beneficio neto
   stats[3]=TesterStatistics(STAT_GROSS_PROFIT);          // beneficio total
   stats[4]=TesterStatistics(STAT_GROSS_LOSS);            // pérdidas totales
   stats[5]=TesterStatistics(STAT_SHARPE_RATIO);          // coeficiente de Sharpe
   stats[6]=TesterStatistics(STAT_PROFIT_FACTOR);         // factor de beneficio
   stats[7]=TesterStatistics(STAT_RECOVERY_FACTOR);       // factor de recuperación
   stats[8]=TesterStatistics(STAT_EXPECTED_PAYOFF);       // esperanza matemática del trade
   stats[9]=OnTesterValue;                                // criterio de usuario para la optimización
//--- calculamos los criterios estándar de optimización incorporados
   double balance=AccountInfoDouble(ACCOUNT_BALANCE);
   double balance_plus_profitfactor=0;
   if(TesterStatistics(STAT_GROSS_LOSS)!=0)
      balance_plus_profitfactor=balance*TesterStatistics(STAT_PROFIT_FACTOR);
   double balance_plus_expectedpayoff=balance*TesterStatistics(STAT_EXPECTED_PAYOFF);
   double balance_plus_dd=balance/TesterStatistics(STAT_EQUITYDD_PERCENT);
   double balance_plus_recoveryfactor=balance*TesterStatistics(STAT_RECOVERY_FACTOR);
   double balance_plus_sharpe=balance*TesterStatistics(STAT_SHARPE_RATIO);
//--- añadimos los valores de los criterios de optimización incorporados
   stats[10]=balance;                                     // Balance
   stats[11]=balance_plus_profitfactor;                   // Balance+ProfitFactor
   stats[12]=balance_plus_expectedpayoff;                 // Balance+ExpectedPayoff
   stats[13]=balance_plus_dd;                             // Balance+EquityDrawdown
   stats[14]=balance_plus_recoveryfactor;                 // Balance+RecoveryFactor
   stats[15]=balance_plus_sharpe;                         // Balance+Sharpe
//--- creamos un frame con los datos y lo enviamos al terminal
   if(!FrameAdd(MQLInfoString(MQL_PROGRAM_NAME)+"_stats", STATS_FRAME, trades, stats))
      Print("Frame add error: ", GetLastError());
   else
      Print("Frame added, Ok");
  }

El segundo método importante de esta clase es OnTesterDeinit(), encargado de leer todos los frames obtenidos al finalizar la optimización y guardar la estadística en la base de datos:

//+------------------------------------------------------------------+
//| TesterDeinit function - leemos los datos de los frames           |
//+------------------------------------------------------------------+
void               CDatabaseFrames::OnTesterDeinit(void)
  {
//--- tomamos el nombre del asesor y la hora de finalización de la optimización
   string filename=MQLInfoString(MQL_PROGRAM_NAME)+" "+TimeToString(TimeCurrent())+".sqlite";
   StringReplace(filename, ":", "."); // el símbolo ":" está prohibido en los nombres de los archivos
//--- abrimos/creamos la base de datos en la carpeta general de los terminales
   int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON);
   if(db==INVALID_HANDLE)
     {
      Print("DB: ", filename, " open failed with code ", GetLastError());
      return;
     }
   else
      Print("DB: ", filename, " opened successful");
//--- creamos el recuadro PASSES
   if(!DatabaseExecute(db, "CREATE TABLE PASSES("
                       "PASS               INT PRIMARY KEY NOT NULL,"
                       "TRADES             INT,"
                       "WIN_TRADES         INT,"
                       "PROFIT             REAL,"
                       "GROSS_PROFIT       REAL,"
                       "GROSS_LOSS         REAL,"
                       "SHARPE_RATIO       REAL,"
                       "PROFIT_FACTOR      REAL,"
                       "RECOVERY_FACTOR    REAL,"
                       "EXPECTED_PAYOFF    REAL,"
                       "ON_TESTER          REAL,"
                       "BL_BALANCE         REAL,"
                       "BL_PROFITFACTOR    REAL,"
                       "BL_EXPECTEDPAYOFF  REAL,"
                       "BL_DD              REAL,"
                       "BL_RECOVERYFACTOR  REAL,"
                       "BL_SHARPE          REAL );"))
     {
      Print("DB: ", filename, " create table failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }
//--- variables para leer los frames
   string        name;
   ulong         pass;
   long          id;
   double        value;
   double        stats[];
//--- trasladamos el puntero de los frames al inicio
   FrameFirst();
   FrameFilter("", STATS_FRAME); // seleccionamos para el trabajo los frames con la estadística comercial
//--- variables para obtener la estadística del frame
   int trades;
   double win_trades_percent;
   double profit, gross_profit, gross_loss;
   double sharpe_ratio, profit_factor, recovery_factor, expected_payoff;
   double ontester_value;                              // criterios de usuario para la optimización
   double balance;                                     // Balance
   double balance_plus_profitfactor;                   // Balance+ProfitFactor
   double balance_plus_expectedpayoff;                 // Balance+ExpectedPayoff
   double balance_plus_dd;                             // Balance+EquityDrawdown
   double balance_plus_recoveryfactor;                 // Balance+RecoveryFactor
   double balance_plus_sharpe;                         // Balance+Sharpe
//--- bloqueamos la base de datos durante las transacciones masivas
   DatabaseTransactionBegin(db);
//--- iteramos por los frames y leemos los datos de ellos
   bool failed=false;
   while(FrameNext(pass, name, id, value, stats))
     {
      Print("Got pass #", pass);
      trades=(int)stats[0];
      win_trades_percent=stats[1];
      profit=stats[2];
      gross_profit=stats[3];
      gross_loss=stats[4];
      sharpe_ratio=stats[5];
      profit_factor=stats[6];
      recovery_factor=stats[7];
      expected_payoff=stats[8];
      stats[9];
      balance=stats[10];
      balance_plus_profitfactor=stats[11];
      balance_plus_expectedpayoff=stats[12];
      balance_plus_dd=stats[13];
      balance_plus_recoveryfactor=stats[14];
      balance_plus_sharpe=stats[15];
      PrintFormat("VALUES (%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%G,%.2f,%.2f,%2.f,%.2f,%.2f,%.2f,%.2f)",
                  pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio,
                  profit_factor, recovery_factor, expected_payoff, ontester_value, balance,
                  balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor,
                  balance_plus_sharpe);
      //--- registramos los datos en el recuadro
      string request=StringFormat("INSERT INTO PASSES (PASS,TRADES,WIN_TRADES, PROFIT,GROSS_PROFIT,GROSS_LOSS,"
                                  "SHARPE_RATIO,PROFIT_FACTOR,RECOVERY_FACTOR,EXPECTED_PAYOFF,ON_TESTER,"
                                  "BL_BALANCE,BL_PROFITFACTOR,BL_EXPECTEDPAYOFF,BL_DD,BL_RECOVERYFACTOR,BL_SHARPE) "
                                  "VALUES (%d, %d, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %G, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f)",
                                  pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio,
                                  profit_factor, recovery_factor, expected_payoff, ontester_value, balance,
                                  balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor,
                                  balance_plus_sharpe);

      //--- realizamos una solicitud para añadir una pasada al recuadro PASSES
      if(!DatabaseExecute(db, request))
        {
         PrintFormat("Failed to insert pass %d with code %d", pass, GetLastError());
         failed=true;
         break;
        }
     }
//--- si ha sucedido un error al realizar la transacción, lo comunicamos y finalizamos el trabajo
   if(failed)
     {
      Print("Transaction failed, error code=", GetLastError());
      DatabaseTransactionRollback(db);
      DatabaseClose(db);
      return;
     }
   else
     {
      DatabaseTransactionCommit(db);
      Print("Transaction done successful");
     }
//--- cerramos la base de datos
   if(db!=INVALID_HANDLE)
     {
      Print("Close database with handle=", db);
      DatabaseClose(db);
     }

A continuación, incluimos el archivo DatabaseFrames.mqh en el asesor "MACD Sample" y declaramos la variable de clase CDatabaseFrames:

#define MACD_MAGIC 1234502
//---
#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\AccountInfo.mqh>
#include "DatabaseFrames.mqh"
...
CDatabaseFrames DB_Frames;

Después, añadimos al final del asesor tres funciones que se llamarán solo durante la optimización:

//+------------------------------------------------------------------+
//| TesterInit function                                              |
//+------------------------------------------------------------------+
int OnTesterInit()
  {
   return(DB_Frames.OnTesterInit());
  }
//+------------------------------------------------------------------+
//| TesterDeinit function                                            |
//+------------------------------------------------------------------+
void OnTesterDeinit()
  {
   DB_Frames.OnTesterDeinit();
  }
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
  {
   double ret=0;
   //--- creamos el criterio de usuario para la optimización como relación del beneficio neto respecto a la reducción relativa del balance
   if(TesterStatistics(STAT_BALANCE_DDREL_PERCENT)!=0)
      ret=TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_BALANCE_DDREL_PERCENT);
   DB_Frames.OnTester(ret);
   return(ret);
  }
//+------------------------------------------------------------------+

Iniciamos la optimización y obtenemos en la carpeta general de los terminales el archivo de la base de datos con la estadística comercial:

CDatabaseFrames::OnTesterInit: optimización iniciada a las 15:53:27
DB: MACD Sample Database 2020.01.20 15.53.sqlite opened successful
Transaction done successful
Close database with handle=65537
Database stored in file 'MACD Sample Database 2020.01.20 15.53.sqlite'

El archivo creado que contiene la base de datos se puede abrir en el MetaEditor o usarse en otro programa MQL5 para el trabajo posterior.

Trabajando con la base de datos en el MetaEditor

De esta forma, usted podrá darle a cualquier dato la forma necesaria para analizarlo posteriormente o intercambiarlo con otros tráders. Podrá en contrar el código fuente, el archivo ini con los parámetros de optimización y los resultados de la ejecución en el fichero ZIP adjunto MACD.zip.


Optimizando la ejecución de solicitudes con la ayuda de índices

La belleza de utilizar SQL (en todas sus implementaciones, no solo en SQLite) reside en que es un lenguaje declarativo, no un lenguaje procesal. Al programar en SQL, usted le dice al sistema ejecutor LO QUE desea calcular, no CÓMO calcular esto. La tarea "cómo hacer esto" se delega en el subsistema del planeador de solicitudes en el núcleo de la base de datos SQL.

Para ejecutar una solicitud SQL concreta, se pueden utilizar centenares e incluso miles de algoritmos de operación distintos. Todos estos algoritmos dan como conclusión una respuesta correcta, aunque algunos pueden funcionar más rápido que otros. El planeador de solicitudes en SQLite intenta seleccionar el algoritmo más rápido y efectivo para cada instrucción SQL.

Normalmente, el planeador de solicitudes en SQLite realiza un buen trabajo en cuanto a la elección del algoritmo correcto. Sin embargo, para trabajar mejor con el planeador de solicitudes, se necesitan índices. Y estos índices deben ser normalmente añadidos por programadores. No obstante, en ocasiones, el planeador de solicitudes puede seleccionar un algoritmo que no resulta óptimo. En estos casos, el programador podría ofrecer sugerencias adicionales, para así ayudar al planeador de solicitudes a ejecutar mejor su trabajo.

Búsqueda sin índices

Supongamos que tenemos un recuadro DEALS con transacciones que contengan los 14 campos indicados. Aquí tenemos las 10 primeras entradas de dicho recuadro.

rowid
ID ORDER_ID POSITION_ID TIME TYPE ENTRY SYMBOL VOLUME PRICE PROFIT SWAP COMMISSION MAGIC REASON
1 34429573 0 0 1567723199 2 0 0 0 2000 0 0 0 0
2 34432127 51447238 51447238 1567749603 0 0 USDCAD 0.1 1.3232 0 0 -0.16 500 3
3 34432128 51447239 51447239 1567749603 1 0 USDCHF 0.1 0.98697 0 0 -0.16 500 3
4 34432450 51447565 51447565 1567753200 0 0 EURUSD 0.1 1.10348 0 0 -0.18 400 3
5 34432456 51447571 51447571 1567753200 1 0 AUDUSD 0.1 0.68203 0 0 -0.11 400 3
6 34432879 51448053 51448053 1567756800 1 0 USDCHF 0.1 0.98701 0 0 -0.16 600 3
7 34432888 51448064 51448064 1567756800 0 0 USDJPY 0.1 106.962 0 0 -0.16 600 3
8 34435147 51450470 51450470 1567765800 1 0 EURUSD 0.1 1.10399 0 0 -0.18 100 3
9 34435152 51450476 51450476 1567765800 0 0 GBPUSD 0.1 1.23038 0 0 -0.2 100 3
10 34435154 51450479 51450479 1567765800 1 0 EURJPY 0.1 118.12 0 0 -0.18 200 3

En ella se muestra la información del apartado Propiedades de las transacciones (excepto DEAL_TIME_MSC, DEAL_COMMENT y DEAL_EXTERNAL_ID), imprescindible para analizar la historia comercial. Cada recuadro, aparte de la propia información, siempre tiene una clave entera rowid, seguida por los campos de la entrada. Los valores de la clave rowid se crean automáticamente, son únicos dentro del recuadro y aumentan al añadir nuevas entradas. Al eliminar las entradas, pueden surgir ciertas lagunas en la numeración, pero las líneas del recuadro siempre se guardarán en el orden ascendente rowid .

Si necesitamos encontrar transacciones relacionadas con alguna posición concreta, por ejemplo, ID=51447571, escribiremos una solicitud con el siguiente aspecto:

SELECT * FROM deals WHERE position_id=51447571

En este caso, se realizará un escaneo completo del recuadro: se revisarán todas las líneas, y en cada línea se comprobará el campo POSITION_ID para ver si es igual a 51447571. Las líneas que cumplan con esta condición, se emitirán en los resultados de la ejecución de la solicitud. Si el recuadro contiene millones o decenas de millones de entradas, la búsqueda podría llevar un tiempo considerable. Si usted realizase la búsqueda, no según la condición position_id=51447571, sino según la condición rowid=5, el tiempo de búsqueda se reduciría miles o millones de veces (depende del tamaño del recuadro).

SELECT * FROM deals WHERE rowid=5

El resultado de la ejecución sería el mismo, puesto que la línea con rowid=5 guarda position_id=51447571. La reducción del tiempo de espera se logra gracias a que los valores de rowid se clasifican por orden ascendente, y para obtener los resultados, se usa la búsqueda binaria. Pero, por desgracia, la búsqueda según el valor rowid no nos resulta conveniente, dado que a nosotros nos interesan las entradas con el valor position_id necesario.

Búsqueda según el índice

Para que la solicitud se ejecute con la misma efectividad en lo que respecta al tiempo, deberemos añadir el índice en el campo POSITION_ID con la ayuda de la consulta:

 CREATE INDEX Idx1 ON deals(position_id)

En este caso, se creará un recuadro aparte con dos columnas: la primera de ellas incluirá los valores POSITION_ID clasificados por orden ascendente, mientras que la segunda estará compuesta por los valores rowid.

POSITION_ID rowid
0 1
51447238 2
51447239 3
51447565 4
51447571 5
51448053 6
51448064 7
51450470 8
51450476 9
51450479 10

En este caso, además, el orden de secuencia de rowid ya podría haberse visto comprometido, aunque en nuestro ejemplo se ha mantenido. Por eso, al abrir una posición según el tiempo, POSITION_ID también aumentará.

Ahora que tenemos el índice del campo POSITION_ID, nuestra solicitud

SELECT * FROM deals WHERE position_id=51447571

se realizará de otra forma. Primero se realizará la búsqueda binaria de transacciones en el índice Idx1 por la columna POSITION_ID y se encontrarán todos los rowid que cumplan con la condición. Y después, con una segunda búsqueda binaria en el recuadro DEALS original se realizará la selección de todas las entradas según los valores rowid conocidos. De esta forma, ahora, en lugar de un escaneo completo del recuadro grande, tienen lugar dos búsquedas consecutivas: primero según el índice, y después según los números de las líneas del recuadro. Esto permite reducir miles de veces el tiempo de ejecución de estas solicitudes cuando existe un gran número de líneas en el recuadro.

Regla general: si algunos campos en el recuadro se usan para la búsqueda/comparación/clasificación, recomendamos crear índices según estos campos.

En este recuadro DEALS, existen también los campos SYMBOL, MAGIC (identificador del asesor) y ENTRY (dirección de entrada). Si usted necesita realizar una selección según estos campos, le recomendamos que cree los índices correspondientes a los mismos. Por ejmplo:

CREATE INDEX Idx2 ON deals(symbol)
CREATE INDEX Idx3 ON deals(magic)
CREATE INDEX Idx4 ON deals(entry)

En este caso, además, es necesario tener en cuenta que la creación de los índices requiere memoria adicional, y que con cada adición/eliminación de entradas, tendrá lugar una reindexación. Podemos crear estos índices múltiples usando como base varios campos. Por ejemplo, si tenemos que seleccionar todas las transacciones realizadas por el asesor MAGIC= 500 con el símbolo USDCAD, podemos crear esta solicitud:

SELECT * FROM deals WHERE magic=500 AND symbol='USDCAD'

En esta situación, podemos crear un índice múltiple según los campos MAGIC y SYMBOL

CREATE INDEX Idx5 ON deals(magic, symbol)

y entonces se creará un recuadro del índice con el aspecto siguiente (mostramos de forma esquemática las primeras 10 líneas)

MAGIC SYMBOL rowid
100 EURUSD 4
100 EURUSD 10
100 EURUSD 20
100 GBPUSD 5
100 GBPUSD 11
200 EURJPY 6
200 EURJPY 12
200 EURJPY 22
200 GBPJPY 7
200 GBPJPY 13

En el índice múltiple creado, las entradas primero se clasifican en bloques según MAGIC, y después, según el campo SYMBOL. Por eso, al realizar solicitudes AND, la búsqueda se dará primero en la primera columna MAGIC, y ya después se comprobará el valor de la segunda columna SYMBOL. Si ambas condiciones se cumplen, rowid se añadirá a la selección resultante, según la cual se realizará ya la búsqueda en el recuadro original. Hablando en general, este índice múltiple ya no resulta convienente para las solicitudes donde se comprueba en primer lugar SYMBOL, y después MAGIC

SELECT * FROM deals WHERE  symbol='USDCAD' AND magic=500 

Aunque el planeador de solicitudes comprende en tales casos cómo actuar correctamente y ejecuta la búsqueda en el orden necesario. Pero, aun así, debemos recordar esto, dado que no nos conviene confiar en que el planeador resuelva nuestros errores de diseño de los recuadros y solicitudes.

Solicitudes OR

Los índices múltiples solo son adecuados para las solicitudes AND. Por ejemplo, queremos encontrar todas las transacciones realizadas por el asesor con MAGIC=100 o según el símbolo EURUSD:

SELECT * FROM deals WHERE magic=100 OR symbol='EURUSD'

En este caso, se realizarán dos búsquedas por separado, y después, los rowid encontrados se combinarán en una selección común para efectuar una búsqueda definitiva según los números de las líneas en el recuadro original.

SELECT * FROM deals WHERE magic=100 
SELECT * FROM deals WHERE symbol='EURUSD'

Pero, incluso en este caso, será necesario que ambos campos de la solicitud OR tengan un índice, dado que, en caso contrario, la búsqueda provocará el escaneo completo del recuadro.

Clasificación

Para acelerar la clasificación, también recomendamos tener un índice para aquellos campos según los cuales se ordenan los resultados de la solicitud. Por ejemplo, tenemos que seleccionar todas las transacciones de EURUSD clasificadas según la hora de realización de la transacción:

SELECT * FROM deals symbol='EURUSD' ORDER BY time

En este caso, merece la pena plantearse la creación de un índice según el campo TIME. La necesidad del uso de los índices depende del tamaño del recuadro. Si el número de entradas en el recuadro es pequeño, la indexación no nos ofrecerá ningún ahorro de tiempo.

Aquí solo hemos analizado las bases esenciales de la optimización de solicitudes, para una mejor comprensión del tema, le recomendamos comenzar a estudiar el apartado Query Planning en el sitio web de los desarrolladores de SQLite.


Integración del trabajo con las bases de datos en MetaEditor

El desarrollo de la plataforma MetaTrader 5 no se detiene. Hemos añadido al lenguaje MQL5 el soporte nativo de solicitudes SQL, incorporando además al MetaEditor una nueva funcionalidad para el trabajo con bases de datos: podrá crear bases de datos, incorporar y eliminar datos, y realizar transacciones masivas. La creación de bases de datos se efectúa por defecto con la ayuda de MQL5 Wizard. Solo tendrá que especificar el nombre del archivo, el nombre del recuadro y añadir los campos necesarios, indicando además su tipo.

Creación de bases de datos en MQL Wizard

A continuación, podemos rellenar el recuadro con datos, realizar una búsqueda o selección, introducir solicitudes SQL, etcétera. De esta forma, trabajar con las bases de datos será posible no solo desde programas MQL5, sino también manualmente: para ello, no necesitaremos recurrir navegadores de terceros.

La implementación de SQLite en MetaTrader descubre para los tráders nuevas posibilidades en cuanto al procesamiento de grandes matrices de datos, ya sea de forma programática o manualmente. Además, hemos intentado que el uso de estas funciones sea lo más cómodo posible, logrando además que su velocidad no se quede atrás con respecto a otras soluciones. Estudie SQL y utilice las solicitudes de este lenguaje en su trabajo.

Traducción del ruso hecha por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/ru/articles/7463

Archivos adjuntos |
SqLiteTest.zip (2708.45 KB)
trades.sqlite (340 KB)
MACD.zip (8.27 KB)
DatabaseRead.mq5 (10.11 KB)
DatabasePrepare.mq5 (35.02 KB)
DatabaseExecute.mq5 (64.83 KB)
Monitoreo multidivisas de las señales comerciales (Parte 2): Implementando la parte visual de la aplicación Monitoreo multidivisas de las señales comerciales (Parte 2): Implementando la parte visual de la aplicación

En el artículo anterior, creamos la plantilla de la aplicación en la que se basará todo nuestro trabajo siguiente. Ahora, pasaremos paso a paso a su desarrollo, es decir, diseñaremos la parte visual de la aplicación y configuraremos las interacciones básicas entre los controles de la interfaz.

Implementando OLAP en la negociación (Parte 3): analizando las cotizaciones con el fin de desarrollar las estrategias comerciales Implementando OLAP en la negociación (Parte 3): analizando las cotizaciones con el fin de desarrollar las estrategias comerciales

En este artículo, continuaremos analizando la tecnología OLAP en aplicación al trading, ampliando la funcionalidad representada en dos artículos anteriores. Esta vez, al análisis operativo se le someterán las cotizaciones. Mostraremos cómo se hacen y se comprueban las hipótesis sobre las estrategias comerciales a base de los indicadores agregados del historial. Además, presentaremos los Asesores Expertos para analizar las regularidades barra por barra y el trading adaptativo.

Optimización móvil continua (Parte 2): Mecanismo de creación de informes de optimización para cualquier robot Optimización móvil continua (Parte 2): Mecanismo de creación de informes de optimización para cualquier robot

Si el primer artículo de la serie estaba dedicado a la creación de la biblioteca DLL que utilizaremos en nuestro optimizador automático y en el robot, este estará completamente dedicado al lenguaje MQL5.

Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXVII): Trabajando con las solicitudes comerciales - Colocación de órdenes pendientes Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXVII): Trabajando con las solicitudes comerciales - Colocación de órdenes pendientes

En el presente artículo, continuaremos trabajando con las solicitudes comerciales e implementaremos la colocación de órdenes pendientes. Asimismo, corregiremos algunos errores localizados en el funcionamiento de la clase comercial.