整合基于MQL的EA交易和数据库 (SQL Server, .NET 和 C#)

Сергей Ткаченко | 13 九月, 2018

简介. 基于 MQL 的 EA 和数据库

在论坛上经常会有关于在使用 MQL5 编写的 EA 交易中整合数据库有关的问题。对这个主题有兴趣并不令人惊讶,数据库是保存数据的一种非常好的方法,与终端记录不同,数据不会从数据库中消失,它们很容易排序和过滤,只选择所需要的部分。数据库可以用于向 EA 传递需要的信息 - 例如,某些命令,最重要的是 — 取得的数据可以从不同角度进行分析和进行统计学处理。例如,写一行查询代码就可以得到每个货币对在指定时间段的平均和总利润。现在让我们想象一下,在交易终端中人工计算这个需要花费多长时间,

不幸的是,MetaTrader 没有提供内建的与数据库服务器交互的工具,这个问题只能通过从 DLL 文件中引入函数来解决。任务并不简单,但是可行。

我已经这样做了很多次,并且决定在这篇文章中分享我的经验。这是一个例子,关于如何使 MQL5 EA交易与 Microsoft SQL Server 数据库服务器进行交互。为了创建 DLL 文件来为 EA 交易引入操作数据库的函数,我们使用了 Microsoft.NET 平台和 C# 语言。本文描述了创建和准备 DLL 文件的过程,以及在一个使用 MQL5 编写的 EA 交易中引入它的函数。EA 的代码只是作为例子提供,它非常简单。如需能使用 MQL4 来进行编译,还需要一些小的改动。

准备工作

我们需要以下的准备工作:

  1. 一个安装好的 MetaTrader 5 终端,并且有一个活动的交易账户。您不仅可以使用模拟账户,也可以使用真实帐户 — 使用测试 EA 不会带来风险。
  2. 安装好的 Microsoft SQL Server 数据库服务器实例。您可以使用另一台电脑上的数据库,通过网络连接它。免费的 Express 版本可以从 Microsoft 网站下载,它的局限对绝大多数用户都很小。您可以在这里下载: https://www.microsoft.com/en-us/sql-server/sql-server-editions-express. Microsoft 有的时候会改变它们网站上的链接,所以如果这个直接的链接无效的话,可以在任何搜索引擎中简单输入类似 "SQL Server Express download" 的短语来搜索。如果您是第一次安装 SQL Server ,可能会在安装中遇到一些问题。特别是在老的操作系统版本上,它可能需要安装另外的组件 (特别是 PowerShell 和 .NET 4.5),另外,SQL Server 和 VS C++ 2017 有时候可能会有冲突,安装包会要求恢复 С++. 您可以通过 "Control Panel(控制面板)", "Programs(程序)", "Programs and Features(程序和功能)", "VS C++ 2017", "Change(修改)", "Repair(修复)" 功能来进行修复。这些问题不是总会出现,并且可以简单地解决。
  3. 使用 .NET 和 C#. 的集成开发环境我个人使用的是 Microsoft Visual Studio (它也有免费版本), 所以,给出的例子也是使用它做开发的。您也可以使用不同的开发环境甚至其它编程语言,但是那样您必须考虑在您的环境中使用选定的语言来实现给出的实例。
  4. 用于从使用 .NET 开发的DLL文件中输出函数到非托管代码的工具基于 MQL 的 EA 不能操作托管的 .NET 代码,所以,得到的 DLL 文件必须做特别准备,提供输出函数的功能。网上有实现这种功能的各种方法,我使用的是由 Robert Giesecke 创建的 "UnmanagedExports" 开发包。如果您使用的是 Microsoft Visual Studio 2012 或者更高版本,您可以直接从 IDE 菜单中把它直接加到项目中。这种方法我们将会晚些再讨论。

除了安装所需的程序,还需要进行其它的准备工作。因为一些原因,如果非 Unicode 程序在您的计算机语言设置部分选择了 "俄语(俄罗斯)","UnmanagedExports" 无法正常工作。其它语言也有类似问题,除非选择的语言是"英语(美国)"。要安装它的话,要打开控制面板,转到 "地区和语言选项" 页面, 然后转到 "进阶" 页面,在 " 非 Unicode 程序所使用的语言"页面, 点击 "修改系统地区...". 如果那里设置的是 "英语 (美国)", 那就不会有问题,如果是其它一些内容,要把它改为 "英语 (美国)" 并重新启动电脑。

如果不这样做,在编译项目的执行“UnmanagedExports”脚本的阶段,会在".il"文件中出现语法错误,它们无法修正。即使您的项目非常简单,而且在 C# 代码中确定没有错误,错误还是会在 ".il" 文件中出现, 并且您将无法从项目中为非托管代码输出函数。

这只是针对于64位应用程序,32位的应用程序可以通过其它方法解决,不需要改变系统语言。例如,您可以使用 DllExporter.exe 程序,它可以在这里下载: https://www.codeproject.com/Articles/37675/Simple-Method-of-DLL-Export-without-C-CLI.

改变系统语言将会使一些应用程序无法工作,不幸的是,这些不便将不得不调和,但不会持续太久。修改语言只是在您编译项目的时候需要,在编译成功之后,系统语言可以再切换回去。

创建一个 DLL 文件

打开 Visual Studio 并创建一个新的 Visual C# 项目, 选择 "Class Library" 作为类型,让我们把它命名为 MqlSqlDemo。在项目属性中, 在 "Build" 部分,需要配置 "Platform target (目标平台)",在那里,需要把 "Any CPU" 改为 "x64" (在 Debug 版的配置和 Release 版配置中都需要)。这是因为要输出非托管代码的函数时 - 必须要指定处理器的类型。

安装 .NET framework 版本 4.5. 它通常是被默认选择的。

当创建项目的时候,会在项目中自动添加包含着 "Class1" 类的 "Class1.cs" 文件,把文件和类都分别重命名为 "MqlSqlDemo.cs" 和 "MqlSqlDemo"。用于从 DLL 文件中输出的函数只能是 static (静态) 类型的 — 这也是输出到非托管代码所要求的。

严格地说,您也可以输出非静态函数,但是这样做的话,还需要使用 C++/CLI 工具,本文中就不再探讨这个方面了。

因为我们类中所有的函数都必须是静态的,把类本身设为静态也就符合逻辑了,在这种情况下,如果有些函数漏掉了 "static" 转义符,在编译项目的时候就会被立即发现。我们得到了下面的类定义:

public static class MqlSqlDemo
{
    // ...
}

现在需要的是配置项目的依赖关系 ( "Solution Explorer(方案浏览器)" 中的 "Reference(参考)"部分). 把多余的选项都去掉,只留下 "System" 和 "System.Data",

现在加上 "UnmanagedExports" 包。

包的描述在作者的网站上: https://sites.google.com/site/robertgiesecke/Home/uploads/unmanagedexports.

加上它的最方便的方法就是使用 NuGet 包管理器,增加包的指导可以在 NuGet 网站上找到: https://www.nuget.org/packages/UnmanagedExports

我们的例子中只需要其中一个指导:

Install-Package UnmanagedExports -Version 1.2.7

在 Visual Studio 菜单中, 选择 "工具" 部分, 然后选 "NuGet Package Manager", 再选 "Package Manager Console",就会在底部出现命令行,把复制的 "Install-Package UnmanagedExports -Version 1.2.7" 指令在那里插入,再按 "Enter" 键,包管理器将会连接互联网再下载开发包,然后把它加到项目中并打印出下面的内容:

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> 

这就表示开发包已经成功添加,

然后,我们就可以继续直接在类定义文件 MqlSqlDemo.cs 中写代码了。

配置使用的 namespaces (命名空间).

结果就是这样的:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;

加上所需变量。静态类中的变量也只能是静态的,我们需要用于操作数据库的对象 — 连接对象(connection object) 和命令对象 (command object):

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

我们还需要一个字符串,用来发送函数执行结果的详细信息:

private static string sMessage = string.Empty;

使用数值 0 和 1 声明两个常量 — 它们将用于大多数函数的返回值,如果函数成功运行,它将返回 0, 否则的话就返回 — 1,这将会使代码更加容易理解。

public const int iResSuccess = 0;
public const int iResError = 1;

现在,转到函数部分。

对于输出在 MQL5 中使用的函数,有一些限制,

  1. 前面已经提到,函数应当是静态的。
  2. 不能使用模板集合类 ( System.Collections.Generic namespace),包含它们的代码将能够正常编译,但是在运行的时候可能出现无法解释的错误。这些类可以用在其它不会被引入的函数中,但是最好完全不使用它们。您可以使用常规的数组。我们的项目只是为了提供信息的目的,所以将不会包含这些类(也不会包含数组)。

演示项目将只使用简单的数据类型来工作 — 数字或者字符串。在理论上,还可以传输 Boolean(布林类型) 的数值,它们内部也是用整数来表示的。但是这些数字的数值在不同的系统中可能会有不同解释 (MQL 和 .NET),这可能会引发错误。所以,我们严格要求自己只使用三种类型的数据 — int, stringdouble,如有必要,Boolean 数值将以 int 来传递。

在实际项目中,可能会传递复杂的数据结构,但是您可以在操作 SQL Server 的时候不使用它们。

为了操作数据库,我们首先需要建立一个连接,这是通过 CreateConnection 函数来完成的。这个函数只使用一个参数 — 一个含有连接 SQL Server 数据库参数的字符串。它将会返回一个整数, 显示连接是否成功。如果连接成功, 将会返回 iResSuccess,也就是 0. 如果失败 — iResError, 也就是 1. 更加详细的信息将会放到消息字符串中 — sMessage.

这里是结果:

[DllExport("CreateConnection", CallingConvention = CallingConvention.StdCall)]
    public static int CreateConnection(
            [MarshalAs(UnmanagedType.LPWStr)] string sConnStr)
    {
        // 清空消息字符串:
        sMessage = string.Empty;
        // 如果已经有了一个连接 - 把它关闭并且把
        // 连接字符串改成新的,如果不是, -
        // 重新创建连接和命令对象:
        if (conn != null)
        {
                conn.Close();
                conn.ConnectionString = sConnStr;
        }
        else
        {
                conn = new SqlConnection(sConnStr);
                com = new SqlCommand();
                com.Connection = conn;
        }
        // 尝试打开连接:
        try
        {
                conn.Open();
        }
        catch (Exception ex)
        {
                // 因为一些原因,连接未能打开.
                // 把错误信息写到消息字符串中:
                sMessage = ex.Message;
                // 释放资源并重置对象:
                com.Dispose();
                conn.Dispose();
                conn = null;
                com = null;
                // 错误:
                return iResError;
        }
        // 一切工作正常,连接已经打开:
        return iResSuccess;
}

每个将要输出的函数在函数定义的前面都使用 DllExport 属性进行标记,它位于 RGiesecke.DllExport 命名空间, 是从 RGiesecke.DllExport.Metadata assembly 中引入的。当 NeGet 管理器安装 UnmanagedExports 开发包时,assembly 会自动加到项目中。要给这个属性传递两个参数:

  • 将要被输出的函数的名称。这个名字将会由外部程序 (包括 MetaTrader 5) 从 DLL 中调用的时候使用。输出函数的名称可以和代码中的函数名一样 — CreateConnection;
  • 第二个参数指出将会使用哪种函数调用机制,CallingConvention.StdCall 适用于我们所有的函数,

请注意 [MarshalAs(UnmanagedType.LPWStr)] 属性,它位于连接字符串参数 ConnStringIn 之前, 从函数中取得。这个属性显示的是字符串应当如何发送。在写这篇文章的时候,MetaTrader 5 和 MetaTrader 4 操作的是 Unicode 字符串 — UnmanagedType.LPWStr,

在调用函数的时候,描述前一次尝试连接错误的文字可能还保留在消息字符串中,所以在函数的开始要清除字符串。另外,这个函数也可能会在前面的连接还没有关闭的时候调用,所以,首先要检查连接和命令对象是否已经存在,如果它们存在,连接应当被关闭,而对象可以重用。如果没有,就需要创建新的对象。

Open 是用于连接的,不返回任何结果,所以,查看连接是否成功的仅有方法就是通过捕捉异常,如果有错误出现,释放资源,把对象清零,在把信息写进消息字符串并返回 iResError,如果一切正常 — 返回 iResSuccess。

如果打开连接失败,如需找到失败的原因,就需要读取包含在 sMessage 字符串中的消息,为此,加上了 GetLastMessage 函数,它将返回包含消息的字符串:

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

与建立连接的函数一样,这个函数也是使用了 DllExport 输出属性做了标记,[return: MarshalAs(UnmanagedType.LPWStr)] 属性指出了返回结果的传递方式。因为结果是一个字符串,它也应当以 Unicode 的形式传给 MetaTrader 5,所以, 这里也是使用的UnmanagedType.LPWStr

当连接打开后,您就能够开始操作数据库了,让我们加上在数据库中执行查询的功能,这是通过 ExecuteSql 函数来完成的:

[DllExport("ExecuteSql", CallingConvention = CallingConvention.StdCall)]
public static int ExecuteSql(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // 清空消息字符串:
        sMessage = string.Empty;
        // 首先,检查连接是否已经建立.
        if (conn == null)
        {
                // 连接还没有打开,
                // 报告错误并返回错误标志:
                sMessage = "连接对象为空,首先要调用 CreateConnection.";
                return iResError;
        }
        // 连接已经可用,尝试执行命令.
        try
        {
                com.CommandText = sSql;
                com.ExecuteNonQuery();
        }
        catch (Exception ex)
        {
                // 执行命令时出错.
                // 把错误信息写到消息字符串中:
                sMessage = ex.Message;
                // 返回错误标志:
                return iResError;
        }
        // 一切执行正常 - 返回成功执行的标志:
        return iResSuccess;
}

查询的文字是通过参数传给函数的,在执行查询之前,首先要检查连接是否已经打开。与打开连接的函数类似,这个函数在成功的时候返回 iResSuccess,如果失败就返回 iResError。为了取得导致错误的详细信息,需要使用 GetLastMessage 函数。ExecuteSql 函数可以用于执行任何查询 — 包括写入、删除或者修改数据。也可以使用数据库结构来进行工作。不幸的是,它不允许读取数据 - 这个函数不返回结果并且不保存从任何地方读取的数据。查询会被执行,但是您不能看到读取到什么。所以,我们又另外加上两个函数来读取数据。

第一个设计用于从数据库表中读取一个整数。

[DllExport("ReadInt", CallingConvention = CallingConvention.StdCall)]
public static int ReadInt(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // 清空消息字符串:
        sMessage = string.Empty;
        // 首先,检查连接是否已经建立.
        if (conn == null)
        {
                // 连接还没有打开,
                // 报告错误并返回错误标志:
                sMessage = "连接对象为空,首先要调用 CreateConnection.";
                return iResError;
        }
        // 用于取得返回结果的变量
        int iResult = 0;
        // 连接已经可用,尝试执行命令.
        try
        {
                com.CommandText = sSql;
                iResult = (int)com.ExecuteScalar();
        }
        catch (Exception ex)
        {
                // 执行命令时出错.
                // 把错误信息写到消息字符串中:
                sMessage = ex.Message;
        }
        // 返回取得的结果:
        return iResult;
}

数据的读取要比命令的简单执行更难实现,这个功能通过使用 SqlCommand 类的 ExecuteScalar 函数可以大幅度简化,它返回查询中第一行第一列的值。所以,通过参数传入的 SQL 查询应该用这种方式构建,它返回一系列数据行,而第一列包含一个整数。另外,这个函数应当返回它所读取数据的数量。所以,它的结果将不再是执行成功的信息。为了了解查询是否成功以及读取数据,在任何情况下都需要通过调用 GetLastMessage 来分析结果。如果最后的消息为空,则没有错误,数据可以读取。如果在那里写入了一些内容,就表示有错误发生,而数据不能读取。

第二个函数也能从数据库里读取一个值,不过是另一种类型 — 不是一个整数,而是一个字符串。字符串可以像数字一样读取;差别只是返回结果的类型不同。因为函数返回一个字符串,它应当使用 [return: MarshalAs(UnmanagedType.LPWStr)] 属性来标记。这里是函数的代码:

[DllExport("ReadString", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
public static string ReadString(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // 清空消息字符串:
        sMessage = string.Empty;
        // 首先,检查连接是否已经建立.
        if (conn == null)
        {
                // 连接还没有打开,
                // 报告错误并返回错误标志:
                sMessage = "连接对象为空,首先要调用 CreateConnection.";
                return string.Empty;
        }
        // 用于取得返回结果的变量
        string sResult = string.Empty;
        // 连接已经可用,尝试执行命令.
        try
        {
                com.CommandText = sSql;
                sResult = com.ExecuteScalar().ToString();
        }
        catch (Exception ex)
        {
                // 执行命令时出错.
                // 把错误信息写到消息字符串中:
                sMessage = ex.Message;
        }
        // 返回取得的结果:
        return sResult;
}

这样的数据读取功能对一个演示项目来说已经足够了,对于一个真实 EA,则可能会完全不够用 — 对于 EA 来说,把数据写到数据库中以作将来分析是更加重要的。如果还是需要读取数据,这些函数就可以使用- 它们非常满足这部分工作。但是,有时候您需要从表格中读取很多行,也包含着多个列,

这可以用两种方法处理。您可以从函数中返回复杂的数据结构 (这种方法对 MQL4 不适合). 我们可以在我们的类中声明一个 DataSet 类型的静态变量,当进行读取时,将需要把数据从数据库中载入到这个 DataSet 中, 然后再使用其它的函数来从中读取数据,或者一次函数调用读取一个单元。这种方法是在下面描述的 HerdOfRobots 项目中实现的,可以在项目代码中详细研究。为了缩小文章的篇幅,我们在这里不会探讨从多个行中读取数据。

在操作数据库完成之后,将需要关闭连接以及释放所使用的资源,这是通过 CloseConnection 函数来完成的:

[DllExport("CloseConnection", CallingConvention = CallingConvention.StdCall)]
public static void CloseConnection()
{
        // 首先,检查连接是否已经建立.
        if (conn == null)
                // 连接还没有打开 - 表示也不需要被关闭:
                return;
        // 连接已经打开 - 应当关闭它:
        com.Dispose();
        com = null;
        conn.Close();
        conn.Dispose();
        conn = null;
}

这个简单的函数不使用任何参数,也不返回结果。

所有所需的函数都具备了。编译项目.

因为函数不是在其它 .NET 应用程序中使用,而是在 MetaTrader (它不使用 .NET) 中使用, 编译将分为两步进行。第一步,一切都和所有的 .NET 项目一样做,会创建一个普通的版本,之后会使用 UnmanagedExports 开发包来处理它。在编译完成后,开发包开始工作,首先,会启动 IL(中间语言) 反编译器,它会分析构建成 IL 代码的结果,然后会修改 IL 代码 — 对 DllExport 引用的属性会被删除,并且加入指令来输出以这个属性标记的函数。所有,重新编译 IL 代码文件,覆盖最初的 DLL。

这些行为都是自动进行的。但是,就像我们之前提到的,如果在操作系统设置中选择了俄语作为非 Unicode 程序的语言,编译和修改 IL 代码很可能会使 UnmanagedExports 出错,这样就什么都不能做了。

如果在编译的时候没有出现错误信息,则一切正常,而获得的 DLL 就可以在 EA 中使用了。另外,当 UnmanagedExports 成功处理了 DLL 的时候, 它会新增两个扩展名为 ".exp" 和 ".lib" 的文件 (在本例中, 即 "MqlSqlDemo.exp" 和 "MqlSqlDemo.lib")。我们不会使用它们,但是它们的出现就表明了 UnmanagedExports 已经成功完成。

需要注意的是,演示项目有一个很大的限制: 它只允许在一个 MetaTrader 终端中只有一个 EA 来操作数据库,所有的 EA 都使用所载入 DLL 的同一个实例,因为我们的类是静态的,它在所有运行的 EA 中只有一个,变量也有同样的问题。如果您运行多个 EA,它们将全部使用同一个连接和相同的命令对象,如果多个 EA 尝试同时访问这些对象,就可能会产生问题。

但是这样的项目已经足以解释操作原则以及测试与数据库的连接了。现在我们已经有了包含函数可以工作的 DLL,我们可以继续使用 MQL5 来开发 EA 交易了。

使用 MQL5 创建 EA 交易

让我们使用 MQL5 创建一个简单的 EA,它的代码也可以在 MQL4 编辑器中编译,只要把它的扩展名从"mq5" 改为 "mq4"就可以了。这个 EA 交易只是用于演示成功操作数据库,所以它不会进行任何交易操作。

运行 MetaEditor, 点击 "新增" 按钮,选择 "Expert Advisor (template)" 并点击 "下一步",把名称指定为 "MqlSqlDemo",另外还加上一个参数 — 字符串类型的 "ConnectionString". 它将是连接字符串,用于指出如何连接到您的数据库服务器。例如,您可以为这个参数设置这样的初始值:

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

这个连接字符串可以连接到 MetaTrader 终端所运行的同一台电脑上安装的未命名的 ("默认实例") 数据库服务器,这里不需要指定登录名和密码 — 认证是使用 Windows 账户进行的。

如果您下载的是 SQL Server Express,并且不修改参数就安装在您的电脑上,那么您的 SQL Server 将是一个“named instance(有名称的实例)”,它会使用名称 "SQLEXPRESS",它的连接字符串就不同了:

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

当在 EA 模板中加入一个字符串型参数时,字符串的大小是有限制的,较长的连接字符串 (例如,对于一个有名称的服务器 "SQLEXPRESS") 可能会容纳不下,不过这也不是个问题 — 这个参数在这个阶段可以设为空,晚些时候可以在编辑 EA 代码的时候把它改为任何值,也可以在运行 EA 的时候设置所需的连接字符串。

点击 "下一步". 不需要加入更多的函数,所以把下个屏幕中所有的复选框都设为不选,再次点击 "下一步",就得到了 EA 的初始代码。

EA 的目的只是演示连接数据库并操作它,为此,只要使用初始化函数就够了 — OnInit. 至于其它函数 — OnDeinit 和 OnTick — 可以马上删除的,

结果,我们就得到了下面的内容:

//+------------------------------------------------------------------+ //|                                                   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 string   ConnectionString = "Server=localhost\\SQLEXPRESS;Database=master;Integrated Security=True"; //+------------------------------------------------------------------+ //| EA交易初始化函数                            | //+------------------------------------------------------------------+ int OnInit()   { //---    //---    return(INIT_SUCCEEDED);   }

请注意:当连接有名称的实例时 (在本例中,是 "SQLEXPRESS"),需要把 "\" 字符写两次: "localhost\\SQLEXPRESS". 在 EA 模板中增加参数的时候和在代码中修改参数的时候都是需要的。如果这个字符只输入了一次,编译器会把它当成字符串中的转意序列 (特殊字符) "\S",而在编译过程中报告它无法识别。

但是,当把编译过的 EA 附加到图表的时候,这个参数只有一个 "\" 字符,尽管在代码中是有两个这个字符。这是因为在编译的时候,字符串中所有的转移序列都已经转换过了。序列 "\\" 就被转换成一个 "\" 字符, 而用户 (不需要操作代码的人) 就看到了一个正常的字符串。所以,如果您不是在代码中指定连接字符串,而是在启动 EA 的时候指定,在连接字符串中应该只指定一个 "\" 字符。

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

现在让我们为 EA 草稿加上功能,首先,需要引入从创建的 DLL 中用于操作数据库的函数,在 OnInit 函数之前加上 import 部分。引入的函数在它们在 C# 代码中声明的方式基本相同,只需要删除所有的修饰词和属性:

// 引入函数的描述.
#import "MqlSqlDemo.dll"

// 用于打开连接的函数:
int CreateConnection(string sConnStr);
// 用于读取最后信息的函数:
string GetLastMessage();
// 用于执行 SQL 命令的函数:
int ExecuteSql(string sSql);
// 用于读取整数的函数:
int ReadInt(string sSql);
// 用于读取字符串的函数:
string ReadString(string sSql);
// 用于关闭连接的函数:
void CloseConnection();

// import 结束:
#import

为了使代码更清晰,声明函数执行结果的常数,和在 DLL 中一样,用0表示成功而1表示出错:

// 函数执行成功:
#define iResSuccess  0
// 执行函数时出错:
#define iResError 1

现在我们可以在 OnInit 初始化函数中加入操作数据库的函数调用了,它看起来是这样的:

int OnInit()
  {
   // 尝试打开一个连接:
   if (CreateConnection(ConnectionString) != iResSuccess)
   {
      // 建立连接失败.
      // 打印消息并退出:
      Print("打开连接时出错. ", GetLastMessage());
      return(INIT_FAILED);
   }
   Print("已经连接到数据.");
   // 连接已经成功建立.
   // 尝试执行查询.
   // 创建一个表并向其中写入数据:
   if (ExecuteSql(
      "create table DemoTest(DemoInt int, DemoString nvarchar(10));")
      == iResSuccess)
      Print("已经在数据库中创建了表.");
   else
      Print("创建表失败. ", GetLastMessage());
   if (ExecuteSql(
      "insert into DemoTest(DemoInt, DemoString) values(1, N'Test');")
      == iResSuccess)
      Print("数据已经写入表.");
   else
      Print("向表中写入数据失败. ", GetLastMessage());
   // 继续读取数据从数据库中读取一个整数:
   int iTestInt = ReadInt("select top 1 DemoInt from DemoTest;");
   string sMessage = GetLastMessage();
   if (StringLen(sMessage) == 0)
      Print("从数据库中读取了数字: ", iTestInt);
   else // 读取数字失败.
      Print("从数据库中读取数字失败. ", GetLastMessage());
   // 现在读取一个字符串:
   string sTestString = ReadString("select top 1 DemoString from DemoTest;");
   sMessage = GetLastMessage();
   if (StringLen(sMessage) == 0)
      Print("已经从数据库中读取了字符串: ", sTestString);
   else // 读取字符串失败.
      Print("从数据库中读取字符串失败. ", GetLastMessage());
   // 不再需要这个表了,可以删除它了.
   if (ExecuteSql("drop table DemoTest;") != iResSuccess)
      Print("删除表失败. ", GetLastMessage());
   // 工作已经完成 - 关闭连接:
   CloseConnection();
   // 初始化完成:
   return(INIT_SUCCEEDED);
  }

编译 EA 交易就这样,测试 EA 交易就准备好了,您可以运行它了。在运行 EA 之前,需要把 DLL 加到您使用的 MetaTrader 的库文件夹中,启动 MetaTrader, 在 "文件" 菜单中,选择 "打开数据文件夹",打开 "MQL5" 文件夹 (对于 MetaTrader 4, 是 "MQL4" 文件夹), 然后是 "Libraries" 文件夹,把创建好的 DLL 文件 (MqlSqlDemo.dll) 放到这个文件夹下。这一次 EA 应当编译好可以使用了,当然,要在 MetaTrader 5 设置中允许运行 EA 交易和从 DLL 中引入函数,否则在启动的时候就会立即失败。

启动 EA,把连接字符串的值设为您自己的数据库服务器访问参数,如果一切操作正确,EA 将会在记录中输出下面的内容:

2018.07.10 20:36:21.428    MqlSqlDemo (EURUSD,H1)    已经连接到数据库.
2018.07.10 20:36:22.187    MqlSqlDemo (EURUSD,H1)    在数据库中创建了表.
2018.07.10 20:36:22.427    MqlSqlDemo (EURUSD,H1)    数据写入表.
2018.07.10 20:36:22.569    MqlSqlDemo (EURUSD,H1)    从数据库中读取数字: 1
2018.07.10 20:36:22.586    MqlSqlDemo (EURUSD,H1)    从数据库中读取字符串: Test

连接到数据库,执行 SQL 命令,写入和读取数据 - 一切都成功执行了。

结论

一个完整的 Visual Studio 解决方案 — 包含了本文中所有所需文件的档案,附加在文章后,名称为 "MqlSqlDemo.zip","UnmanagedExports" 开发包已经安装,测试 EA MqlSqlDemo.mq5 以及它的 MQL4 版本位于 "MQL" 子文件夹中。

本文中描述的方法是完全可操作的,根据上面的原则,已经创建了应用程序,可以同时工作于成千上万的 EA 中,现在一切都进行了多次测试和运行。

本文中创建的 DLL 和 EA 都是用于教学和评估目的,当然,您可以在实际项目中使用这个 DLL,但是,很可能不能满足您的要求,因为它有很大的限制。如果您需要增加功能以在您自己的 EA 中操作数据库,您很可能需要更多的特性。如果是那样,您就需要以本文为例,增加您自己的代码。如果您有任何困难,可以在本文的留言处请求帮助,通过我的 Skype (cansee378) 联系我,或者通过我网站的联系页面: http://life-warrior.org/contact.

如果您没有时间或者只是需要浏览 C# 代码,您可以下载已经完成的项目。叫做 HerdOfRobots 的免费开源程序,就是使用本文中所述的相同原则实现的。安装包中包含了针对 MetaTrader 5 和 MetaTrader 4 的输出函数的完整文件, 还有完整的程序。这些开发库有更加强大的特性,例如,它们可以允许在一个终端中运行多至 63 个EA (可以连接到不同数据库), 从表中按行读取数据,向数据库中写入日期/时间值。

HerdOfRobots 程序提供了用于控制 EA 连接到数据库的方便的功能,以及分析它们所写入的数据。开发包中有一份手册,详细描述了工作的方方面面。程序安装包档案 — SetupHerdOfRobots.zip — 也已经附加在文章中。如果您想看到 MqlToSql64 项目 (MT4 的 MqlToSql) 中用于连接数据库的程序代码,以便晚些在您的项目中使用所述的高级功能,代码可以从开源库中免费下载:

https://bitbucket.org/CanSeeThePain/herdofrobots

https://bitbucket.org/CanSeeThePain/mqltosql64

https://bitbucket.org/CanSeeThePain/mqltosql