如何从 MQL5 (MQL4) 访问 MySQL 数据库

Eugeniy Lugovoy | 24 十月, 2014

介绍

MQL 与数据库的交互问题并非新事物,但它们依然是相关的。利用数据库​​可以极大增强 MetaTrader 的可塑性: 存储并分析价格历史,从一个交易平台拷贝交易至另一个平台,提供实时报价/交易,在服务器端定期进行深度分析计算,使用 web 技术监视并远程控制账户。

总之,有许多种应用尝试从 MQL 和 MySQL 的组合之中获益,一些方案已经在代码库里出现。

例如 "MySQL 包装 - 用于 MetaTrader 4 的链接库" 就是这样的项目,许多程序员开始自己开发,在将来还可扩充。我认为,这种解决方案的缺点之一是分配特殊数组用来从数据库中读数据。

另一个项目 "MySQL 日志 1 - 用于 MetaTrader 4 的 EA" 更加专业,它不使用包装来访问标准链接库 libmysql.dll。因此,它不能在 MetaTrader4 编译版 600+ 上工作,由于 char 字符类型已经被 wchar_t 替代,且使用 int 类型替代了 TMYSQL 结构指针,导致在项目中产生内存泄漏 (内存分配不能控制/释放)。

另一个有趣的项目是 "EAX_Mysql - MySQL 链接库 - 用于 MetaTrader 5 的链接库"。它是十分出色的实现。不过作者列出了一些缺点,在使用时有强制限制。

任何人若需要在他们的项目中使用数据库,有两个选项: 要么开发自己的解决方案,并了解它的每一个部分,或者使用/改编任何第三方解决方案,了解如何使用它们并检测是否会阻碍他们的项目。

在我开发一个相当复杂的自动交易时,就要面对这样的必要性和两个选项。依照现有项目经过搜索,且研究了很多的解决方案后,我意识到,已发现的实施方案均无助于把我的自动交易提升到“专业水平”。

此外,也有些荒谬的方案,例如: 使用标准 libmysql.dll 执行 DML/DDL 操作 (插入/更新/删除数据, 在数据库中创建/废弃对象), 以及将数据检索 (SELECT) 的实现作为 HTTP 请求 (使用 inet.dll) 与 MySQL 服务器端的 web 服务器上的 PHP 脚本通信。而 SQL 查询被写在 PHP 脚本中。

换句话说,要运行该项目,一定需要保证下述所有部件准备妥当,配置好并运行:MySQL 服务器,Apache/ IIS Web 服务器,在服务器端的 PHP/ASP 脚本... 大量技术的组合。当然,在某些情况下,这是可以接受的,但当唯一的任务就是从数据库中查询数据 - 那么这些全无意义。此外,支持如此累赘的方案也耗费时间。

大部分的方案在插入数据,创建对象等等操作时没有问题。问题在于数据查询,因为数据将会被返回调用环境。

我认为出于此目的而使用数组是不切实际的和不方便的,简单的原因就是在主程序的开发/调试/支持过程中,数据库查询是可以变化的,而您也要正确控制为数组分配的内存.. 那么,这些可以,而且必须要避免。

下文讨论的 MQL <-> MySql 的接口基于 Oracle PL/SQL, MS SQL T-SQL, AdoDB 等产品内使用的典型方式- 使用游标。这个接口的开发目标是易于编程和维护,再加上最少元部件。它作为 DLL 包装器实现,连接标准链接库 libmysql.dll,且接口函数集合作为一个 .mqh 文件。

1. MQL <-> MySQL 接口

在 MetaTrader 终端之间交互 (通过 MQL 程序) 可以在如下元部件的帮助下实现:

MQL 和 MySQL 交互规划

1. 接口库 MQLMySQL.mqh. 使用 #include 语句将它加到项目工程里,并且可以按照您的喜好进行修改。

它包含的指令用于导入 MQLMySQL.dll 动态库的函数,以及调用它们和处理错误的函数。

2. MQLMySQL.dll 动态库。这是一个包装器,用来访问标准库 libmysql.dll 的功能。

此外,MQLMySQL.dll 链接库处理操作的结果并共享访问数据库的连接和游标。这意味着您可以在同一时间创建和使用多个连接 (来自一个或多个 MQL 程序), 保持少量的打开游标, 查询一个或多个数据库。互斥则用于分隔访问共享资源。

3. 标准动态链接库 libmysql.dll 是本地访问驱动器。您可以从任何 MySql 数据库的发布位置 C:\Windows\Sytem32 或 <终端>\MQL5\Libraries (对于 MetaTrader 4 在 <终端>\MQL4\Libraries) 中拷贝它。

事实上,它负责发送查询到数据库并接收检索结果。

让我们来详述要点,诸如: 打开/关闭连接, 执行 DML/DDL 查询和数据检索。

1.1. 打开和关闭连接

该 MySqlConnect 函数已经实现了打开与 MySQL 数据库的连接:

类型

名称

参数

描述

int

MySqlConnect

该功能已实现与数据库的连接并返回一个连接标识符。这个 ID 在数据库查询时需要。

在连接失败情况下,则返回值 "-1"。对于错误详情,检查变量 MySQLErrorNumberMySqlErrorDescription

典型地,这个函数在 MQL 程序处理 OnInit() 事件时调用。

string pHost

这是 MySQL 服务器的域名或 IP 地址

string pUser

数据库用户名 (例如, root)

string pPassword

数据库用户的口令

string pDatabase

数据库名称

int pPort

数据访问的 TCP/IP 端口 (通常是 3306)

string pSocket

Unix 套接 (对于 Unix 基准的系统)

int pClientFlag

特殊标志组合 (通常是 0)

该 MySqlDisconnect 接口函数已实现关闭连接:

类型

名称 参数 描述

void

MySqlDisconnect

此函数关闭与 MySQL 数据库的连接。

典型地,这个函数在 MQL 程序处理 OnDeinit() 事件时调用。

int pConnection

连接标识符

应当注意的是,MySQL 数据库在硬件故障时,网络拥塞或超时 (若长时间无查询发送到数据库),能够自行关闭连接。

开发者经常利用 OnTick() 事件写数据至数据库。然而,当周末来临,市场闭市,连接仍然在“悬挂”。在此情况下, MySQL 将超时关闭 (省缺是 8 小时)。

而周一,当市场开市,系统发现连接错误。因此,强烈建议每隔一段时间就检查连接,并且/或者重新连接服务器,时间间隔应稍小于 MySQL 服务器设置中指定的超时数值。

1.2. 执行 DML/DDL 查询

DML 操作用于数据操纵 (数据操纵语言)。数据操纵包括以下语句集合: INSERT, UPDATE 和 DELETE。

DDL 操作用于数据定义 (数据定义语言)。这些包括创建 (CREATE) 数据库对象 (表, 视图, 存储过程, 触发器, 等等) 以及修改 (ALTER) 和删除 (DROP)。

这些不是全部的 DML/DDL 语句, 此外, DCL (数据控制语言) 用于分隔数据访问, 但我们不会深入探讨 SQL 的特性。所有这些命令都可以使用 MySqlExecute 接口函数执行:

类型

名称

参数

描述

bool

MySqlExecute

这个函数用来在数据库成功建立之后 (使用 MySqlConnect 函数),执行非 SELECT 的 SQL 语句。

如果命令执行成功,函数返回 true,否则 - false。有关错误详情, 使用 MySQLErrorNumberMySqlErrorDescription

int pConnection

连接标识符

string pQuery

SQL 查询

作为一个 SQL 查询,您也可以使用 USE 命令选择数据库。我想提醒一下使用复合语句的查询。它是 SQL 命令集合,分隔字符 ";"。

要启用复合语句模式,打开数据库连接时应携带 CLIENT_MULTI_STATEMENTS 标志:

...
int ClientFlag = CLIENT_MULTI_STATEMENTS; // Setting the multi-statements flag
int DB; 

DB = MySqlConnect(Host, User, Password, Database, Port, Socket, ClientFlag); // Connection to the database

if (DB == -1)
   {
    // Handling the connection error
   }
...

// Preparing a SQL query to insert data (3 rows in one query)
string SQL;
SQL = "INSERT INTO EURUSD(Ask,Bid) VALUES (1.3601,1.3632);";
SQL = SQL + "INSERT INTO EURUSD(Ask,Bid) VALUES (1.3621,1.3643);";
SQL = SQL + "INSERT INTO EURUSD(Ask,Bid) VALUES (1.3605,1.3629);";
...

if (!MySqlExecute(DB,SQL)) 
   {
    // Showing an error message
   }
...

在这个片段中,利用单次调用数据库,3 个条目将被插入 EURUSD 表中。存储在 SQL 变量中的每个查询,通过 ";" 分隔。

这种方法可用来频繁插入/更新/删除;必要的命令集合组合到一个 "包" 中,从而减轻了网络流量,并提高数据库的性能。

在 MySQL 中,INSERT 语法对异常处理十分出色。

例如,如果任务是移动价格历史,应该创建一个对应货币对的表,其主键是日期型,因为柱线的日期和时间都是唯一的。此外,应该检查是否任意特定的柱线数据都存在于数据库中 (提高数据迁移的稳定性)。对于 MySQL 不需要这项检查, 因为 INSERT 语句支持 ON DUPLICATE KEY。

简单来说, 如果尝试插入数据, 且表中已经有一个记录带有相同日期和时间, 则 INSERT 语句可以被忽视,或此行被 UPDATE 替换 (参见http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html)。

1.3. 数据检索

SQL SELECT 语句用于从数据库中检索数据。下面的操作序列用于检索数据并返回检索的结果:

  1. 准备 SELECT 语句。
  2. 打开游标。
  3. 获得查询回的行数。
  4. 循环获取结果的每一行。
  5. 在循环内将数据赋值给 MQL 变量。
  6. 关闭游标。

当然,这是常用规划,所以并非每种情况都需要全部操作。例如,如果您打算确认表中存在一行数据 (按照任意标准), 这对于准备查询足够了, 打开游标, 得到行数并关闭游标。事实上,强制部分是 - 准备 SELECT 语句,打开和关闭游标。

什么是游标?它是针对一片具有逻辑关系的内存区域的引用, 实际上 - 结果数值的集合。当您发送 SELECT 查询, 数据库为结果分配内存并创建行指针,您可以利用其在数据行之间移动。因此,可以按顺序访问按照查询定义的队列的每一行。 (SELECT 语句的 ORDER BY 子句)。

以下接口函数用于数据检索:

打开游标:

类型

名称

参数

描述

int

MySqlCursorOpen

这个函数为 SELECT 查询打开一个游标,若成功则返回游标标识符。否则, 函数返回 "-1"。为了查找错误原因, 使用变量 MySQLErrorNumberMySqlErrorDescription

int pConnection

数据库连接标识符

string pQuery

SQL 查询 (SELECT 语句)

获得查询返回的行数:

类型

名称

参数

描述

int

MySqlCursorRows

这个函数返回查询的检索行数。

int pCursorID

MySqlCursorOpen 返回的游标标识符

取查询数据行:

类型

名称

参数

描述

bool

MySqlCursorFetchRow

从查询返回的数据集合中取一行数据。在成功执行之后, 您可以将数据赋值到 MQL 变量。若成功函数返回 true, 否则返回 false。

int pCursorID

MySqlCursorOpen 返回的游标标识符

取查询数据行之后,赋值到 MQL 变量:

类型

名称

参数

描述

int

MySqlGetFieldAsInt

函数以 int 数据类型返回数据表字段的值。

int pCursorID

MySqlCursorOpen 返回的游标标识符

int pField

在 SELECT 中的字段号码 (起始编号为 0)

double

MySqlGetFieldAsDouble

函数以 double 数据类型返回数据表字段的值。

int pCursorID

MySqlCursorOpen 返回的游标标识符

int pField

在 SELECT 中的字段号码 (起始编号为 0)

datetime

MySqlGetFieldAsDatetime

函数以 datetime 数据类型返回数据表字段的值。

int pCursorID

MySqlCursorOpen 返回的游标标识符

int pField

在 SELECT 中的字段号码 (起始编号为 0)

string

MySqlGetFieldAsString

函数以 string 数据类型返回数据表字段的值。

int pCursorID

MySqlCursorOpen 返回的游标标识符

int pField

在 SELECT 中的字段号码 (起始编号为 0)

所有 MySQL 返回的数据都有本地表达方式 (不仅表达为字符串)。

因此,使用这些函数,您可以将所选的数据转换为所需的类型。唯一不足的是,在 SELECT 清单中,用列编号(起始编号为 0)替代了它的名字。不过,当开发一个应用程序时,准备 SELECT 语句并获取结果几乎都在一个页面,所以当您规定数据获取逻辑时,您可以看到 SELECT 查询。

因此,您总能知道在 SELECT 清单中的字段数量 (这种方式也同样适用于访问 AdoDB 的数据)。好了,这部分可以在以后修改。它对方案中的功能开发只是略有影响。

关闭游标:

类型

名称

参数

描述

void

MySqlCursorClose

这个函数关闭指定的游标并释放内存。

int pCursorID

MySqlCursorOpen 返回的游标标识符

关闭游标是一个关键操作。 不要忘记关闭游标。

想象一下您打开了一个游标,并忘记关闭它。设想,每次即时报价到达,处理 OnTick() 事件时,数据都要通过游标检索,并且每次都打开一个新游标,并为其分配内存 (客户端与服务器端两者相同)。在某一时刻,服务器将拒绝服务,因为到达打开游标限制,并且导致缓存区溢出。

当然,这有点夸张,这样的结果可能在与 libmysql.dll 直接工作的时候出现。不过,MQLMySQL.DLL 动态链接库为游标分配内存,并将在超出了允许的限制时拒绝打开新游标。

当执行实际任务时,保持 2-3 个打开游标就足够了。每个游标可以处理一个笛卡尔尺寸的数据; 并发使用二至三个游标 (嵌套,例如,一个参数依赖于另一个游标) 可覆盖二至三个维度。对于大多数任务这是完全正常的。此外,为了实现复杂的数据检索,您可以随时使用这些对象来代表数据库 (视图),在服务器端创建它们,并从 MQL 代码里发送查询,就如同数据表一样。

1.4. 附加信息

下面提到的可以作为附加特征:

1.4.1. 从一个 .INI 文件中读取数据

类型

名称

参数

描述

String

ReadIni

返回 INI 文件中给定段落的键值。

string pFileName

INI 文件名file

string pSection

段落名

string pKey

键名

经常在 MQL 代码(或在 EA、指标、脚本的参数)中直接保存有关数据库连接的信息(服务器的 IP 地址, 端口, 用户名, 口令, 等等)是不理性的,因为服务器也许会转移,它的地址也会动态变化,等等。在这种情况下您将需要修改 MQL 代码。因此,所有这些数据应该最好被存储在标准 .INI 文件里,而只将它的名称写在 MQL 程序中。然后,使用 ReadINI 函数来读取连接参数,并使用它们。

例如,INI 文件包含以下信息:

[MYSQL]
Server = 127.0.0.1
User = root
Password = Adm1n1str@t0r
Database = mysql
Port = 3306

为了得到服务器 IP 地址,执行以下语句:

string vServer = ReadIni("C:\\MetaTrader5\\MQL5\\Experts\\MyConnection.ini", "MYSQL", "Server");

该 INI 文件被放置于 C:\MetaTrader5\MQL5\Experts 并且称为 "MyConnection.ini",您可以访问 MYSQL 段落的 Server 键值。在一个 INI 文件中您可以保存多个您的项目中用到的服务器设置。

1.4.2. 问题区域追踪

在接口库中提供了追踪模式,可以启用该模式来调试 MQL 程序中任意位置的 SQL 查询。

在问题区域指定如下:

SQLTrace = true;

或是

SQLTrace = false;

如果您在 MQL 程序开始启用追踪,并且未禁用它,则所有数据库调用都将被记录。而记录被保持在终端的控制台里 (使用 Print 命令)。

2. 例程

这一段提供了一些连接和使用已开发的链接库的例程。参阅它们并评估软件解决方案的可用性。

例程 MySQL-003.mq5 出示如下: 连接数据库 (连接参数保存在 .ini 文件), 创建数据表, 插入数据 (也用到复合语句) 以及从数据库断开连接。

//+------------------------------------------------------------------+
//|                                                    MySQL-003.mq5 |
//|                                   Copyright 2014, Eugene Lugovoy |
//|                                              https://www.mql5.com |
//| Inserting data with multi-statement (DEMO)                       |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, Eugene Lugovoy."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

#include <MQLMySQL.mqh>

string INI;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
 string Host, User, Password, Database, Socket; // database credentials
 int Port,ClientFlag;
 int DB; // database identifier
 
 Print (MySqlVersion());

 INI = TerminalInfoString(TERMINAL_PATH)+"\\MQL5\\Scripts\\MyConnection.ini";
 
 // reading database credentials from INI file
 Host = ReadIni(INI, "MYSQL", "Host");
 User = ReadIni(INI, "MYSQL", "User");
 Password = ReadIni(INI, "MYSQL", "Password");
 Database = ReadIni(INI, "MYSQL", "Database");
 Port     = (int)StringToInteger(ReadIni(INI, "MYSQL", "Port"));
 Socket   = ReadIni(INI, "MYSQL", "Socket");
 ClientFlag = CLIENT_MULTI_STATEMENTS; //(int)StringToInteger(ReadIni(INI, "MYSQL", "ClientFlag"));  

 Print ("Host: ",Host, ", User: ", User, ", Database: ",Database);
 
 // open database connection
 Print ("Connecting...");
 
 DB = MySqlConnect(Host, User, Password, Database, Port, Socket, ClientFlag);
 
 if (DB == -1) { Print ("Connection failed!Error: "+MySqlErrorDescription); } else { Print ("Connected!DBID#",DB);}
 
 string Query;
 Query = "DROP TABLE IF EXISTS `test_table`";
 MySqlExecute(DB, Query);
 
 Query = "CREATE TABLE `test_table` (id int, code varchar(50), start_date datetime)";
 if (MySqlExecute(DB, Query))
    {
     Print ("Table `test_table` created.");
     
     // Inserting data 1 row
     Query = "INSERT INTO `test_table` (id, code, start_date) VALUES ("+(string)AccountInfoInteger(ACCOUNT_LOGIN)+",\'ACCOUNT\',\'"+TimeToString(TimeLocal(), TIME_DATE|TIME_SECONDS)+"\')";
     if (MySqlExecute(DB, Query))
        {
         Print ("Succeeded: ", Query);
        }
     else
        {
         Print ("Error: ", MySqlErrorDescription);
         Print ("Query: ", Query);
        }
     
     // multi-insert
     Query =         "INSERT INTO `test_table` (id, code, start_date) VALUES (1,\'EURUSD\',\'2014.01.01 00:00:01\');";
     Query = Query + "INSERT INTO `test_table` (id, code, start_date) VALUES (2,\'EURJPY\',\'2014.01.02 00:02:00\');";
     Query = Query + "INSERT INTO `test_table` (id, code, start_date) VALUES (3,\'USDJPY\',\'2014.01.03 03:00:00\');";
     if (MySqlExecute(DB, Query))
        {
         Print ("Succeeded!3 rows has been inserted by one query.");
        }
     else
        {
         Print ("Error of multiple statements: ", MySqlErrorDescription);
        }
    }
 else
    {
     Print ("Table `test_table` cannot be created. Error: ", MySqlErrorDescription);
    }
 
 MySqlDisconnect(DB);
 Print ("Disconnected. Script done!");
}

例程 MySQL-004.mq5 出示从数据表中检索数据,该表通过 "MySQL-003.mq5" 脚本创建。 

//+------------------------------------------------------------------+
//|                                                    MySQL-004.mq5 |
//|                                   Copyright 2014, Eugene Lugovoy |
//|                                              https://www.mql5.com |
//| Select data from table (DEMO)                                    |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, Eugene Lugovoy."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

#include <MQLMySQL.mqh>

string INI;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
 string Host, User, Password, Database, Socket; // database credentials
 int Port,ClientFlag;
 int DB; // database identifier
 
 Print (MySqlVersion());

 INI = TerminalInfoString(TERMINAL_PATH)+"\\MQL5\\Scripts\\MyConnection.ini";
 
 // reading database credentials from INI file
 Host = ReadIni(INI, "MYSQL", "Host");
 User = ReadIni(INI, "MYSQL", "User");
 Password = ReadIni(INI, "MYSQL", "Password");
 Database = ReadIni(INI, "MYSQL", "Database");
 Port     = (int)StringToInteger(ReadIni(INI, "MYSQL", "Port"));
 Socket   = ReadIni(INI, "MYSQL", "Socket");
 ClientFlag = (int)StringToInteger(ReadIni(INI, "MYSQL", "ClientFlag"));  

 Print ("Host: ",Host, ", User: ", User, ", Database: ",Database);
 
 // open database connection
 Print ("Connecting...");
 
 DB = MySqlConnect(Host, User, Password, Database, Port, Socket, ClientFlag);
 
 if (DB == -1) { Print ("Connection failed!Error: "+MySqlErrorDescription); return; } else { Print ("Connected!DBID#",DB);}
 
 // executing SELECT statement
 string Query;
 int    i,Cursor,Rows;
 
 int      vId;
 string   vCode;
 datetime vStartTime;
 
 Query = "SELECT id, code, start_date FROM `test_table`";
 Print ("SQL> ", Query);
 Cursor = MySqlCursorOpen(DB, Query);
 
 if (Cursor >= 0)
    {
     Rows = MySqlCursorRows(Cursor);
     Print (Rows, " row(s) selected.");
     for (i=0; i<Rows; i++)
         if (MySqlCursorFetchRow(Cursor))
            {
             vId = MySqlGetFieldAsInt(Cursor, 0); // id
             vCode = MySqlGetFieldAsString(Cursor, 1); // code
             vStartTime = MySqlGetFieldAsDatetime(Cursor, 2); // start_time
             Print ("ROW[",i,"]: id = ", vId, ", code = ", vCode, ", start_time = ", TimeToString(vStartTime, TIME_DATE|TIME_SECONDS));
            }
     MySqlCursorClose(Cursor); // NEVER FORGET TO CLOSE CURSOR !!!
    }
 else
    {
     Print ("Cursor opening failed. Error: ", MySqlErrorDescription);
    }
    
 MySqlDisconnect(DB);
 Print ("Disconnected. Script done!");
}

以上例程包含在实际项目中运用的典型错误处理。

事实上,在 MQL 程序中使用的每个查询,都应该在任意 MySQL 客户端上进行调试(PHPMyAdmin中,DB Ninja,MySQL 控制台)。我个人使用并推荐专业数据库开发软件 Quest TOAD 的 MySQL 版本。

结论

本文并未详细介绍 MQLMySQL.DLL 如何在 Microsoft Visual Studio 2010 (C/C++) 环境里开发与实现。该软件解决方案是专为特别应用设计,并且在多种 MQL 软件开发领域,有超过 100 种成功实现 (来自创建复杂的交易系统至 Web 发布)。