SQLite: trabajo nativo con bases de datos en SQL en MQL5
Contenido
- Trading algorítmico moderno en MetaTrader 5
- Funciones para trabajar con las bases de datos
- Ejemplo de solicitud simple
- Depuración de solicitudes SQL en el MetaEditor
- Lectura automática de los resultados de las solicitudes en la estructura con la ayuda de DatabaseReadBind()
- Acelerando las transacciones con el envoltorio en DatabaseTransactionBegin()/DatabaseTransactionCommit()
- Trabajando con las transacciones de la historia comercial
- Análisis del portafolio desde el punto de vista de la estrategia
- Análisis de transacciones según el símbolo
- Análisis de transacciones según la hora de entrada
- Mostrando de forma sencilla los datos en el diario de asesores en DatabasePrint()
- Importación y exportación de datos
- Guardando los resultados de la optimización en la base de datos
- Optimizando la ejecución de solicitudes con la ayuda de índices
- Integración del trabajo con las bases de datos en MetaEditor
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.0El 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:
- todos los manejadores de las solicitudes deberán ser eliminados después de utilizarse con la ayuda de DatabaseFinalize();
- antes de finalizar su funcionamiento, la base de datos deberá cerrarse con la ayuda de DatabaseClose();
- los resultados de la ejecución de las solicitudes deberán comprobarse;
- 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.
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:
- DatabaseColumnName — nombre,
- DatabaseColumnType — tipo de datos,
- DatabaseColumnSize — tamaño de los datos en bytes,
- DatabaseColumnText — lee en forma de texto,
- DatabaseColumnInteger — obtiene un valor del tipo int,
- DatabaseColumnLong — obtiene un valor del tipo long,
- DatabaseColumnDouble — obtiene un
valor del tipo double,
- DatabaseColumnBlob — obtiene una matriz de datos.
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.
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.
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 Ltd.
Artículo original: https://www.mql5.com/ru/articles/7463
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso