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 文件。
在 MetaTrader 终端之间交互 (通过 MQL 程序) 可以在如下元部件的帮助下实现:
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"。对于错误详情,检查变量 MySQLErrorNumber 和 MySqlErrorDescription。 典型地,这个函数在 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。有关错误详情, 使用 MySQLErrorNumber 和 MySqlErrorDescription。 |
|
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 语句用于从数据库中检索数据。下面的操作序列用于检索数据并返回检索的结果:
当然,这是常用规划,所以并非每种情况都需要全部操作。例如,如果您打算确认表中存在一行数据 (按照任意标准), 这对于准备查询足够了, 打开游标, 得到行数并关闭游标。事实上,强制部分是 - 准备 SELECT 语句,打开和关闭游标。
什么是游标?它是针对一片具有逻辑关系的内存区域的引用, 实际上 - 结果数值的集合。当您发送 SELECT 查询, 数据库为结果分配内存并创建行指针,您可以利用其在数据行之间移动。因此,可以按顺序访问按照查询定义的队列的每一行。 (SELECT 语句的 ORDER BY 子句)。
以下接口函数用于数据检索:
打开游标:
类型 |
名称 |
参数 |
描述 |
int |
MySqlCursorOpen |
这个函数为 SELECT 查询打开一个游标,若成功则返回游标标识符。否则, 函数返回 "-1"。为了查找错误原因, 使用变量 MySQLErrorNumber 和 MySqlErrorDescription。 |
|
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 命令)。
这一段提供了一些连接和使用已开发的链接库的例程。参阅它们并评估软件解决方案的可用性。
例程 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 发布)。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程