Integración de un experto en MQL y bases de datos (SQL Server, .NET y C#)

23 octubre 2018, 16:40
Сергей Ткаченко
0
711

Introducción. Expertos en MQL y bases de datos

En los foros nos encontramos periódicamente con usuarios que preguntan cómo integrar en los expertos escritos en MQL 5 el trabajo con bases de datos. El interés por este tema no es nada sorprendente. Las bases de datos son muy útiles como recursos a la hora de guardar datos. A diferencia de los logs del terminal, los datos de las bases no desaparecen en ninguna parte. Son fáciles de clasificar y filtrar, permitiendo elegir solo las necesarias. Con la ayuda de una base de datos podemos transmitir al experto la información necesaria, por ejemplo, un cierto comando. Y lo más importante, los datos recibidos pueden ser analizados desde distintos puntos de vista y procesados estadísticamente. Por ejemplo, para averiguar el promedio y los beneficios totales durante un tiempo específico para cada pareja de divisas, basta con escribir una solicitud en una línea, lo que ocupará literalmente un minuto. Ahora imagine cuánto tiempo le llevaría calcularlo manualmente usando la historia de la cuenta en el terminal comercial.

Por desgracia, no existen medios estándar para interactuar con los servidores de bases de datos en los terminales MetaTrader. El problema se resuelve con la importación de funciones desde archivos DLL. La traea es compleja, pero asumible.

No es la primera vez que el autor se ve en esa situación, así que en este artículo vamos a compartir juntos estas experiencias. Como ejemplo, describiremos la organización de la interacción entre los expertos en MQL5 y el servidor de base de datos Microsoft SQL Server. Para crear el archivo DLL desde el que los expertos importarán las funciones para trabajar con la base de datos, usaremos la plataforma Microsoft.NET y el lenguaje de programación C#. En el artículo, se describe con detalle la creación del proyecto y la preparación del archivo DLL correspondiente, así como la importación de funciones desde el mismo a un experto escrito en MQL5. El código mostrado como ejemplo es muy sencillo. Para compilarlo en MQL4, solo debemos introducir en él unos pequeños cambios.

Preparación para el trabajo

Para trabajar, vamos a necesitar lo siguiente:

  1. MetaTrader 5 instalado y una cuenta comercial activa. Podemos utilizar no solo una cuenta demo, sino también una real: usando un experto de prueba, usted no arriesgará su depósito.
  2. Ejemplar instalado del servidor de base de datos Microsoft SQL Server. Es posible usar el servidor de base de datos en cualquier computadora conectándose al mismo online. Podemos descargar del sitio de Microsoft e instalar una versión gratuita de Express Edition, para la mayoría de los usuarios sus limitaciones no serán significativas. Por ejemplo, podemos descargarla aquí: https://www.microsoft.com/es-es/sql-server/sql-server-editions-express.  La compañía Microsoft a veces cambia las direcciones de los enlaces en su sitio, por eso, si el enlace directo no funciona, solo tendrá que introducir en el buscador cualquier frase del estilo "descargar SQL Server Express". Si usted instala SQL Server por primera vez, podrían surgir ciertas complicaciones con la instalación. En concreto, en las versiones antiguas del SO, podría pedir instalar componentes adicionales (concretamente, PowerShell y .NET 4.5). Asimismo, a veces surge un conflicto entre el SQL Server y VS C++ 2017, en este caso, además, el instalador pedirá restaurar el С++. Esto se puede hacer a través de "Panel de control", "Programas", "Programas y componentes", "VS C++ 2017", "Cambiar", "Recuperar". No siempre hay problemas, y normalmente estos se resuelven con facilidad.
  3. Entorno de desarrollo con uso de .NET y C#. Nosotros vamos a usar Microsoft Visual Studio (existe una versión gratuita para este), por eso los ejemplos van a ser precisamente para dicha versión. Podemos usar otro entorno de desarrollo e incluso otro lenguaje de programación. Pero entonces usted deberá pensar por sí mismo cómo implementar los ejemplos mostrados en su entorno y en el lenguaje que usted elija.
  4. Herramienta para exportar funciones de archivos DLL .NET a código no gestionado. Los expertos en MQL no saben trabajar con código gestionado .NET. Por eso, deberemos preparar de forma especial el archivo DLL obtenido, proporcionando la posibilidad de exportar funciones. En la Red se describen los métodos para hacer esto. Nosotros hemos usado el paquete "UnmanagedExports", creado por el programador Robert Giesecke. Si usted usa Microsoft Visual Studio versión 2012 o superior, entonces podrá añadirle el proyecto directamente desde el menú del entorno de desarrollo. A continuación, veremos cómo hacer esto.

Aparte de la instalación de los programas necesarios, debemos ejecutar otra operación preparatoria. Por una serie de motivos, el paquete "UnmanagedExports" no puede funcionar si en los ajustes de idioma de su computadora se ha elegido "Ruso (Rusia)" como idioma para los programas que no dan soporte a Unicode. También pueden surgir problemas con otros idiomas, si no se trata del "Inglés (EE.UU)". Para instalarlo, abra el panel de control. Allí encontrará la pestaña "Configuración regional e idiomas", de allí, pase a la pestaña "Avanzado". En la pestaña "Idioma de los programas sin soporte de Unicode" pulsamos en "Cambiar idioma del sistema...". Si está configurado el "Inglés (EE.UU.)", todo irá bien. Si hay algún otro, cámbielo a "Inglés (EE.UU.)" y reinicie la computadora.

Si no lo hacemos, al compilar el proyecto en la etapa de ejecución de los scripts "UnmanagedExports", veremos errores sintácticos en los archivos ".il". No será posible corregirlos. Incluso si su proyecto es muy sencillo y no hay errores en C#, en los archivos ".il" aparecerán errores de todas formas, y usted no podrá exportar las funciones desde el proyecto al código no gestionado.

Esto solo se refiere a las aplicaciones de 64 bits. En las de 32, es posible procesar con otros métodos que no requieren cambiar el idioma del sistema. Por ejemplo, resultará adecuado el programa DllExporter.exe, que se puede descargar en el enlace: https://www.codeproject.com/Articles/37675/Simple-Method-of-DLL-Export-without-C-CLI.

El cambio del idioma del sistema inutilizará algunas aplicaciones. Por desgracia, deberemos aceptar estas incomodidades, pero por poco tiempo. El cambio de idioma es necesario solo al compilar el proyecto. Después de realizar la compilación con éxito, podrá cambiar de nuevo el idioma. 

Creando el archivo DLL

Abrimos Visual Studio y creamos un nuevo proyecto Visual C#, eligiendo para su tipo "Class Library". Lo llamaremos MqlSqlDemo. En las propiedades del proyecto, en el apartado "Build", debemos necesariamente configurar la plataforma objetivo ("Platform target"). Ahí tenemos que cambiar "Any CPU" por "x64" (tanto en la configuración Debug, como en la configuración Release). Esto está relacionado con las peculiaridades de la exportación de funciones al código no gestionado, se les debe indicar explícitamente el tipo de procesador.

En la versión del framework .NET, pondremos 4.5. Normalmente, ya está elegida por defecto.

Al crear un proyecto en él inmediatamente, se añade de forma automática el archivo "Class1.cs", que contiene la clase "Class1". Renombramos también el archivo en "MqlSqlDemo.cs" y "MqlSqlDemo". Las funciones que se exportarán desde el archivo DLL pueden ser solo estáticas: esto se requerirá para la exportación al código no gestionado.

Hablando estrictamente, también podemos exportar funciones no estáticas. Pero para ello, deberemos recurrir a herramientas de C++/CLI, que no vamos a analizar en este artículo.

Puesto que todas las funciones en nuestra clase deberán ser obligatoriamente estáticas, tendrá sentido hacer estática la propia clase. En este caso, si para alguna función se ha omitido el modificador "static", esto se descubrirá de inmediato al compilar el proyecto. Obtenemos la siguiente descripción de la clase:

public static class MqlSqlDemo
{
    // ...
}
Ahora tenemos que configurar la dependencia del proyecto (apartado "References" en "Solution Explorer"). Quitamos todo lo sobrante, dejando solo "System" y "System.Data".

Ahora, añadimos el paquete "UnmanagedExports".

Podemos encontrar una descripción completa del paquete en el sitio web de su creador: https://sites.google.com/site/robertgiesecke/Home/uploads/unmanagedexports.

La forma más cómoda de añadirlo es a través del gestor de paquetes NuGet. Es posible encontrar las instrucciones sobre la adición en el sitio web NuGet: https://www.nuget.org/packages/UnmanagedExports

De estas instrucciones, solo necesitamos una:

Install-Package UnmanagedExports -Version 1.2.7

En el menú Visual Studio, elegimos el apartado "Tools", en dicho apartado, el punto "NuGet Package Manager", y a continuación, "Package Manager Console". Abajo se abrirá la línea de comandos. En ella, debemos pegar la instrucción copiada "Install-Package UnmanagedExports -Version 1.2.7" y pulsar la tecla "Enter". El gestor de paquetes tardará un tiempo en conectarse a internet y descargar el paquete; después lo añadirá al proyecto y mostrará lo siguiente:

PM> Install-Package UnmanagedExports -Version 1.2.7
Installing 'UnmanagedExports 1.2.7'.
Successfully installed 'UnmanagedExports 1.2.7'.
Adding 'UnmanagedExports 1.2.7' to MqlSqlDemo.
Successfully added 'UnmanagedExports 1.2.7' to MqlSqlDemo.

PM> 

Esto significa que el paquete se ha añadido con éxito.

Después, podemos proceder directamente a la escritura del código en el archivo de descripción de la clase MqlSqlDemo.cs. 

Configuramos el espacio de nombres utilizado.

  • Visual Studio añade demasiadas cosas. Quitamos todo del apartado "using", excepto "using System;".
  • Ahora añadimos "using System.Data;", de aquí se tomarán las clases para trabajar con las bases de datos.
  • Añadimos "using System.Data.SqlClient;": aquí se contienen las clases para trabajar concretamente con la base de datos SQL Server.
  • Añadimos "using System.Runtime.InteropServices;", aquí están los atributos para interactuar con el código no gestionado.
  •  Añadimos "using RGiesecke.DllExport;", de aquí tomaremos el atributo con el que se deben marcar las funciones exportadas.

Obtenemos el siguiente conjunto:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;

Añadimos las variables necesarias. Las variables en la clase estática también pueden ser estáticas solamente. Vamos a necesitar objetos para trabajar con la base de datos: un objeto de conexión y un objeto de comando. 

private static SqlConnection conn = null;
private static SqlCommand com = null;

También necesitaremos una línea con la que transmitir mensajes detallados sobre los resultados de la ejecución de las funciones:

private static string sMessage = string.Empty;

Declaramos dos constantes con los valores 0 y 1: estas servirán como valores retornados para la mayoría de funciones. Si la función se ejecuta con éxito, retornará 0, si se ejecuta erróneamente, 1. Esto hará el código más comprensible.

public const int iResSuccess = 0;
public const int iResError = 1;
Ahora vamos a pasar a las funciones.

Las funciones que se exportarán para su uso en MQL5, tienen limitaciones.

  1. Las funciones, como ya hemos dicho, deberán ser estáticas.
  2. No se pueden utilizar las clases de plantilla de colecciones (espacio de nombres System.Collections.Generic). Todo se compilará bien con ellas, pero en la etapa de ejecución pueden aparecer errores inexplicables. El uso de estas clases es posible en otras funciones que no se exportarán, pero lo mejor es arreglárselas sin ellas. Podemos utilizar matrices normales. Nuestro proyecto es demostrativo, por eso no habrá en él ninguna clase así (como tampoco habrá matrices).

En nuestro proyecto demo, solo transmitiremos datos sencillos: cifras o líneas. De forma igualmente teórica se podrían transmitir los valores del tipo boolean, que en su representación interna también son números enteros. Pero los valores de esos números se interpretan de forma diferente con diferentes sistemas (MQL y .NET). Esto provoca errores. Por eso, nos limitaremos a tres tipos de datos int, string y double. Los valores boolean, si surge la necesidad, deberemos transmitirlos como int.

En los proyectos reales podemos transmitir estructuras de datos complejas, pero para organizar el trabajo con SQL Server no necesitamos esto en absoluto.

Para trabajar con bases de datos, necesitaremos ante todo establecer una conexión. Para eso precisamente sirve la función CreateConnection. La función aplicará un parámetro: la línea con los parámetros de conexión a SQL Server. Retornará un número entero que nos mostrará si se ha logrado establecer la conexión. Si la conexión tiene éxito, retornaremos iResSuccess, es decir, 0. En caso de fallo, retornaremos iResError, es decir, 1. Introduciremos información más detallada sobre el error en la línea de mensaje sMessage.

Vamos a ver lo que sale:

[DllExport("CreateConnection", CallingConvention = CallingConvention.StdCall)]
public static int CreateConnection(
        [MarshalAs(UnmanagedType.LPWStr)] string sConnStr)
{
        // Limpiamos la línea del mensaje:
        sMessage = string.Empty;
        // Si ya tenemos conexión, la cerramos y cambiamos
        // la línea de conexión por una nueva, si no, 
        // creamos de nuevo los objetos de conexión y comando:
        if (conn != null)
        {
                conn.Close();
                conn.ConnectionString = sConnStr;
        }
        else
        {
                conn = new SqlConnection(sConnStr);
                com = new SqlCommand();
                com.Connection = conn;
        }
        // Intentamos abrir una conexión:
        try
        {
                conn.Open();
        }
        catch (Exception ex)
        {
                // Por algún motivo, la conexión no se ha abierto.
                // Introducimos la información sobre el error en la línea de mensajes:
                sMessage = ex.Message;
                // Liberamos los recursos y redefinimos los objetos:
                com.Dispose();
                conn.Dispose();
                conn = null;
                com = null;
                // Error:
                return iResError;
        }
        // Todo ha resultado bien, la conexión está abierta:
        return iResSuccess;
    }

Con el atributo DllExport se marca cada función exportada antes de la descripción. Se encuentra en el espacio de nombres RGiesecke.DllExport, importado a partir del build de RGiesecke.DllExport.Metadata. Dependiendo del proyecto, el build se añade de forma automática, cuando el gestor de paquetes NuGet instala el paquete UnmanagedExports. A este atributo debemos transmitir dos parámetros: 

  • el nombre de la función bajo el cual será exportada. Por este nombre lo llamarán desde la DLL los programas externos, incluido MetaTrader 5. Podemos crear el nombre de la función exportada igual que el nombre de la función en el código CreateConnection;
  • El segundo parámetro muestra qué mecanismo de llamada de funciones se usará. Para todas nuestras funciones, resultará adecuado CallingConvention.StdCall.

Vamos a prestar también atención al atributo [MarshalAs(UnmanagedType.LPWStr)]. Se encuentra antes del parámetro de línea de conexión ConnStringIn tomado por la función. Este atributo muestra cómo se debe transmitir la línea. En el momento en que escribimos el artículo, MetaTrader 5 y MetaTrader 4 funcionan con líneas en Unicode: UnmanagedType.LPWStr.

Cuando llamamos la función, en la línea de mesajes puede permanecer el texto que describe el error en el anterior intento de conexión, por eso la línea se borra al comienzo de la función. Asimismo, la función puede ser llamada cuando la conexión anterior no se ha cerrado aún. Por eso, comprobamos primero si hay objetos de conexión y comando. Si existen, podemos cerrar la conexión y usar los objetos de nuevo. Si no, tendremos que crear nuevos objetos.

El método Open usado para la conexión no retorna ningún resultado. Por eso, solo podemos saber si la conexión ha tenido éxito mediante la captura de excepciones. En caso de error, liberamos recursos, redefinimos los objetos, introducimos la información en la línea de mensajes y retornamos iResError. Si todo va bien, retornamos iResSuccess.

Si no podemos abrir la conexión, entonces el robot deberá leer el mensaje escrito en la línea sMessage para averiguar el motivo del error. Para ello, añadiremos la función GetLastMessage. Esta retornará una línea con el mensaje:

[DllExport("GetLastMessage", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
public static string GetLastMessage()
{
        return sMessage;
}

Al igual que la función de establecimiento de conexión, esta función también está marcada con el atributo de exportación DllExport. El atributo [return: MarshalAs(UnmanagedType.LPWStr)] muestra de qué forma debe transmitirse el resultado retornado. Puesto que el resultado es una línea, deberemos tansmitirlo a MetaTrader 5 también en Unicode. Por eso, aquí usaremos UnmanagedType.LPWStr.

Después de abrir la conexión, podemos comenzar a trabajar con la base de datos. Vamos a añadir la posibilidad de ejecutar solicitudes a la base de datos. De esto se ocupará la función ExecuteSql:

[DllExport("ExecuteSql", CallingConvention = CallingConvention.StdCall)]
public static int ExecuteSql(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // Limpiamos la línea del mensaje:
        sMessage = string.Empty;
        // Primero tenemos que comprobar si se ha establecido la conexión.
        if (conn == null)
        {
                // La conexión aún no ha sido abierta.
                // Comunicamos sobre el error y retornamos la bandera de error:
                sMessage = "Connection is null, call CreateConnection first.";
                return iResError;
        }
        // La conexión está preparada, intentamos ejecutar el comando.
        try
        {
                com.CommandText = sSql;
                com.ExecuteNonQuery();
        }
        catch (Exception ex)
        {
                // Error al ejecutar el comando.
                // Introducimos la información sobre el error en la línea de mensajes:
                sMessage = ex.Message;
                // Retornamos la bandera de error:
                return iResError;
        }
        // Todo ha salido bien, retornamos la bandera de ejecución exitosa:
        return iResSuccess;
}

El texto de la solicitud se transmite a la función con un parámetro. Antes de ejecutar la solicitud, comprobamos si se ha abierto la conexión. Como sucede con la función de apertura de conexión, en el caso de que la ejecución tenga éxito, la función retornará iResSuccess, en caso de error, retornará iResError. Para obtener información más detallada sobre los motivos del error, debemos usar la función GetLastMessage. Utilizando la función ExecuteSql, podemos ejecutar cualquier solicitud, escribir datos, eliminarlos o modificarlos. Podemos incluso trabajar con la estructura de la base de datos. Pero, por desgracia, esta no permite leer datos: la función no retorna el resultado y no guarda en ninguna parte los datos leídos. La solicitud se ejecutará, pero no lograremos ver lo que se ha leído. Por eso, vamos a añadir dos funciones para leer los datos.

La primera de ellas se ha pensado para leer un número entero en el recuadro de la base de datos.

[DllExport("ReadInt", CallingConvention = CallingConvention.StdCall)]
public static int ReadInt(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // Limpiamos la línea del mensaje:
        sMessage = string.Empty;
        // Primero tenemos que comprobar si se ha establecido la conexión.
        if (conn == null)
        {
                // La conexión aún no ha sido abierta.
                // Comunicamos sobre el error y retornamos la bandera de error:
                sMessage = "Connection is null, call CreateConnection first.";
                return iResError;
        }
        // Variable para obtener el resultado retornado:
        int iResult = 0;
        // La conexión está preparada, intentamos ejecutar el comando.
        try
        {
                com.CommandText = sSql;
                iResult = (int)com.ExecuteScalar();
        }
        catch (Exception ex)
        {
                // Error al ejecutar el comando.
                // Introducimos la información sobre el error en la línea de mensajes:
                sMessage = ex.Message;
        }
        // Retornamos el resultado obtenido:
        return iResult;
}

Implementar la lectura de datos es significativamente más complejo que la ejecución de comandos. Esta función se ha simplificado mucho y usa la función ExecuteScalar de la clase SqlCommand. Esta retorna el valor de la primera columna de la primera línea retornada por la solicitud. Por eso, la solicitud SQL transmitida por el parámetro debe formarse de tal manera que en el conjunto de datos retornado por él estén las líneas, mientras que el número entero se encuentre en la primara columna. Además, la función deberá retornar de alguna forma el número leído. Por eso, su resultado ya no será un mensaje sobre el éxito de la ejecución. Para comprender si se ha logrado ejecutar la solicitud y leer los datos, deberemos analizar en cualquier caso el último mensaje, llamando GetLastMessage. Si el último mensaje está vacío, esto significará que no ha habido errores, y que los datos han sido leídos. Si hay algo escrito, significará que ha sucedido un error y no se ha logrado leer los datos.

La segunda función también lee un valor de la base de datos, pero de otro tipo: no un número entero, sino una línea. Las líneas se pueden leer de la misma forma que los números, la diferencia estriba solo en el tipo del resultado retornado. Puesto que la función retorna una línea, deberemos marcarla con el atributo [return: MarshalAs(UnmanagedType.LPWStr)]. Aquí tenemos el código de esta función:

[DllExport("ReadString", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
public static string ReadString(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // Limpiamos la línea del mensaje:
        sMessage = string.Empty;
        // Primero tenemos que comprobar si se ha establecido la conexión.
        if (conn == null)
        {
                // La conexión aún no ha sido abierta.
                // Comunicamos sobre el error y retornamos la bandera de error:
                sMessage = "Connection is null, call CreateConnection first.";
                return string.Empty;
        }
        // Variable para obtener el resultado retornado:
        string sResult = string.Empty;
        // La conexión está preparada, intentamos ejecutar el comando.
        try
        {
                com.CommandText = sSql;
                sResult = com.ExecuteScalar().ToString();
        }
        catch (Exception ex)
        {
                // Error al ejecutar el comando.
                // Introducimos la información sobre el error en la línea de mensajes:
                sMessage = ex.Message;
        }
        // Retornamos el resultado obtenido:
        return sResult;
}

Para el proyecto demo, esa lectura de datos será suficiente. Para un experto real, puede que ni siquiera la necesitemos, ya que para los expertos es más importante escribir los datos en la base de datos para su posterior análisis. Si aun así necesitamos leer los datos, podemos usar tranquilamente estas funciones: son totalmente operativas. Sin embargo, a veces debemos leer muchas líneas de un recuadro que contiene varias columnas.

Para ello, podemos tomar dos caminos. Se pueden retornar de la función estructuras complejas de datos (esta opción no servirá para MQL4). O bien podemos declarar en nuestra clase una variable estática de la clase DataSet. Al leer, deberemos cargar los datos de la base de datos en esta DataSet, y después leer ya con otras funciones los datos desde ahí, una celda por cada llamada de la función. Este enfoque ha sido implementado en el proyecto HerdOfRobots, mencionado más abajo. Podemos analizarlo con detalle en el código del proyecto, pero, para no aumentar la longitud del artículo, no vamos a discutir la lectura de datos de varias líneas.

Después de finalizar el trabajo con la base de datos, deberemos cerrar la conexión, liberando los recursos utilizados. Para ello, se ha diseñado la función CloseConnection:

[DllExport("CloseConnection", CallingConvention = CallingConvention.StdCall)]
public static void CloseConnection()
{
        // Primero tenemos que comprobar si se ha establecido la conexión.
        if (conn == null)
                // La conexión aún no ha sido abierta, esto significa que no necesitamos cerrarla:
                return;
        // La conexión está abierta, hay que cerrarla:
        com.Dispose();
        com = null;
        conn.Close();
        conn.Dispose();
        conn = null;
}

Esta sencilla función no adopta ningún parámetro y no retorna ningún resultado.

Todas las funciones ya están preparadas. Compilamos el proyecto.

Puesto que se deberán utilizar las funciones no de otras aplicaciones .NET, sino de MetaTrader (que no usa .NET), la compilación tendrá lugar en dos etapas. En la primera etapa todo se hace igual que en los demás proyectos .NET. Se crea un build normal, y después se procesa con el paquete UnmanagedExports. El funcionamiento del paquete comienza tras la compilación del build. Primero se inicia el decompilador IL, que desmonta el build obtenido en código IL. A continuación, el código IL cambia, de ahí se eliminan los enlaces a los atributos de DllExport, y se añaden las instrucciones de exportación de las funciones marcadas con este atributo. Después de ello, el archivo con código IL se compila de nuevo y se escribe en lugar de la DLL original.

Todas estas acciones se ejecutan automáticamente. Pero, como hemos mencionado anteriormente, si hemos elegido en los ajustes del sistema operativo el idioma ruso para programas que no son compatibles con Unicode, al intentar compilar un archivo con un código IL modificado, el paquete UnmanagedExports generará un error y no podrá hacer nada.

Si al realizar la compilación no han aparecido mensajes de error, esto significará que todo ha resultado bien, y la DLL obtenida se podrá usar en los expertos. Además, si la DLL se procesa con éxito, UnmanagedExports añadirá dos archivos más con las extensiones ".exp" y ".lib" (en nuestro caso, se tratará de "MqlSqlDemo.exp" y "MqlSqlDemo.lib"). No las vamos a necesitar, pero por su presencia podemos valorar si el funcionamiento de UnmanagedExports ha finalizado con éxito.

Hay que destacar que el proyecto demo tiene una limitación muy importante: permite iniciar solo un experto que trabaje con bases de datos en un terminal MetaTrader. El asunto es que todos los expertos usan un ejemplar de la DLL descargada. Puesto que nuestra clase se ha hecho estática, será el mismo para todos los expertos iniciados. Las variables también serán generales. Si ejecutamos varios expertos, todos usarán la misma conexión y un objeto de comando para todos. Si varios expertos intentan contactar con estos objetos al mismo tiempo, podrían surgir problemas.

Pero será suficiente para explicar los principios de operación y probar la conexión con la base de datos de un proyecto así. Ahora ya tenemos un archivo DLL con funciones. Podemos proceder a escribir el experto en MQL5. 

Creando un experto en MQL5

Vamos a hacer un experto sencillo en MQL5. Su código se puede compilar en el editor de MQL4, si cambiamos su extensión de "mq5" a "mq4". El experto es necesario solo para demostrar que el trabajo con la base de datos ha tenido éxito, por eso no ejecutará ninguna transacción comercial.

Iniciamos el MetaEditor y pulsamos el botón "Crear". Dejamos el punto "Asesor (Plantilla)" y pulsamos "Continuar". Indicamos el nombre "MqlSqlDemo". Asimismo, añadimos un parámetro: "ConnectionString" del tipo "string". Esta será la línea de conexión, que indicará cómo conectarse a su servidor de base de datos. Podemos indicar su valor, por ejemplo, de esta forma:

Server=localhost;Database=master;Integrated Security=True

Esta línea de conexión permite conectarse al servidor de base datos denominado ("Default Instance"), instalado en la misma computadora en la que funciona MetaTrader. El login y la contraseña, en este caso, no son necesarios: se usa la autorización según la cuenta del SO de Windows.

Si usted ha descargado SQL Server Express y lo ha instalado en su computadora, al realizar la instalación sin cambiar los parámetros, su SQL Server será el "ejemplar nombrado". Tendrá el nombre "SQLEXPRESS". Su línea de conexión será otra:

Server=localhost\\SQLEXPRESS;Database=master;Integrated Security=True

Al añadir el parámetro de línea a la plantilla del asesor existe una limitación del tamaño de la línea. Una línea de conexión más larga (para el servidor nombrado "SQLEXPRESS") podría no caber. Pero no hay ningún problema a este respecto: podemos dejar vacío el valor del parámetro en esta etapa. Después, al editar el código del experto, podemos hacerlo de la forma que queramos. Asimismo, podemos indicar la línea de conexión necesaria al iniciar el experto.

Pulsamos "Continuar". No es necesario añadir ninguna función, por eso en la siguiente pantalla dejaremos todas las casillas de verificación sin marcar. Pulsamos una vez más "Continuar" y obtenemos el código inicial generado del experto.

Solo necesitamos el experto para mostrar la conexión a la base de datos y el trabajo con la misma. Para ello, basta con usar la función de inicialización OnInit. Los borradores de las demás funciones — OnDeinit y OnTick — podemos quitarlos de inmediato.

Como resultado, obtenemos lo siguiente:

//+------------------------------------------------------------------+ //|                                                   MqlSqlDemo.mq5 | //|                        Copyright 2018, MetaQuotes Software Corp. | //|                                             https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2018, MetaQuotes Software Corp." #property link      "https://www.mql5.com" #property version   "1.00" #property strict //--- input parameters input string   ConnectionString = "Server=localhost\\SQLEXPRESS;Database=master;Integrated Security=True"; //+------------------------------------------------------------------+ //| Expert initialization function                                   | //+------------------------------------------------------------------+ int OnInit()   { //---    //---    return(INIT_SUCCEEDED);   }

Preste atención: al conectarse al ejemplar nombrado (en nuestro caso, "SQLEXPRESS") tenemos que repetir el símbolo "\" dos veces: "localhost\\SQLEXPRESS". Esto también es necesario al añadir el parámetro en la plantilla del asesor y en el código. Si lo indicamos solo una vez, el compilador pensará que en la línea se ha indicado la secuencia Escape (símbolo especial) "\S", y al realizar la compilación, informará de que no ha sido reconocida.

Pero si trasladamos al gráfico un robot ya compilado, en sus parámetros solo habrá un símbolo "\", aunque en el código se hayan indicado dos. En realidad, al realizar la compilación, todas las secuencias Escape en las líneas se transforman en los símbolos correspondientes. La secuencia "\\" se transforma en un símbolo "\", y los usuarios (que no necesitan tratar con el código) ven ya una línea normal. Por eso, si usted indica la línea de conexión no en el código, sino al iniciar el asesor, en la línea de conexión se indicará solo un símbolo "\":

Server=localhost\SQLEXPRESS;Database=master;Integrated Security=True

Ahora vamos a añadir la funcionalidad al borrador del experto. Primero debemos importar de la DLL creada las funciones para trabajar con la base de datos. Añadimos el apartado de importación antes de la función OnInit. Las funciones importadas se describen de la misma forma que se han declarado en el código en C#, solo tenemos que quitar los modificadores y los atributos:

// Descripción de las funciones importadas.
#import "MqlSqlDemo.dll"

// Función de apertura de conexión:
int CreateConnection(string sConnStr);
// Función de lectura del último mensaje:
string GetLastMessage();
// Función de ejecución del comando SQL:
int ExecuteSql(string sSql);
// Función de lectura del número entero:
int ReadInt(string sSql);
// Función de lectura de la línea:
string ReadString(string sSql);
// Función de cierre de la conexión:
void CloseConnection();

// Finalizando la importación:
#import

Para que el código resulte más comprensible, declaramos las constantes de los resultados de ejecución de las funciones. Como en las DLL, será 0 si se ha ejecutado con éxito, y 1 si tenemos error:

// La función se ha ejecutado correctamente:
#define iResSuccess  0
// Error al ejecutar la función:
#define iResError 1

Ahora podemos añadir a la función de inicialización OnInit las llamadas de las funciones que van a trabajar con la base de datos. Este será su aspecto:

int OnInit()
  {
   // Tratamos de abrir la conexión:
   if (CreateConnection(ConnectionString) != iResSuccess)
   {
      // La conexión ha fallado.
      // Escribimos el mensaje y finalizamos el trabajo:
      Print("Error al abrir la conexión. ", GetLastMessage());
      return(INIT_FAILED);
   }
   Print("Conexión establecida con la base de datos.");
   // La conexión ha tenido éxito.
   // Tratamos de ejecutar una solicitud.
   // Creamos un recuadro y escribimos los datos en él:
   if (ExecuteSql(
      "create table DemoTest(DemoInt int, DemoString nvarchar(10));")
      == iResSuccess)
      Print("Recuadro creado en la base de datos.");
   else
      Print("No se ha podido crear el recuadro. ", GetLastMessage());
   if (ExecuteSql(
      "insert into DemoTest(DemoInt, DemoString) values(1, N'Test');")
      == iResSuccess)
      Print("Los datos se han escrito en el recuadro.");
   else
      Print("No se ha logrado escribir los datos en el recuadro. ", GetLastMessage());
   // Procedemos a la lectura de datos. Leemos un número entero de la base de datos:
   int iTestInt = ReadInt("select top 1 DemoInt from DemoTest;");
   string sMessage = GetLastMessage();
   if (StringLen(sMessage) == 0)
      Print("Número leído de la base de datos: ", iTestInt);
   else // No se ha logrado leer el número.
      Print("No se ha logrado leer el número de la base de datos. ", GetLastMessage());
   // Ahora leemos la línea:
   string sTestString = ReadString("select top 1 DemoString from DemoTest;");
   sMessage = GetLastMessage();
   if (StringLen(sMessage) == 0)
      Print("Línea leída de la base de datos: ", sTestString);
   else // No se ha logrado leer la línea.
      Print("No se ha logrado leer la línea de la base de datos. ", GetLastMessage());
   // Ya no necesitamos el recuadro: podemos eliminarlo.
   if (ExecuteSql("drop table DemoTest;") != iResSuccess)
      Print("No se ha logrado eliminar el recuadro. ", GetLastMessage());
   // Hemos finalizado el trabajo: cerramos la conexión:
   CloseConnection();
   // Finalizamos la inicialización:
   return(INIT_SUCCEEDED);
  }

Copiamos el experto. Ya está, hemos terminado el experto de prueba. Podemos iniciarlo. Antes de iniciarlo, debemos añadir una DLL a la carpeta de bibliotecas del perfil de MetaTrader que usamos. Iniciamos MetaTrader, y en el menú "archivo", elegimos "Abrir catálogo de datos". Abrimos la carpeta "MQL5" (en el caso de MetaTrader 4, se tratará de la carpeta "MQL4"), y en ella, abrimos la carpeta "Libraries". Colocamos allí el archivo de nuestra DLL: MqlSqlDemo.dll. En este momento, el experto ya debe estar compilado y accesible para su uso. Está claro que el inicio de los expertos y la importación de las funciones desde DLL deberán estar permitidos en los ajustes de MetaTrader 5, de lo contrario, el inicio del experto finalizará de inmediato con error.

Iniciamos el experto, cambiando en los parámetros los datos de la línea de conexión por los parámetros de acceso a nuestro servidor de base de datos. Si lo hemos hecho todo bien, el experto mostrará en el log las siguientes entradas:

2018.07.10 20:36:21.428    MqlSqlDemo (EURUSD,H1)    Conexión con la base de datos establecida.
2018.07.10 20:36:22.187    MqlSqlDemo (EURUSD,H1)    Creado recuadro en la base de datos.
2018.07.10 20:36:22.427    MqlSqlDemo (EURUSD,H1)    Datos escritos en el recuadro.
2018.07.10 20:36:22.569    MqlSqlDemo (EURUSD,H1)    Número leído desde la base de datos: 1
2018.07.10 20:36:22.586    MqlSqlDemo (EURUSD,H1)    Línea leída desde la base de datos: Test

 La conexión a la base de datos, la ejecución de los comandos SQL, la escritura y lectura de datos: todo se realiza con éxito.

Conclusión

La solución completa para Visual Studio, un fichero que contiene todos los archivos necesarios, se adjunta al artículo bajo el nombre "MqlSqlDemo.zip". El paquete "UnmanagedExports" ya está instalado. El experto de prueba MqlSqlDemo.mq5 y su variante para MQL4 también se encuentran en la carpeta incorporada "MQL".

El enfoque descrito en este artículo es completamente funcional. Con la ayuda de los principios explicados anteriormente, hemos creado aplicaciones que permiten trabajar con miles e incluso decenas de miles de expertos que se inician simultáneamente. Todo ha sido comprobado en múltiples ocasiones y funciona hasta ahora.

El archivo DLL y el experto creados en el marco de este artículo han sido diseñados solo como material de aprendizaje y lectura. Por supuesto que podemos usar DLL en proyectos reales. Pero, con toda probabilidad, dejará de resultarnos útil por sus numerosas limitaciones. Si usted quiere añadir a sus expertos la posibilidad de trabajar con bases de datos, seguramente requerirá de más capacidades. Entonces necesitará modificar el código por sí mismo, usando el artículo como guía. Si le surge cualquier dificultad, describa su problema en los comentarios al artículo, por Skype (mi login es cansee378) o a través de la página de contacto de mi sitio web: http://life-warrior.org/contact.

Si usted tiene tiempo y energías de analizar el código en C#, podrá descargar un proyecto ya preparado. El programa gratuito de código abierto HerdOfRobots ha sido desarrollado sobre los mismos principios que se han descrito en el artículo. En el paquete de instalación se incluyen con el programa archivos ya listos con funciones importadas para MetaTrader 5 y MetaTrader 4. Estas bibliotecas posee capacidades mucho más amplias. Por ejemplo, permiten iniciar hasta 63 expertos en un terminal, conectarlos a distintas bases de datos, leer los datos de los recuadros línea a línea o escribir en las bases de datos los valores de fecha/hora.

El propio programa HerdOfRobots ofrece cómodas posibilidades para el control de expertos que se conectan a la base de datos, así como para el análisis de los datos escritos por ellos. En el paquete se incluye una guía detallada que describe todos los aspectos del funcionamiento. El fichero con el archivo de instalación del programa — SetupHerdOfRobots.zip — también se adjunta al artículo. Si usted quiere echar un vistazo al código del programa usado para conectarse a la base de datos del proyecto MqlToSql64 (MqlToSql para MT4), con el objetivo de utilizar después las mencionadas capacidades avanzadas en sus propios proyectos, podrá descargar el código en los repositorios abiertos:

https://bitbucket.org/CanSeeThePain/herdofrobots

https://bitbucket.org/CanSeeThePain/mqltosql64

https://bitbucket.org/CanSeeThePain/mqltosql

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

Archivos adjuntos |
MqlSqlDemo.zip (565.98 KB)
SetupHerdOfRobots.zip (6741.3 KB)
Indicador universal RSI para operar simultáneamente en dos direcciones Indicador universal RSI para operar simultáneamente en dos direcciones

Al desarrollar algoritmos comerciales topamos con frecuencia con un problema: ¿cómo determinar dónde comienza y dónde termina la tendencia/flat? En este artículo, vamos a intentar crear un indicador universal en el que conjugaremos señales para distintos tipos de estrategia. También intentaremos simplificar la obtención de señales para las transacciones comerciales en el experto. Asimismo, mostraremos un ejemplo de combinación de varios indicadores diferentes en uno.

Neuroredes profundas (Parte VIII). Aumentando la calidad de la clasificación de los conjuntos bagging Neuroredes profundas (Parte VIII). Aumentando la calidad de la clasificación de los conjuntos bagging

En el artículo se analizan tres métodos con cuya ayuda podemos aumentar la calidad de clasificación de los conjuntos bagging y valorar su efectividad. Se ha evaluado cómo influye la optimización de los hiperparámetros de las redes neuronales ELM y los parámetros de post-procesado en la calidad de clasificación del conjunto.

Desarrollo de indicadores bursátiles con control de volumen tomando como ejemplo el indicador delta Desarrollo de indicadores bursátiles con control de volumen tomando como ejemplo el indicador delta

En el artículo se analiza el algoritmo de construcción de indcadores sobre volúmenes reales usando las funciones CopyTicks() y CopyTicksRange(). Asimismo, se muestran las peculiaridades de la construcción de estos indicadores y se describe su funcionamiento en tiempo real y en el simulador de estrategias.

Gráfico PairPlot basado en CGraphic para analizar correlaciones entre los arrays de datos (series temporales) Gráfico PairPlot basado en CGraphic para analizar correlaciones entre los arrays de datos (series temporales)

En el proceso del análisis técnico, a menudo a los traders se les plantea la tarea de comparar varias series temporales. La realización de este análisis requiere las herramientas correspondientes. En este artículo, yo propongo desarrollar una herramienta para el análisis gráfico y la búsqueda de las correlaciones entre dos y más series temporales.