Simulación de mercado: Iniciando SQL en MQL5 (V)
Introducción
Hola a todos y bienvenidos a un artículo más de la serie sobre cómo construir un sistema de repetición/simulación.
En el artículo anterior, Simulación de mercado: Iniciando SQL en MQL5 (IV), expliqué una pequeña precaución que deberemos tomar al usar SQLite dentro de MetaTrader 5. Aunque puede que esto cambie en el futuro, por precaución siempre debes probar primero antes de usar SQL con SQLite dentro de MetaTrader 5. De todos modos, allí mostré cómo debías proceder para poder añadir el mecanismo de consulta, para que, dentro del código MQL5, pudieras usar SQL plenamente y obtener los resultados al usar el comando SELECT FROM de SQL.
Pero faltó hablar de la última función que necesitamos implementar. Esta es la función DatabaseReadBind. Y, como para entenderla adecuadamente hace falta una explicación un poco más amplia, se decidió hacerlo no en aquel artículo anterior, sino en este. Entonces, como el tema será relativamente largo, vayamos directamente al siguiente tema.
Implementando la función de lectura de las consultas
La implementación de la función responsable de leer los datos devueltos por SQL es bastante simple y directa. Pero hay un detalle que nunca debes olvidar. El resultado que devolverá la función de lectura es precisamente el resultado de la ejecución anterior de la llamada ExecRequestOfData. Por eso, esta siempre deberá preceder a la llamada para leer los datos devueltos.
Aquí, sin embargo, conviene abrir un paréntesis. Para entender esto mejor, primero hace falta ver el código de la clase. Así, el código completo de la clase C_DB_SQL puede verse a continuación. Recuerda que este código está orientado al uso de SQL integrado en MetaTrader 5. Para otros casos de uso de SQL, este código deberá modificarse para incorporar las funcionalidades necesarias.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\Service Graphics\Support\C_Array.mqh" 005. //+------------------------------------------------------------------+ 006. class C_DB_SQL 007. { 008. private : 009. C_Array m_Arr; 010. int m_handleDB, 011. m_Request; 012. //+------------------------------------------------------------------+ 013. void Convert(const char &buff[], const int size) 014. { 015. string sz0 = ""; 016. bool b0, b1, bs1, bs2, bc0, bc1, bc; 017. int nLine = 1; 018. 019. b0 = b1 = bs1 = bs2 = bc0 = bc1 = bc = false; 020. for (int count = 0, nC0 = nLine; count < size; count++) 021. { 022. switch (buff[count]) 023. { 024. case '\t': 025. sz0 += (bs1 || bs2 ? "\t" : ""); 026. break; 027. case '\n': 028. nC0++; 029. case '\r': 030. bc0 = false; 031. break; 032. case ';': 033. b0 = (bs1 || bs2 || bc0 || bc1 ? b0 : true); 034. default: 035. switch (buff[count]) 036. { 037. case '"': 038. bs1 = (bs2 || bc0 || bc1 ? bs1 : !bs1); 039. break; 040. case '\'': 041. bs2 = (bs1 || bc0 || bc1 ? bs2 : !bs2); 042. break; 043. } 044. if (((count + 1) < size) && (!bs1) && (!bs2)) 045. { 046. if (bc = ((buff[count] == '-') && (buff[count + 1] == '-'))) bc0 = true; 047. if (bc = ((buff[count] == '/') && (buff[count + 1] == '*'))) bc1 = true; 048. if (bc = ((buff[count] == '*') && (buff[count + 1] == '/'))) bc1 = false; 049. if (bc) 050. { 051. count += 1; 052. bc = false; 053. continue; 054. } 055. } 056. if (!(bc0 || bc1)) 057. { 058. if ((!b1) && (buff[count] > ' ')) 059. { 060. b1 = true; 061. nLine = nC0; 062. } 063. sz0 += (b1 ? StringFormat("%c", buff[count]) : ""); 064. } 065. } 066. if (b0) 067. { 068. m_Arr.Add(sz0, nLine); 069. sz0 = ""; 070. b0 = b1 = false; 071. } 072. } 073. } 074. //+------------------------------------------------------------------+ 075. const string ExecSQL(void) 076. { 077. string szCmd; 078. 079. for (int count = 0, nLine; count >= 0; count++) 080. { 081. szCmd = m_Arr.At(count, nLine); 082. if (nLine < 0) break; 083. if (!ExecCommandSQL(szCmd)) 084. return StringFormat("Execution of line %d of the SQL script failed...", nLine); 085. } 086. 087. return NULL; 088. } 089. //+------------------------------------------------------------------+ 090. public : 091. //+------------------------------------------------------------------+ 092. C_DB_SQL(const string szFileName = ":memory:") 093. { 094. m_handleDB = DatabaseOpen(szFileName, DATABASE_OPEN_CREATE | DATABASE_OPEN_READWRITE); 095. m_Request = INVALID_HANDLE; 096. } 097. //+------------------------------------------------------------------+ 098. ~C_DB_SQL() 099. { 100. DatabaseClose(m_handleDB); 101. } 102. //+------------------------------------------------------------------+ 103. const string ExecResourceSQL(const string szResource) 104. { 105. char buff[]; 106. int size; 107. 108. ArrayResize(buff, size = StringLen(szResource)); 109. StringToCharArray(szResource, buff); 110. Convert(buff, size); 111. ArrayFree(buff); 112. 113. return ExecSQL(); 114. } 115. //+------------------------------------------------------------------+ 116. const string ExecScriptSQL(const string szFileName) 117. { 118. int file, size; 119. char buff[]; 120. 121. if ((file = FileOpen(szFileName, FILE_READ | FILE_BIN)) == INVALID_HANDLE) 122. return StringFormat("Unable to open script file: %s", szFileName); 123. ArrayResize(buff, size = (int) FileSize(file)); 124. FileReadArray(file, buff); 125. FileClose(file); 126. Convert(buff, size); 127. ArrayFree(buff); 128. 129. return ExecSQL(); 130. } 131. //+------------------------------------------------------------------+ 132. bool ExecCommandSQL(const string szCmd) 133. { 134. return (m_handleDB == INVALID_HANDLE ? false : DatabaseExecute(m_handleDB, szCmd)); 135. } 136. //+------------------------------------------------------------------+ 137. bool ExecRequestOfData(const string szCmd) 138. { 139. if (m_Request != INVALID_HANDLE) DatabaseFinalize(m_Request); 140. return ((m_Request = DatabasePrepare(m_handleDB, szCmd)) != INVALID_HANDLE); 141. } 142. //+------------------------------------------------------------------+ 143. template <typename T> bool GetRegisterOfRequest(T &stObject, const bool Finish = true) 144. { 145. if (!DatabaseReadBind(m_Request, stObject)) 146. { 147. if (Finish) 148. { 149. DatabaseFinalize(m_Request); 150. m_Request = INVALID_HANDLE; 151. } 152. return false; 153. } 154. 155. return true; 156. } 157. //+------------------------------------------------------------------+ 158. }; 159. //+------------------------------------------------------------------+
Código de C_DB_SQL.mqh
Bien. Este es el código completo de la clase. Pero, como solo una pequeña parte todavía no se comentó, o mejor dicho, no se había mostrado antes, no voy a abordar las funciones o puntos ya comentados en otros artículos. Aquí nos centraremos únicamente en la función de la línea 143. Es decir, la función GetRegisterOfRequest. La primera pregunta, y sería una pregunta justa, es: ¿por qué esta función se declara de esta manera? Esta es una parte un poco complicada de explicar usando solo MQL5.
Para entender esto en detalle, sería necesario explicar un concepto muy complicado que existe en el lenguaje C/C++. Este concepto implica el uso de punteros de tipo void. Y sí, podemos declarar y usar un puntero de tipo void, no solo de tipos comunes, como muchos imaginan. Pero, como este concepto es extremadamente complicado de explicar en un artículo, intentaré mostrarte, querido lector, por qué esta función se declara de esta manera, evitando al máximo entrar en el lenguaje C/C++.
Para empezar, aquí tenemos una dependencia de tipo. No en la declaración de la función, que se hace en la línea 143, sino en la función DatabaseReadBind. Esta dependencia nos obliga a utilizar un determinado tipo de dato para que el compilador de MQL5 no genere una avalancha de errores al intentar compilar el código. Muchos programadores, cuando van a usar, o intentar usar, la función DatabaseReadBind, no la colocan en una llamada como la que estoy haciendo aquí. El motivo es precisamente que no entienden la dependencia de tipo que se genera aquí.
Entonces, ¿qué hacen normalmente? Crean algún procedimiento o función donde se hace la consulta, se analiza el resultado devuelto por SQL y, por último, se devuelve el valor normalmente al autor de llamada. Bien. Este tipo de enfoque funciona y es bastante adecuado para casos muy específicos. En otros casos, los programadores simplemente usan, cuando la usan, la función DatabaseReadBind dentro del cuerpo del código principal.
Pero, ¿por qué recurren a este enfoque? Nuevamente, el motivo es que existe una dependencia de tipo. Y esta no es tan simple de manejar aquí en MQL5. Sin embargo, cuando usamos C/C++, la situación cambia un poco, ya que en ese caso podemos usar un puntero de tipo void. Imagino que todavía no has entendido esta cuestión. Entonces, veamos cómo se declara la función DatabaseReadBind en MQL5. Puedes verlo a continuación.
bool DatabaseReadBind( int request, // manipulador da consulta criada no DatabasePrepare void& struct_object // referência para a estrutura para leitura do registro );
Esto que estás viendo puede consultarse en la documentación de la función en MQL5. Tal vez, para ti, esto no tenga mucho sentido. Pero, para un programador de C/C++, esta declaración tiene todo el sentido. Así que, sin entrar en detalles sobre C/C++, lo que tenemos aquí es lo siguiente: la función deberá recibir dos parámetros. El primer parámetro es un valor de tipo entero, que representa el identificador devuelto por la función DatabasePrepare. Hablamos de esto en el artículo anterior. El segundo parámetro, en cambio, es el gran detalle aquí. Este parámetro es un puntero, pero un puntero de tipo void. En teoría, podría ser de cualquier tipo, ya que el tipo void no especifica el tipo esperado.
Pero este puntero recibirá una referencia a una posición de memoria, y no una dirección, como sería lo natural. Esto se debe a que los punteros normalmente se refieren a una posición de memoria. Pero, cuando usamos el carácter ampersand (&) antes del nombre, estamos indicando que haremos una referencia a una variable, y no a una posición. Si realmente deseas entender esto, procura aprender a usar punteros en C/C++, ya que es algo que muchas veces requiere varios capítulos de un libro orientado a enseñar C/C++. Porque el tema es bastante complejo y denso.
Como MQL5 no nos permite usar punteros de la misma forma que en C/C++, necesitamos un pequeño truco para poder acceder a la función DatabaseReadBind, como se vio en el código de la clase C_DB_SQL. Ese truco consiste precisamente en usar un sistema de conversión de tipos, en el que le decimos a la función que podrá recibir cualquier tipo, y que el compilador lo convertirá internamente al tipo adecuado. Aparentemente, esto sería algo muy complicado y difícil de resolver. Pero, afortunadamente, MQL5 nos da la posibilidad de usar algo que también existe en C/C++, que es la declaración:
template <typename T >
Aquí hay un detalle. T puede ser cualquier cosa, pero normalmente usamos T por convención al programar. El detalle es que precisamente esta declaración, que aparece antes del nombre de la función, le dice al compilador que deberá realizar la conversión correcta para resolver el problema de tipado. Ahora observa lo siguiente: el primer parámetro de la función GetRegisterOfRequest es precisamente T. Como no podemos usar el tipo void, esta declaración, de esta forma, sustituye la forma de hacerlo en C/C++.
Bien. Pero todavía no consigo entender esta lógica. Al mirar la documentación de MQL5 sobre la función DatabaseReadBind, vemos que allí se indica que el segundo parámetro deberá ser una estructura. E incluso en los ejemplos donde se demuestra el uso de esta función, también se usa una estructura como segundo parámetro. Entonces, no consigo entender por qué esta complicación de usar template <typename T > y esta declaración extraña en la línea 143. ¿Por qué no pasar simplemente una estructura directamente a esta función? ¿Por qué complicar las cosas de esta manera?
Realmente, mi querido lector, coincido plenamente con tu forma de ver las cosas. Y estás en lo cierto. Podríamos simplemente pasar una estructura como parámetro de la función. Así, dejaríamos mucho más simple la declaración de la línea 143. Pero ahora te pregunto: ¿cuántos campos y qué tipo de campos debería tener esa estructura? Si logras responderme a esta pregunta de una manera que permita generalizar las cosas, con toda certeza modelaré la llamada de la línea 143 de otra forma.
Pero esa estructura jamás se construirá de un modo que permita generalizar todas las situaciones. Esto se debe a que, aunque conozcas el número de campos presentes en la base de datos, tarde o temprano ese número podrá cambiar. Y, si eso ocurre, toda la construcción tendrá que rehacerse aquí en el código MQL5. Por esta razón, muchos usan la función DatabaseReadBind de una forma distinta de la que estoy mostrando.
Esta función que estoy mostrando, cuya finalidad es devolver a nuestro código en MQL5 lo que SQL ha devuelto, es una función genérica. Y en breve entenderás por qué. En cualquier caso, en la línea 145 haremos la llamada y, según el valor devuelto, tendremos aquí dos situaciones completamente distintas. La primera se da cuando la función DatabaseReadBind logra leer algún registro devuelto por SQL. En ese caso, se ejecutará la línea 155. Pero puede ocurrir que ya no haya más registros dentro del bloque. Entonces tendremos dos situaciones distintas. Sin embargo, ambas quedarán dentro del bloque de error asociado al retorno de la función DatabaseReadBind.
Observa que, por defecto, el segundo parámetro de la función GetRegisterOfRequest es true, es decir, un valor verdadero. Cuando falle la comprobación de la línea 145, verificaremos en la línea 147 si el valor del segundo parámetro es verdadero o no. Si el autor de llamada no lo modificó, se ejecutará la función DatabaseFinish en la línea 149. Y, para completar, en la línea 150 modificamos el valor de la variable m_Request, indicando que ya no hay más registros disponibles y que será necesario hacer una nueva consulta en la base de datos. Pero, si el autor de llamada indica que el valor del parámetro Finish es falso, la comprobación de la línea 147 fallará y dejará las cosas tal como están. Esto será útil para lo que veremos después en el código principal. Pero, independientemente de ello, el valor devuelto será falso. Esto se debe a la línea 152.
Con esto, cerramos por completo la parte básica de la clase C_DB_SQL. A partir de ahora, cualquier código podrá usarla dentro de nuestro sistema de repetición/simulación. Pero, antes de hacerlo, necesitamos verificar si todo funciona como se espera. Y, para eso, pasemos a un nuevo tema, de modo que podamos separar mejor las cosas.
Probando la clase C_DB_SQL para usarla con SQLite de MetaTrader 5
Gran parte de nuestra clase C_DB_SQL ya fue probada. Ahora solo falta probar la parte referida a las consultas y lograr, de alguna manera, entender qué nos devolverá SQL cuando se haga una consulta.
Para intentar explicar esto de una forma adecuada y que, al mismo tiempo, resulte fácil de entender para todos, sobre todo para ti, querido lector y entusiasta, voy a mostrar un fragmento del código principal, mostrar el resultado obtenido, explicar qué está ocurriendo y por qué se obtuvo ese resultado. Y, hacia el final del artículo, pondré el código completo para que puedas probar localmente lo que haremos aquí.
Una vez entendida esta parte, vayamos a la primera prueba. Se hará usando el siguiente fragmento.
18. //+------------------------------------------------------------------+ 19. void SELECT_Type_01(void) 20. { 21. struct st 22. { 23. int id; 24. string symbol; 25. }stLocal; 26. 27. Print("Executing type 1 data request..."); 28. if ((*SQL).ExecRequestOfData("SELECT * FROM tb_Symbols")) 29. { 30. Print("Request: OK..."); 31. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 32. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 33. Print("Reload..."); 34. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 35. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 36. }; 37. Print("Finish type 1..."); 38. } 39. //+------------------------------------------------------------------+
Fragmento del archivo principal
La numeración de las líneas sigue exactamente lo que verás en el código completo. Así, podrás estudiar con calma cada fragmento. El resultado de la ejecución de este fragmento puede verse en la siguiente animación.

Observa que es algo bastante simple y directo. Observa que, en la línea 21, declaramos una estructura. Ten en cuenta que, dentro de esta estructura, tendremos dos variables. NO debes verlas de esa manera. No pienses que son variables, porque eso te confundirá y hará más difícil entender otras cosas después. Piensa en ellas como campos, o columnas, dentro de la respuesta devuelta por SQL. Entonces, tenemos una estructura que debe pensarse como una fila de respuesta de SQL. En esta fila, tendremos dos campos esperados.
Entonces, en la línea 28, intentamos solicitar datos de la base de datos con SQL. Presta mucha, muchísima atención a lo que está entre comillas dobles ("), porque ese deberá ser el comando que ejecutará SQL. Deberás escribir exactamente lo que deseas que SQL ejecute. En este ejemplo más simple, solo queremos todos los valores presentes en la tabla tb_Symbols. Aunque esta forma de consulta no es adecuada en la práctica, para nuestro ejemplo sí lo es, ya que nuestra tabla es muy pequeña y contiene pocos registros.
Si nuestra solicitud tiene éxito, primero ejecutaremos un bucle for en la línea 31. Ahora, presta atención al hecho de que lo que hará que el bucle termine será precisamente que ya no haya más registros devueltos. Y no dejes de notar que, durante estas llamadas de la línea 31, le estamos diciendo a la función GetRegisterOfRequest que no queremos cerrar el bloque leído. Es decir, cuando ya no haya más registros disponibles, no cerraremos el bloque. Mantendremos disponible el conjunto de registros devueltos.
Ya en la línea 32, imprimiremos en el terminal el resultado devuelto. En la línea 33, indicamos en el terminal que haremos una nueva lectura de los datos devueltos por SQL. Observa que estamos haciendo prácticamente lo mismo que se hizo en la línea 31. Sin embargo, esta vez no informaremos absolutamente nada en el segundo parámetro de la llamada a GetRegisterOfRequest. Entonces, en el momento en que el bloque se lea por completo, todo lo devuelto por SQL se descartará y, por ello, tendremos que hacer una nueva solicitud.
La siguiente forma de hacer las cosas se ve en el fragmento siguiente:
39. //+------------------------------------------------------------------+ 40. void SELECT_Type_02(void) 41. { 42. struct st 43. { 44. string of_day; 45. double price; 46. string symbol; 47. }stLocal; 48. 49. Print("Executing type 2 data request..."); 50. if ((*SQL).ExecRequestOfData("SELECT tq.of_day, tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 51. { 52. Print("Request: OK..."); 53. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 54. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 55. } 56. Print("Finish type 2..."); 57. } 58. //+------------------------------------------------------------------+
Fragmento del código principal
Y el resultado puede verse en la siguiente animación:

Observa que aquí tenemos algo un poco diferente. En primer lugar, en la línea 42, tenemos la declaración de una estructura que tendrá tres campos. En el caso anterior, teníamos dos campos por fila. Pero observa que, aunque tenemos más campos, seguimos usando un código muy parecido al anterior. Sin embargo, esta vez quiero que observes atentamente qué se usa como comando SQL en la línea 50. Observa que estamos haciendo una combinación entre la tabla tb_Quotes y la tabla tb_Symbol para presentar de una determinada manera lo que realmente existe en la base de datos.
Ahora, presta atención al orden en que se están declarando los campos en la solicitud a SQL. Esto es importante, ya que ese mismo orden será respetado por el comando DatabaseReadBind en el momento en que vayas a leer los datos de respuesta de SQL. Entonces, si cambias el orden, ya sea en el comando SQL o en la declaración de los campos dentro de la estructura de la línea 42, podrás ver cosas completamente inesperadas en los datos devueltos. Entonces, ten cuidado con estos detalles.
Ahora tenemos un caso un poco más complicado a primera vista. Pero no te dejes llevar, porque es tan simple como todos los demás. Solo haremos un uso un poco más elaborado de las funciones disponibles en MQL5 cuya finalidad es permitir el uso de SQL dentro de MQL5. Puedes ver el fragmento a continuación.
58. //+------------------------------------------------------------------+ 59. void SELECT_Type_03(void) 60. { 61. struct st0 62. { 63. double price; 64. string symbol; 65. }stLocal; 66. struct st1 67. { 68. string tmp1, 69. tmp2, 70. date; 71. }std; 72. 73. Print("Executing type 3 data request..."); 74. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol, tq.of_day FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 75. { 76. Print("Request: OK..."); 77. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 78. Print(" Resq ", c, " ==> { ", stLocal.price, " } ", stLocal.symbol); 79. Print("Reload..."); 80. for (int c = 0; (*SQL).GetRegisterOfRequest(std); c++) 81. Print(" Resq ", c, " ==> < ", std.date, " > "); 82. 83. } 84. Print("Finish type 3..."); 85. } 86. //+------------------------------------------------------------------+
Fragmento del código principal
El resultado de la ejecución de este fragmento puede verse a continuación.

Hasta el momento, hemos usado una estructura de fila con dos o tres campos. Pero, como expliqué en el tema anterior, podemos hacer mucho más que eso. Podemos hacer que SQL nos devuelva más campos de los que necesitamos. Y, después de analizar adecuadamente los campos que realmente queríamos, podemos aprovechar lo que SQL ya nos devolvió. Así, no necesitaremos hacer una nueva consulta solo para buscar los datos que realmente queremos. Esto se debe a que ya vinieron junto con la consulta anterior.
Aquí hay un detalle que necesitarás entender. Se trata de que cada fila devuelta por SQL se corresponderá con el contenido de cada campo específico. Pero, como podrás comprobar en este ejemplo, haremos la misma consulta del fragmento anterior. Solo que ahora SQL recibirá el orden de los campos en otra secuencia. Es muy, pero muy importante que notes esto. El motivo es que la estructura declarada en la línea 61 contiene dos campos. La estructura declarada en la línea 66, en cambio, contiene tres campos. Y SQL nos devolverá tres campos. Esto se debe a que el comando de consulta le indica a SQL que devuelva esos tres campos.
Ahora observa que la estructura st0 se intercala en la estructura st1. Sin embargo, el campo llamado date no existe en la estructura st0 y, por eso, se ignora en el momento en que vayamos a leer los datos devueltos por SQL. Esto ocurre en la línea 77, donde hacemos un bucle para leer e imprimir en el terminal los datos devueltos por SQL.
Sin embargo, en la línea 80 hacemos lo mismo. Solo que, esta vez, ignoraremos los demás datos que SQL nos devolvió y mostraremos únicamente el campo llamado date. Aquí no hay grandes complicaciones. Todo es cuestión de entender lo que estamos haciendo y lo que SQL nos está diciendo.
Ya debes de estar pensando que podemos hacer las cosas de cualquier manera, que, independientemente de lo que hagamos, se obtendrá algo. Sin embargo, las cosas no son exactamente así. Para comprobarlo, mira el siguiente fragmento.
086. //+------------------------------------------------------------------+ 087. void SELECT_Type_04(void) 088. { 089. struct st 090. { 091. string of_day; 092. double price; 093. string symbol; 094. }stLocal; 095. 096. Print("Executing type 4 data request..."); 097. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 098. { 099. Print("Request: OK..."); 100. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 101. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 102. } 103. Print("Finish type 4..."); 104. } 105. //+------------------------------------------------------------------+
Fragmento del código principal
El resultado de la ejecución de este fragmento puede verse a continuación y, en apariencia, resulta algo extraño, debido a que obtenemos un resultado completamente inesperado.

Puede que estés pensando que esto es bastante extraño. Pero no es tan extraño: basta con observar la declaración de la línea 89, donde decimos qué campos esperamos obtener en la consulta a SQL. Ahora mira la línea 97 y lo que estamos solicitando a SQL. Observa que la información no está emparejada entre lo que esperamos como resultado y lo que solicitamos a SQL.
Aquí surge una cuestión por la que, una vez más, debemos recurrir a la documentación de MQL5 para entender por qué el resultado es el que se muestra en la animación. Entonces, al consultar la documentación de la función principal de la rutina GetRegisterOfRequest, que es la función DatabaseReadBind, tenemos el siguiente fragmento destacado:
Observación
El número de campos en la estructura struct_object no puede exceder DatabaseColumnsCount(). Si el número de campos en la estructura struct_object es menor que el número de campos del registro, se realizará una lectura parcial. Los datos restantes pueden obtenerse explícitamente usando las funciones correspondientes DatabaseColumnText(), DatabaseColumnInteger() y así sucesivamente
Pero espera un momento, no estamos usando DatabaseColumnsCount. Entonces, el resultado no tiene sentido. Bien, en realidad sí lo tiene, ya que, en la consulta a SQL, estamos indicando que queremos dos campos. Observa, en la línea 97, que estamos pidiendo el campo price y el campo symbol, cada uno procedente de una tabla distinta. Como la función GetRegisterOfRequest no usa ningún método para ajustar las cosas y conseguir que se rellenen dos de los tres campos indicados en la estructura, la función DatabaseReadBind simplemente ignora cualquier intento de asignar datos a los campos de la estructura. Por lo tanto, el retorno es exactamente este que puedes ver en la animación.
Podemos mejorar esto, pero por ahora quedará así, ya que todavía hay otro caso que debemos ver. Este puede observarse en el siguiente fragmento.
105. //+------------------------------------------------------------------+ 106. void SELECT_Type_05(void) 107. { 108. struct st 109. { 110. double price; 111. }stLocal; 112. 113. Print("Executing type 5 data request..."); 114. if ((*SQL).ExecRequestOfData("SELECT tq.price FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = (SELECT ts.id FROM tb_Symbols WHERE ts.symbol = 'PETR4') AND tq.of_day = '2023-07-11';")) 115. { 116. Print("Request: OK..."); 117. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 118. Print(" Resq ", c, " ==> { ", stLocal.price, " } "); 119. } 120. Print("Finish type 5..."); 121. } 122. //+------------------------------------------------------------------+
Fragmento del código principal
Como resultado de la ejecución, tenemos exactamente lo que se ve en la siguiente animación.

Este es un caso un poco más extremo del uso de SQL, donde forzamos a SQL a hacer exactamente algo que posiblemente pensarías que habría que hacer fuera de SQL. Pero aquí simplemente estamos haciendo una consulta muy específica para obtener un registro concreto de la base de datos. Observa con mucha atención la línea 114. En ella tenemos el comando que SQL deberá ejecutar. Observa que estamos pidiendo la cotización del símbolo PETR4, y esto del día 11-07-2023. Claro que la fecha sigue el formato esperado por SQL. Pero creo que, a esta altura de la lectura de los artículos, ya has entendido cómo funciona SQL.
Si miras la base de datos, obtendrás el mismo resultado que se ve en el terminal para la consulta solicitada a SQL. Este tipo de situación exige que solo haya un único campo devuelto. Observa cómo se declara esto en la línea 108.
Pero aquí puede surgir una pregunta válida: ¿no podríamos simplemente usar la línea 110, prescindiendo de la construcción de la estructura que se ve en la línea 108? Por desgracia, no, mi querido lector. No de la forma en que se declaró la función GetRegisterOfRequest. Esto se debe a que, si intentas usar solo la línea 110, prescindiendo de la construcción de la estructura, en realidad no estarías llamando a la lectura hecha con la función DatabaseReadBind. Estarías usando algo diferente. Sin embargo, como programadores, podemos generar un código capaz de trabajar con este tipo de situación, así como de manejar la situación vista en el fragmento anterior.
De todos modos, puedes probar las consultas, o mejor dicho, los comandos en SQL que se vieron aquí, directamente en MetaEditor, para experimentar un poco más cómo crear una consulta orientada a obtener resultados como los de este último fragmento. Pero no olvides probar lo mismo también en el código final que se usará como aplicación de MetaTrader 5. El código completo, para que puedas probarlo, puede verse a continuación.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property description "Basic script for SQL database written in MQL5" 004. #property version "1.00" 005. #property script_show_inputs 006. //+------------------------------------------------------------------+ 007. #resource "\\Files\\Script 01.sql" as string SQL_Create 008. #resource "\\Files\\Script 02.sql" as string SQL_Insert 009. //+------------------------------------------------------------------+ 010. #include <Market Replay\SQL\C_DB_SQL.mqh> 011. //+------------------------------------------------------------------+ 012. enum eCall {eType_00, eType_01, eType_02, eType_03, eType_04, eType_05}; 013. //+------------------------------------------------------------------+ 014. input string user01 = "DataBase01"; //Database File Name 015. input ECall user02 = eType_01; //Search type 016. //+------------------------------------------------------------------+ 017. C_DB_SQL *SQL; 018. //+------------------------------------------------------------------+ 019. void SELECT_Type_01(void) 020. { 021. struct st 022. { 023. int id; 024. string symbol; 025. }stLocal; 026. 027. Print("Executing type 1 data request..."); 028. if ((*SQL).ExecRequestOfData("SELECT * FROM tb_Symbols")) 029. { 030. Print("Request: OK..."); 031. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 032. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 033. Print("Reload..."); 034. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 035. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 036. }; 037. Print("Finish type 1..."); 038. } 039. //+------------------------------------------------------------------+ 040. void SELECT_Type_02(void) 041. { 042. struct st 043. { 044. string of_day; 045. double price; 046. string symbol; 047. }stLocal; 048. 049. Print("Executing type 2 data request..."); 050. if ((*SQL).ExecRequestOfData("SELECT tq.of_day, tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 051. { 052. Print("Request: OK..."); 053. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 054. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 055. } 056. Print("Finish type 2..."); 057. } 058. //+------------------------------------------------------------------+ 059. void SELECT_Type_03(void) 060. { 061. struct st0 062. { 063. double price; 064. string symbol; 065. }stLocal; 066. struct st1 067. { 068. string tmp1, 069. tmp2, 070. date; 071. }std; 072. 073. Print("Executing type 3 data request..."); 074. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol, tq.of_day FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 075. { 076. Print("Request: OK..."); 077. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 078. Print(" Resq ", c, " ==> { ", stLocal.price, " } ", stLocal.symbol); 079. Print("Reload..."); 080. for (int c = 0; (*SQL).GetRegisterOfRequest(std); c++) 081. Print(" Resq ", c, " ==> < ", std.date, " > "); 082. 083. } 084. Print("Finish type 3..."); 085. } 086. //+------------------------------------------------------------------+ 087. void SELECT_Type_04(void) 088. { 089. struct st 090. { 091. string of_day; 092. double price; 093. string symbol; 094. }stLocal; 095. 096. Print("Executing type 4 data request..."); 097. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 098. { 099. Print("Request: OK..."); 100. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 101. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 102. } 103. Print("Finish type 4..."); 104. } 105. //+------------------------------------------------------------------+ 106. void SELECT_Type_05(void) 107. { 108. struct st 109. { 110. double price; 111. }stLocal; 112. 113. Print("Executing type 5 data request..."); 114. if ((*SQL).ExecRequestOfData("SELECT tq.price FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = (SELECT ts.id FROM tb_Symbols WHERE ts.symbol = 'PETR4') AND tq.of_day = '2023-07-11';")) 115. { 116. Print("Request: OK..."); 117. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 118. Print(" Resq ", c, " ==> { ", stLocal.price, " } "); 119. } 120. Print("Finish type 5..."); 121. } 122. //+------------------------------------------------------------------+ 123. const string ExecScripts(void) 124. { 125. string szMsg = (*SQL).ExecResourceSQL(SQL_Create); 126. if (szMsg != NULL) return szMsg; 127. return (*SQL).ExecResourceSQL(SQL_Insert); 128. }; 129. //+------------------------------------------------------------------+ 130. void OnStart() 131. { 132. string szMsg = NULL; 133. 134. SQL = new C_DB_SQL(user01); 135. 136. if (user02 == eType_00) szMsg = ExecScripts(); 137. Print(szMsg == NULL ? "Result of executing the SQL script: Success" : szMsg); 138. 139. if (szMsg == NULL) switch (user02) 140. { 141. case eType_01: SELECT_Type_01(); break; 142. case eType_02: SELECT_Type_02(); break; 143. case eType_03: SELECT_Type_03(); break; 144. case eType_04: SELECT_Type_04(); break; 145. case eType_05: SELECT_Type_05(); break; 146. } 147. 148. delete SQL; 149. } 150. //+------------------------------------------------------------------+
Código principal en MQL5
Observa que todo lo explicado está en el código, siguiendo la misma numeración de las líneas. Lo único un poco diferente del código que vimos en artículos anteriores es precisamente la línea 139, donde, a partir de la variable declarada en la línea 15, seleccionamos cuál será el tipo o, mejor dicho, qué fragmento se ejecutará. Así verás lo mismo que en cada una de las animaciones.
Consideraciones finales
En este artículo mostré cómo pueden leerse los datos devueltos por un comando SQL cuando usamos SQLite de MetaTrader 5. Aunque, como puedes haber notado durante la lectura del artículo, todavía podemos mejorar más nuestra clase C_DB_SQL, siempre centrados en trabajar con el mismo SQLite de MetaTrader 5. Las principales mejoras que podemos hacer son:
- Permitir que, cuando hagamos una consulta con un número menor de columnas, puedan rellenarse los campos definidos en la estructura del código MQL5. Esto evitaría el desagradable resultado de que no se devuelva ninguna información en la llamada GetRegisterOfRequest.
- Otra mejora sería que, en caso de una consulta cuyo resultado sea un único registro de interés, como ocurrió en el fragmento SELECT_Type_05, no tengamos que declarar la variable de retorno dentro de una estructura y podamos usarla directamente como una variable estándar e independiente.
Aunque estas mejoras son interesantes de implementar, todavía no sé con certeza si mostraré cómo hacerlo. Porque, durante la programación de la aplicación en MQL5, podemos hacer las cosas de una forma un poco más criteriosa. De todos modos, si se decide mostrar cómo hacer esas modificaciones para dar un mejor soporte a los casos indicados en los fragmentos, eso se hará en el próximo artículo. Pero, independientemente de ello, no dejes de ver el próximo artículo de esta serie, porque el tema se vuelve más interesante con cada día que pasa.
| Archivo | Descripción |
|---|---|
| Experts\Expert Advisor.mq5 | Demuestra la interacción entre Chart Trade y el Asesor Experto (es necesario el Mouse Study para la interacción) |
| Indicators\Chart Trade.mq5 | Crea la ventana para configurar la orden a ser enviada (es necesario el Mouse Study para la interacción) |
| Indicators\Market Replay.mq5 | Crea los controles para la interacción con el servicio de repetición/simulador (es necesario el Mouse Study para la interacción) |
| Indicators\Mouse Study.mq5 | Permite la interacción entre los controles gráficos y el usuario (necesario tanto para operar el sistema de repetición/simulador como en el mercado real) |
| Services\Market Replay.mq5 | Crea y mantiene el servicio de repetición y simulación de mercado (archivo principal de todo el sistema) |
| Code VS C++\Servidor.cpp | Crea y mantiene un socket servidor desarrollado en C++ (versión MiniChat). |
| Code in Python\Server.py | Crea y mantiene un socket en Python para la comunicación entre MetaTrader 5 e Excel |
| Indicators\Mini Chat.mq5 | Permite implementar un minichat mediante un indicador (requiere el uso de un servidor para funcionar) |
| Experts\Mini Chat.mq5 | Permite implementar un minichat mediante un Asesor Experto (requiere el uso de un servidor para funcionar) |
| Scripts\SQLite.mq5 | Demuestra el uso de un script SQL mediante MQL5 |
| Files\Script 01.sql | Demuestra la creación de una tabla simple, con clave foránea |
| Files\Script 02.sql | Demuestra la adición de valores en una tabla |
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/13117
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Utilizando redes neuronales en MetaTrader
Del básico al intermedio: Eventos en Objetos (III)
Particularidades del trabajo con números del tipo double en MQL4
Herramientas personalizadas de depuración y perfilado para el desarrollo en MQL5 (Parte I): Registro avanzado
- 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