Ejemplo de búsqueda de una estrategia de trading mediante SQLite

Intentemos utilizar SQLite para resolver problemas prácticos. Importaremos estructuras a la base de datos MqlRates con el historial de cotizaciones y las analizaremos para identificar patrones y buscar posibles estrategias de trading. Por supuesto, cualquier lógica elegida también se puede implementar en MQL5, pero SQL le permite hacerlo de una manera diferente, en muchos casos de manera más eficiente y utilizando muchas funciones SQL integradas interesantes. El tema del libro, orientado al aprendizaje de MQL5, no permite profundizar en esta tecnología, pero la mencionamos como digna de la atención de un operador de trading algorítmico.

El script para convertir el historial de cotizaciones en un formato de base de datos se llama DBquotesImport.mq5. En los parámetros de entrada, puede establecer el prefijo del nombre de la base de datos y el tamaño de la transacción (el número de registros de una transacción).

input string Database = "MQL5Book/DB/Quotes";
input int TransactionSize = 1000;

Para añadir estructuras MqlRates a la base de datos utilizando nuestra capa ORM, el script define una estructura MqlRatesDB auxiliar que proporciona las reglas para vincular campos de estructura a columnas base. Dado que nuestro script sólo escribe datos en la base de datos y no los lee desde allí, no es necesario vincularlo mediante la función DatabaseReadBind, que impondría una restricción a la «simplicidad» de la estructura. La ausencia de una restricción permite derivar la estructura MqlRatesDB de MqlRates (y no repetir la descripción de los campos).

struct MqlRatesDBpublic MqlRates
{
   /* for reference:
   
      datetime time;
      double   open;
      double   high;
      double   low;
      double   close;
      long     tick_volume;
      int      spread;
      long     real_volume;
   */
   
   bool bindAll(DBQuery &qconst
   {
      return q.bind(0time)
         && q.bind(1open)
         && q.bind(2high)
         && q.bind(3low)
         && q.bind(4close)
         && q.bind(5tick_volume)
         && q.bind(6spread)
         && q.bind(7real_volume);
   }
   
   long rowid(const long setter = 0)
   {
 // rowid is set by us according to the bar time
      return time;
   }
};
   
DB_FIELD_C1(MqlRatesDBdatetimetimeDB_CONSTRAINT::PRIMARY_KEY);
DB_FIELD(MqlRatesDBdoubleopen);
DB_FIELD(MqlRatesDBdoublehigh);
DB_FIELD(MqlRatesDBdoublelow);
DB_FIELD(MqlRatesDBdoubleclose);
DB_FIELD(MqlRatesDBlongtick_volume);
DB_FIELD(MqlRatesDBintspread);
DB_FIELD(MqlRatesDBlongreal_volume);

El nombre de la base de datos se forma a partir del prefijo Database, el nombre y el marco temporal del gráfico actual en el que se está ejecutando el script. Se crea una única tabla «MqlRatesDB» en la base de datos con la configuración de campos especificada por las macros DB_FIELD. Tenga en cuenta que la clave primaria no será generada por la base de datos, sino que se toma directamente de las barras, del campo time (hora de apertura de la barra).

void OnStart()
{
   Print("");
   DBSQLite db(Database + _Symbol + PeriodToString());
   if(!PRTF(db.isOpen())) return;
   
   PRTF(db.deleteTable(typename(MqlRatesDB)));
   
   if(!PRTF(db.createTable<MqlRatesDB>(true))) return;
   ...

A continuación, utilizando paquetes de barras TransactionSize, solicitamos barras del historial y las añadimos a la tabla. Esta es una tarea de la función auxiliar ReadChunk, llamada en bucle mientras haya datos (la función devuelve true) o el usuario no detenga el script manualmente. El código de la función se muestra a continuación:

   int offset = 0;
   while(ReadChunk(dboffsetTransactionSize) && !IsStopped())
   {
      offset += TransactionSize;
   }

Una vez finalizado el proceso, pedimos a la base de datos el número de registros generados en la tabla y lo enviamos al registro.

   DBRow *rows[];
   if(db.prepare(StringFormat("SELECT COUNT(*) FROM %s",
      typename(MqlRatesDB))).readAll(rows))
   {
      Print("Records added: "rows[0][0].integer_value);
   }
}

La función ReadChunk tiene el siguiente aspecto:

bool ReadChunk(DBSQLite &dbconst int offsetconst int size)
{
   MqlRates rates[];
   MqlRatesDB ratesDB[];
   const int n = CopyRates(_SymbolPERIOD_CURRENToffsetsizerates);
   if(n > 0)
   {
      DBTransaction tr(dbtrue);
      Print(rates[0].time);
      ArrayResize(ratesDBn);
      for(int i = 0i < n; ++i)
      {
         ratesDB[i] = rates[i];
      }
      
      return db.insert(ratesDB);
   }
   else
   {
      Print("CopyRates failed: "_LastError" "E2S(_LastError));
   }
   return false;
}

Llama a la función integrada CopyRates a través de la cual se rellena el array de barras rates. A continuación, las barras se transfieren al array ratesDB, de modo que con una sola sentencia db.insert(ratesDB) podríamos escribir información en la base de datos (en MqlRatesDB hemos formalizado cómo hacerlo correctamente).

La presencia del objeto DBTransaction (con la opción «commit» automática activada) dentro del bloque significa que todas las operaciones con el array se «superponen» a una transacción. Para indicar el progreso, durante el procesamiento de cada bloque de barras, la etiqueta de la primera barra se muestra en el registro.

Mientras la función CopyRates devuelve los datos y su inserción en la base de datos se realiza con éxito, el bucle en OnStart continúa con el desplazamiento de los números de las barras copiadas hacia el fondo del historial. Cuando se alcance el final del historial disponible o el límite de barras establecido en la configuración del terminal, CopyRates devolverá el error 4401 (HISTORY_NOT_FOUND) y el script saldrá.

Vamos a ejecutar el script en XAUUSD, gráfico H1. El registro debería mostrar algo como lo siguiente:

   db.isOpen()=true / ok
   db.deleteTable(typename(MqlRatesDB))=true / ok
   db.createTable<MqlRatesDB>(true)=true / ok
   2022.06.29 20:00:00
   2022.05.03 04:00:00
   2022.03.04 10:00:00
   ...
   CopyRates failed: 4401 HISTORY_NOT_FOUND
   Records added: 100000

Ahora tenemos la base QuotesEURUSDH1.sqlite, sobre la que puede experimentar para probar diversas hipótesis de trading. Puede abrirlo en MetaEditor para asegurarse de que los datos se transfieren correctamente.

Comprobemos una de las estrategias más sencillas basadas en regularidades del historial. Encontraremos las estadísticas de dos barras consecutivas en la misma dirección, desglosadas por hora intradiaria y día de la semana. Si existe una ventaja tangible para alguna combinación de hora y día de la semana, puede considerarse en el futuro como una señal para entrar en el mercado en la dirección de la primera barra.

En primer lugar, diseñemos una consulta SQL que solicite las cotizaciones de un periodo determinado y calcule el movimiento de precios en cada barra, es decir, la diferencia entre precios de apertura adyacentes.

Dado que la hora de las barras se almacena como un número de segundos (según los estándares de datetime en MQL5 y, concurrentemente, la «época Unix» de SQL), es deseable convertir su visualización en una cadena para facilitar su lectura, por lo que vamos a iniciar la consulta SELECT del campo datetime basada en la función DATETIME:

SELECT
   DATETIME(time, 'unixepoch') as datetime, open, ...

Este campo no participará en el análisis y se facilita aquí sólo para el usuario. A continuación, se muestra el precio como referencia, para que podamos comprobar el cálculo de los incrementos de precio mediante la impresión de depuración.

Dado que vamos a seleccionar, en su caso, un período determinado de todo el archivo, la condición requerirá el campo time en «forma pura», y también deberá añadirse a la solicitud. Además, según el análisis de cotizaciones previsto, necesitaremos aislar de la etiqueta de la barra su hora intradiaria, así como el día de la semana (su numeración corresponde a la adoptada en MQL5, 0 es domingo). Vamos a llamar a las dos últimas columnas de la consulta intraday y day, respectivamente, y utilizaremos las funciones TIME y STRFTIME para obtenerlas.

SELECT
   DATETIME(time, 'unixepoch') as datetime, open,
   time,
   TIME(time, 'unixepoch') AS intraday,
   STRFTIME('%w', time, 'unixepoch') AS day, ...

Para calcular el incremento de precio en SQL, puede utilizar la función LAG. Devuelve el valor de la columna especificada con un desplazamiento del número de filas especificado. Por ejemplo, LAG(X, 1) significa obtener el valor X de la entrada anterior, con el segundo parámetro 1 que significa el desplazamiento por defecto a 1, es decir, se puede omitir para obtener la entrada equivalente LAG(X). Para obtener el valor de la siguiente entrada, llame a LAG(X,-1). En cualquier caso, cuando se utiliza LAG, se requiere una construcción sintáctica adicional que especifique el orden de clasificación de los registros, en el caso más sencillo, en forma de OVER(ORDER BY column).

Así, para obtener el incremento de precio entre los precios de apertura de dos barras vecinas, escribimos:

   ...
   (LAG(open,-1) OVER (ORDER BY time) - open) AS delta, ...

Esta columna es predictiva porque mira hacia el futuro.

Podemos revelar que dos barras se formaron en la misma dirección multiplicando los incrementos por ellas: los valores positivos indican una subida o bajada constante:

...

(LAG(open,-1) OVER (ORDER BY time) - open) * (open - LAG(open) OVER (ORDER BY time))

AS product, ...

Este indicador se elige como el más sencillo de utilizar en el cálculo: para sistemas de trading reales, puede elegir un criterio más complejo.

Para evaluar el beneficio generado por el sistema en el backtest, es necesario multiplicar la dirección de la barra anterior (que actúa como indicador del movimiento futuro) por el incremento del precio en la barra siguiente. La dirección se calcula en la columna direction (utilizando la función SIGNO), sólo como referencia. La estimación de beneficios en la columna estimate es el producto del movimiento anterior direction y el incremento de la barra siguiente (delta): si se conserva la dirección, obtenemos un resultado positivo (en puntos).

...

SIGN(open - LAG(open) OVER (ORDER BY time)) AS direction,

(LAG(open,-1) OVER (ORDER BY time) - open) * SIGN(open - LAG(open) OVER (ORDER BY time))

AS estimate ...

En las expresiones de un comando SQL, no se pueden utilizar alias AS definidos en el mismo comando. Por eso no podemos determinar estimate como delta * direction, y tenemos que repetir el cálculo del producto explícitamente. Sin embargo, recordamos que las columnas delta y direction no son necesarias para el análisis programático y se añaden aquí sólo para visualizar la tabla ante el usuario.

Al final del comando SQL especificamos la tabla desde la que se realiza la selección y las condiciones de filtrado para el intervalo de fechas del backtest: dos parámetros «desde» y «hasta».

...
FROM MqlRatesDB
WHERE (time >= ?1 AND time < ?2)

Opcionalmente, podemos añadir una restricción LIMIT?3 (e introducir algún valor pequeño, por ejemplo, 10) para que la verificación visual de los resultados de la consulta al principio no le obligue a buscar entre decenas de miles de registros.

Puede comprobar el funcionamiento del comando SQL utilizando la función DatabasePrint; sin embargo, la función, desafortunadamente, no le permite trabajar con consultas preparadas con parámetros. Por lo tanto, tendremos que sustituir la preparación del parámetro SQL '?n' por el formato de cadena de consulta utilizando StringFormat y sustituir allí los valores de los parámetros. Otra posibilidad sería evitar por completo DatabasePrint y enviar los resultados al registro de forma independiente, línea por línea (a través de un array DBRow).

Así, el fragmento final de la solicitud se convertirá en:

   ...
   WHERE (time >= %ld AND time < %ld)
   ORDER BY time LIMIT %d;

Debe tenerse en cuenta que los valores datetime de esta consulta procederán de MQL5 en formato «máquina», es decir, el número de segundos transcurridos desde el comienzo de 1970. Si queremos depurar la misma consulta SQL en MetaEditor, entonces es más conveniente escribir la condición de rango de fechas usando literales de fecha (cadenas) como sigue:

   WHERE (time >= STRFTIME('%s', '2015-01-01') AND time < STRFTIME('%s', '2021-01-01'))

De nuevo, aquí necesitamos usar la función STRFTIME (el modificador '%s' en SQL establece la transferencia de la cadena de fecha especificada a la etiqueta «Unix epoch»; el hecho de que '%s' se parezca a una cadena de formato MQL5 es sólo una coincidencia).

Guarde la consulta SQL diseñada en un archivo de texto independiente DBQuotesIntradayLag.sql y conéctelo como recurso al script de prueba del mismo nombre, DBQuotesIntradayLag.mq5.

#resource "DBQuotesIntradayLag.sql" as string sql1

El primer parámetro del script le permite establecer un prefijo en el nombre de la base de datos, que ya debería existir después de lanzar DBquotesImport.mq5 en el gráfico con el mismo símbolo y marco temporal. Las siguientes entradas son para el intervalo de fechas y el límite de longitud de la impresión de depuración en el registro.

input string Database = "MQL5Book/DB/Quotes";
input datetime SubsetStart = D'2022.01.01';
input datetime SubsetStop = D'2023.01.01';
input int Limit = 10;

La tabla con cotizaciones se conoce de antemano, por el script anterior.

const string Table = "MqlRatesDB";

En la función OnStart abrimos la base de datos y nos aseguramos de que la tabla de cotizaciones está disponible.

void OnStart()
{
   Print("");
   DBSQLite db(Database + _Symbol + PeriodToString());
   if(!PRTF(db.isOpen())) return;
   if(!PRTF(db.hasTable(Table))) return;
   ...

A continuación, sustituimos los parámetros en la cadena de consulta SQL. Prestamos atención no sólo a la sustitución de los parámetros SQL '?n' por secuencias de formato, sino también al doble de los símbolos de porcentaje '%' primero, porque de lo contrario la función StringFormat los percibirá como sus propios comandos, y no los pasará por alto en SQL.

   string sqlrep = sql1;
   StringReplace(sqlrep"%""%%");
   StringReplace(sqlrep"?1""%ld");
   StringReplace(sqlrep"?2""%ld");
   StringReplace(sqlrep"?3""%d");
   
   const string sqlfmt = StringFormat(sqlrepSubsetStartSubsetStopLimit);
   Print(sqlfmt);

Todas estas manipulaciones eran necesarias únicamente para ejecutar la solicitud en el contexto de la función DatabasePrint. En la versión de trabajo del script analítico, leeríamos los resultados de la consulta y los analizaríamos mediante programación, pasando por alto el formateo y llamando a DatabasePrint.

Por último, vamos a ejecutar la consulta SQL y mostrar la tabla con los resultados en el registro.

   DatabasePrint(db.getHandle(), sqlfmt0);
}

Esto es lo que veremos para 10 barras EURUSD,H1 a principios de 2022:

db.isOpen()=true / ok

db.hasTable(Table)=true / ok

SELECT

DATETIME(time, 'unixepoch') as datetime,

open,

time,

TIME(time, 'unixepoch') AS intraday,

STRFTIME('%w', time, 'unixepoch') AS day,

(LAG(open,-1) OVER (ORDER BY time) - open) AS delta,

SIGN(open - LAG(open) OVER (ORDER BY time)) AS direction,

(LAG(open,-1) OVER (ORDER BY time) - open) * (open - LAG(open) OVER (ORDER BY time))

AS product,

(LAG(open,-1) OVER (ORDER BY time) - open) * SIGN(open - LAG(open) OVER (ORDER BY time))

AS estimate

FROM MqlRatesDB

WHERE (time >= 1640995200 AND time < 1672531200)

ORDER BY time LIMIT 10;

#| datetime open time intraday day delta dir product estimate

--+------------------------------------------------------------------------------------------------

1| 2022-01-03 00:00:00 1.13693 1641168000 00:00:00 1 0.0003200098

2| 2022-01-03 01:00:00 1.13725 1641171600 01:00:00 1 2.999999e-05 1 9.5999478e-09 2.999999e-05

3| 2022-01-03 02:00:00 1.13728 1641175200 02:00:00 1 -0.001060006 1 -3.1799748e-08 -0.001060006

4| 2022-01-03 03:00:00 1.13622 1641178800 03:00:00 1 -0.0003400007 -1 3.6040028e-07 0.0003400007

5| 2022-01-03 04:00:00 1.13588 1641182400 04:00:00 1 -0.001579991 -1 5.3719982e-07 0.001579991

6| 2022-01-03 05:00:00 1.1343 1641186000 05:00:00 1 0.0005299919 -1 -8.3739827e-07 -0.0005299919

7| 2022-01-03 06:00:00 1.13483 1641189600 06:00:00 1 -0.0007699937 1 -4.0809905e-07 -0.0007699937

8| 2022-01-03 07:00:00 1.13406 1641193200 07:00:00 1 -0.0002600149 -1 2.0020098e-07 0.0002600149

9| 2022-01-03 08:00:00 1.1338 1641196800 08:00:00 1 0.000510001 -1 -1.3260079e-07 -0.000510001

10| 2022-01-03 09:00:00 1.13431 1641200400 09:00:00 1 0.0004800036 1 2.4480023e-07 0.0004800036

...

Es fácil asegurarse de que la hora intradiaria de la barra está correctamente asignada, así como el día de la semana - 1, que corresponde al lunes. También puede comprobar el incremento delta. Los valores product y estimate están vacíos en la primera fila porque necesitan que se calcule la fila anterior que falta.

Vamos a complicar nuestra consulta SQL agrupando los registros con las mismas combinaciones de hora del día (intraday) y día de la semana (day), y calculando un determinado indicador objetivo que caracterice el éxito del trading para cada una de estas combinaciones. Tomemos como indicador el tamaño medio de las celdas product dividido por la desviación típica de los mismos productos. Cuanto mayor sea el producto medio de los incrementos de precio de las barras vecinas, mayor será el beneficio esperado, y cuanto menor sea la dispersión de estos productos, más estable será la previsión. El nombre del indicador en la consulta SQL es objective.

Además del indicador de objetivos, también calcularemos la estimación de beneficios (backtest_profit) y el factor de beneficios (backtest_PF). Estimaremos el beneficio como la suma de los incrementos de precio (estimate) para todas las barras en el contexto de la hora intradiaria y el día de la semana (el tamaño de la barra de apertura como incremento de precio es un análogo del beneficio futuro en puntos por una barra). El factor de beneficio es tradicionalmente el cociente de los incrementos positivos y negativos.

SELECT

AVG(product) / STDDEV(product) AS objective,

SUM(estimate) AS backtest_profit,

SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /

SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS backtest_PF,

intraday, day

FROM

(

SELECT

time,

TIME(time, 'unixepoch') AS intraday,

STRFTIME('%w', time, 'unixepoch') AS day,

(LAG(open,-1) OVER (ORDER BY time) - open) AS delta,

SIGN(open - LAG(open) OVER (ORDER BY time)) AS direction,

(LAG(open,-1) OVER (ORDER BY time) - open) * (open - LAG(open) OVER (ORDER BY time))

AS product,

(LAG(open,-1) OVER (ORDER BY time) - open) * SIGN(open - LAG(open) OVER (ORDER BY time))

AS estimate

FROM MqlRatesDB

WHERE (time >= STRFTIME('%s', '2015-01-01') AND time < STRFTIME('%s', '2021-01-01'))

)

GROUP BY intraday, day

ORDER BY objective DESC

Se ha anidado la primera consulta SQL, a partir de la cual ahora acumulamos datos con una consulta SQL externa. La agrupación por todas las combinaciones de hora y día de la semana proporciona un «extra» de GROUP BY intraday, day. Además, hemos añadido la ordenación por indicador de destino (ORDER BY objective DESC) para que las mejores opciones estén en la parte superior de la tabla.

En la consulta anidada, eliminamos el parámetro LIMIT, ya que el número de grupos ha pasado a ser aceptable, mucho menor que el número de barras analizadas. Así, para H1 obtenemos 120 opciones (24 * 5).

La consulta ampliada se coloca en el archivo de texto DBQuotesIntradayLagGroup.sql, que a su vez está conectado como recurso al script de prueba del mismo nombre, DBQuotesIntradayLagGroup.mq5. Su código fuente difiere poco del anterior, por lo que mostraremos inmediatamente el resultado de su lanzamiento para el intervalo de fechas por defecto: desde principios de 2015 hasta principios de 2021 (excluyendo 2021 y 2022).

db.isOpen()=true / ok

db.hasTable(Table)=true / ok

SELECT

AVG(product) / STDDEV(product) AS objective,

SUM(estimate) AS backtest_profit,

SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /

SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS backtest_PF,

intraday, day

FROM

(

SELECT

...

FROM MqlRatesDB

WHERE (time >= 1420070400 AND time < 1609459200)

)

GROUP BY intraday, day

ORDER BY objective DESC

#| objective backtest_profit backtest_PF intraday day

---+---------------------------------------------------------------------------

1| 0.16713214428916 0.073200000000001 1.46040631486258 16:00:00 5

2| 0.118128291843983 0.0433099999999995 1.33678071539657 20:00:00 3

3| 0.103701251751617 0.00929999999999853 1.14148790506616 05:00:00 2

4| 0.102930330078208 0.0164399999999973 1.1932071923845 08:00:00 4

5| 0.089531492651001 0.0064300000000006 1.10167615433271 07:00:00 2

6| 0.0827628326995007 -8.99999999970369e-05 0.999601152226913 17:00:00 4

7| 0.0823433025146974 0.0159700000000012 1.21665988332657 21:00:00 1

8| 0.0767938336191962 0.00522999999999874 1.04226945769012 13:00:00 1

9| 0.0657741522256548 0.0162299999999986 1.09699976093712 15:00:00 2

10| 0.0635243373432768 0.00932000000000044 1.08294766820933 22:00:00 3

...

110| -0.0814131025461459 -0.0189100000000015 0.820605255668329 21:00:00 5

111| -0.0899571263478305 -0.0321900000000028 0.721250432975386 22:00:00 4

112| -0.0909772560603298 -0.0226100000000016 0.851161872161138 19:00:00 4

113| -0.0961794181717023 -0.00846999999999931 0.936377976414036 12:00:00 5

114| -0.108868074018582 -0.0246099999999998 0.634920634920637 00:00:00 5

115| -0.109368419185336 -0.0250700000000013 0.744496534855268 08:00:00 2

116| -0.121893581607986 -0.0234599999999998 0.610945273631843 00:00:00 3

117| -0.135416609546408 -0.0898899999999971 0.343437294573087 00:00:00 1

118| -0.142128458003631 -0.0255200000000018 0.681835182645536 06:00:00 4

119| -0.142196924506816 -0.0205700000000004 0.629769618430515 00:00:00 2

120| -0.15200009633513 -0.0301499999999988 0.708864426419475 02:00:00 1

Por lo tanto, el análisis nos indica que la barra H1 de 16 horas del viernes es la mejor candidata para continuar la tendencia basada en la barra anterior. Le sigue en preferencia la barra de los miércoles a las 20 horas. Y así sucesivamente.

Sin embargo, es conveniente comprobar los ajustes encontrados en el periodo de reenvío.

Para ello, podemos ejecutar la consulta SQL actual no sólo en el intervalo de fechas «pasado» (en nuestra prueba hasta 2021), sino una vez más en el «futuro» (desde principios de 2021). Los resultados de ambas consultas deben unirse (JOIN) por nuestros grupos (intraday, day). A continuación, manteniendo la ordenación por el indicador objetivo, veremos en las columnas adyacentes el beneficio y el factor de beneficio para las mismas combinaciones de hora y día de la semana, y cuánto se hundieron.

He aquí la consulta SQL final (abreviada):

SELECT * FROM

(

SELECT

AVG(product) / STDDEV(product) AS objective,

SUM(estimate) AS backtest_profit,

SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /

SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS backtest_PF,

intraday, day

FROM

(

SELECT ...

FROM MqlRatesDB

WHERE (time >= STRFTIME('%s', '2015-01-01') AND time < STRFTIME('%s', '2021-01-01'))

)

GROUP BY intraday, day

) backtest

JOIN

(

SELECT

SUM(estimate) AS forward_profit,

SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /

SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS forward_PF,

intraday, day

FROM

(

SELECT ...

FROM MqlRatesDB

WHERE (time >= STRFTIME('%s', '2021-01-01'))

)

GROUP BY intraday, day

) forward

USING(intraday, day)

ORDER BY objective DESC

El texto completo de la solicitud figura en el archivo DBQuotesIntradayBackAndForward.sql. Se conecta como recurso en el script DBQuotesIntradayBackAndForward.mq5.

Ejecutando el script con la configuración por defecto, obtenemos los siguientes indicadores (con abreviaturas):

#| objective backtest_profit backtest_PF intraday day forward_profit forward_PF

--+------------------------------------------------------------------------------------------------

1| 0.16713214428916 0.073200000001 1.46040631486 16:00:00 5 0.004920000048 1.12852664576

2| 0.118128291843983 0.0433099999995 1.33678071539 20:00:00 3 0.007880000055 1.277856135

3| 0.103701251751617 0.00929999999853 1.14148790506 05:00:00 2 0.002210000082 1.12149532710

4| 0.102930330078208 0.0164399999973 1.1932071923 08:00:00 4 0.001409999969 1.07253086419

5| 0.089531492651001 0.0064300000006 1.10167615433 07:00:00 2 -0.009119999869 0.561749159058

6| 0.0827628326995007 -8.99999999970e-05 0.999601152226 17:00:00 4 0.009070000091 1.18809622563

7| 0.0823433025146974 0.0159700000012 1.21665988332 21:00:00 1 0.00250999999 1.12131464475

8| 0.0767938336191962 0.00522999999874 1.04226945769 13:00:00 1 -0.008490000055 0.753913043478

9| 0.0657741522256548 0.0162299999986 1.09699976093 15:00:00 2 0.01423999997 1.34979120609

10| 0.0635243373432768 0.00932000000044 1.08294766820 22:00:00 3 -0.00456999993 0.828967065868

...

Así pues, el sistema de trading con los mejores calendarios de trading encontrados sigue arrojando beneficios en el periodo «futuro», aunque no tan grandes como en el backtest.

Por supuesto, el ejemplo considerado es sólo un caso particular de un sistema de trading. Podríamos, por ejemplo, encontrar combinaciones de la hora y el día de la semana en que una estrategia de inversión funciona en barras vecinas, o basarnos en otros principios totalmente distintos (análisis de ticks, calendario, cartera de señales de trading, etc.).

La conclusión es que el motor SQLite proporciona muchas herramientas prácticas que tendría que implementar en MQL5 por su cuenta. A decir verdad, aprender SQL lleva su tiempo. La plataforma permite elegir la combinación óptima de dos tecnologías para una programación eficaz.