简介:这份文档资料面向使用MetaTrader平台进行量化开发的程序员,聚焦MQL5(MQL4)与MySQL数据库交互这一常见却不易实现的需求。MQL本身不直接支持数据库操作,文档围绕如何借助第三方库或自研方案打通这一链路展开,适合具备一定MQL编程基础、希望实现价格历史存储、跨平台交易复制、实时报价与远程账户监控等功能的开发者参考。资源包内含1个doc文档,大小约147KB,以文字讲解与接口说明为主,便于随时查阅。目前已有227人学习。文档重点剖析了MQLMySQL.mqh接口库、MQLMySQL.dll包装器与libmysql.dll驱动三层结构,并给出打开关闭连接、执行DML/DDL查询、通过游标检索数据的具体思路,同时对比了MySQL包装、MySQL日志、EAX_Mysql等现有项目的优缺点,提醒开发者关注兼容性、性能、游标使用、错误处理与连接管理等关键问题,帮助读者少走弯路,快速搭建可维护的数据库访问方案。
1. 从 MQL5 打通 MySQL:为什么我最终选了 DLL 包装器这条路
如果你写过稍微复杂点的 EA,迟早会撞上同一个问题:MetaTrader 自带的文件读写和全局变量根本撑不起正经的数据分析。想存 tick 级历史、想跨平台同步持仓、想在服务端跑定期统计,最后都得落到一个外部数据库上。MySQL 是最顺手的选择,但 MQL5 和 MQL4 本身不提供任何数据库访问能力,必须借助 DLL 桥接。市面上现成的方案我翻过一圈,有的用数组接收结果集,查询字段一改就得重新分配内存;有的在 MetaTrader 4 build 600 之后因为char换成wchar_t、TMYSQL结构指针换成int直接内存泄漏。折腾到最后,我选了一条基于游标(Cursor)的接口路线:MQLMySQL.mqh头文件 +MQLMySQL.dll包装器 + 标准libmysql.dll驱动,三层结构,职责清晰。这套东西适合已经能写 EA、指标、脚本,但被数据持久化卡住的开发者,也适合想把交易逻辑和数据分析拆开的团队。下面把我拆解和复现的过程完整写出来。
2. 三层架构拆解:MQLMySQL.mqh、MQLMySQL.dll 与 libmysql.dll 怎么配合
2.1 为什么不用数组接收结果集
先说选型理由。很多早期方案的做法是:调用一个查询函数,DLL 内部把结果集塞进一个预分配好的数组,MQL 侧再从这个数组里读。听起来简单,实际用起来很别扭。SELECT 语句的字段数量和类型在开发、调试、维护过程中随时可能变,你每次改查询就得同步改数组大小和类型映射。更麻烦的是内存管理——DLL 分配的内存谁来释放、什么时候释放,一旦逻辑有漏洞就是慢性泄漏。
游标方案把这个问题绕开了。SELECT 执行后不直接返回数据,而是返回一个游标标识符。你通过这个标识符逐行取数据,每取一行,用MySqlGetFieldAsInt、MySqlGetFieldAsString这类函数按字段编号取值。字段编号从 0 开始,对应 SELECT 清单里的顺序。取完一行再取下一行,最后关闭游标释放内存。整个过程内存占用可控,字段增减只影响取值那几行代码,不影响整体结构。
2.2 三个组件各自的职责
MQLMySQL.mqh是你直接#include进项目的头文件。它做了两件事:声明从MQLMySQL.dll导入的函数,以及封装错误处理。你不需要直接调 Windows API,所有交互都通过这个头文件暴露的接口完成。
MQLMySQL.dll是核心包装器。它链接标准libmysql.dll,负责连接管理、游标管理、结果集处理。关键设计是它支持多连接和多游标并发——你可以在同一个 EA 里开多个数据库连接,也可以在一个连接上同时打开多个游标做嵌套查询。互斥机制用来保护共享资源,避免多线程环境下数据错乱。
libmysql.dll是 MySQL 官方提供的本地访问驱动,真正负责和 MySQL 服务器通信。你需要把它放到系统目录或者终端的数据目录下。MetaTrader 5 放在<终端>\MQL5\Libraries,MetaTrader 4 放在<终端>\MQL4\Libraries。也可以直接放C:\Windows\System32,但我不推荐,因为不同项目可能依赖不同版本的libmysql.dll,放终端目录下更干净。
2.3 连接参数与 INI 文件配置
连接参数不要硬编码在 MQL 代码里。服务器地址会变、端口会调、密码会改,每次改代码重新编译不现实。标准做法是写一个.ini文件,MQL 侧用ReadIni函数读取。
INI 文件内容长这样:
[MYSQL] Host = 127.0.0.1 User = root Password = your_password Database = trading_data Port = 3306 Socket = ClientFlag = 0对应的 MQL 读取代码:
// 拼接 INI 文件路径,放在脚本同目录下 INI = TerminalInfoString(TERMINAL_PATH) + "\\MQL5\\Scripts\\MyConnection.ini"; // 逐项读取连接参数 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");ReadIni返回字符串,端口需要转成int。Socket在 Windows 环境下留空即可,Unix 系统才需要指定。ClientFlag控制客户端行为,普通查询填 0,需要一次执行多条 SQL 语句时填CLIENT_MULTI_STATEMENTS。
提示:INI 文件路径用
TerminalInfoString(TERMINAL_PATH)动态获取,不要写死盘符和目录名,换台机器就找不到文件了。
3. 打开连接、执行 DML/DDL、游标取数:完整操作流程
3.1 连接与断开的正确姿势
MySqlConnect返回一个连接标识符,后续所有操作都要带上这个 ID。返回-1表示连接失败,具体原因查MySQLErrorNumber和MySqlErrorDescription。
int DB; int ClientFlag = CLIENT_MULTI_STATEMENTS; // 启用多语句模式 DB = MySqlConnect(Host, User, Password, Database, Port, Socket, ClientFlag); if (DB == -1) { Print("连接失败,错误: ", MySqlErrorDescription); return; } else { Print("连接成功,DBID#", DB); }连接通常在OnInit()里建立,在OnDeinit()里用MySqlDisconnect(DB)关闭。但这里有个血泪经验:MySQL 服务器有超时机制,默认 8 小时没有查询发过来就主动断开。周末市场关闭,EA 不跑OnTick(),连接就挂在那里。周一开盘,第一个查询直接报连接错误。解决办法是定期检查连接状态,间隔略小于服务器超时时间,发现断了就重连。
3.2 执行 INSERT、UPDATE、DELETE 和 CREATE
MySqlExecute负责所有非 SELECT 的 SQL 语句,返回true表示成功,false表示失败。
string Query; // 建表 Query = "CREATE TABLE IF NOT EXISTS `eurusd_ticks` (" "`id` BIGINT AUTO_INCREMENT PRIMARY KEY," "`ask` DOUBLE NOT NULL," "`bid` DOUBLE NOT NULL," "`tick_time` DATETIME NOT NULL," "UNIQUE KEY `uk_time` (`tick_time`)" ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"; if (!MySqlExecute(DB, Query)) { Print("建表失败: ", MySqlErrorDescription); }多语句模式开启后,可以用分号分隔多条 SQL,一次调用全部执行:
Query = "INSERT INTO `eurusd_ticks` (ask, bid, tick_time) VALUES (1.3601, 1.3632, '2024-01-15 10:00:01');"; Query = Query + "INSERT INTO `eurusd_ticks` (ask, bid, tick_time) VALUES (1.3621, 1.3643, '2024-01-15 10:00:02');"; Query = Query + "INSERT INTO `eurusd_ticks` (ask, bid, tick_time) VALUES (1.3605, 1.3629, '2024-01-15 10:00:03');"; if (!MySqlExecute(DB, Query)) { Print("批量插入失败: ", MySqlErrorDescription); }这样做的好处是减少网络往返次数。三条 INSERT 合成一次调用,网络流量降到三分之一,数据库性能明显提升。但要注意,多语句模式下如果中间某条语句出错,前面的可能已经执行了,后面的可能没执行,事务一致性需要自己保证。
对于价格历史这种场景,更稳妥的做法是利用 MySQL 的INSERT ... ON DUPLICATE KEY UPDATE。把tick_time设为主键或唯一键,插入时如果时间戳已存在,自动跳过或更新,不用先 SELECT 检查再决定 INSERT 还是 UPDATE。
3.3 游标取数的六步操作
SELECT 查询走的是另一条路:打开游标、获取行数、循环取行、关闭游标。
string Query; int Cursor, Rows, i; int vId; double vAsk, vBid; datetime vTime; Query = "SELECT id, ask, bid, tick_time FROM `eurusd_ticks` ORDER BY tick_time DESC LIMIT 100"; Print("SQL> ", Query); Cursor = MySqlCursorOpen(DB, Query); if (Cursor >= 0) { Rows = MySqlCursorRows(Cursor); Print(Rows, " 行数据被选中"); for (i = 0; i < Rows; i++) { if (MySqlCursorFetchRow(Cursor)) { vId = MySqlGetFieldAsInt(Cursor, 0); // 第0列: id vAsk = MySqlGetFieldAsDouble(Cursor, 1); // 第1列: ask vBid = MySqlGetFieldAsDouble(Cursor, 2); // 第2列: bid vTime = MySqlGetFieldAsDatetime(Cursor, 3); // 第3列: tick_time Print("行[", i, "]: id=", vId, " ask=", vAsk, " bid=", vBid, " time=", TimeToString(vTime, TIME_DATE|TIME_SECONDS)); } } MySqlCursorClose(Cursor); // 必须关闭,否则内存泄漏 } else { Print("游标打开失败: ", MySqlErrorDescription); }字段编号从 0 开始,对应 SELECT 清单里的顺序。MySqlGetFieldAsInt、MySqlGetFieldAsDouble、MySqlGetFieldAsDatetime、MySqlGetFieldAsString分别处理不同类型。MySQL 返回的数据本身有类型信息,这些函数负责转换成 MQL 对应的类型。
游标关闭是强制操作。每次OnTick()都打开新游标不关闭,内存会持续增长,最终 DLL 拒绝打开新游标。实际任务中保持 2 到 3 个打开游标就够用了,嵌套查询最多用到两三层。
注意:
MySqlCursorRows返回的是结果集总行数,不是剩余行数。循环里用MySqlCursorFetchRow逐行推进,不要重复调用MySqlCursorRows。
4. 避坑排查:连接超时、游标泄漏、多语句翻车与字符集乱码
4.1 连接超时导致周一开盘报错
现象:周五收盘前一切正常,周一开盘第一个数据库操作返回连接错误,错误码类似 "MySQL server has gone away"。
原因:MySQL 服务器默认wait_timeout是 28800 秒(8 小时)。周末两天没有任何查询发过去,服务器主动断开了连接。EA 这边还拿着旧的连接标识符,以为连接还在。
解决:在OnTick()或定时器里加连接检查。简单做法是每次数据库操作前先执行一个轻量查询(比如SELECT 1),如果失败就重新调用MySqlConnect。更优雅的做法是记录上次操作时间,超过一定间隔(比如 4 小时)就主动重连。
4.2 游标忘记关闭导致内存泄漏
现象:EA 运行几个小时后,数据库操作开始报错,提示无法打开新游标。终端内存占用持续上升。
原因:每次OnTick()都调用MySqlCursorOpen,但某条错误分支里MySqlCursorClose没被执行到。游标占用的内存没有释放,累积到 DLL 限制后拒绝服务。
解决:把MySqlCursorClose放在if (Cursor >= 0)块的最后,确保只要游标打开成功就一定关闭。如果中间有return或continue,要特别小心。我一般会在打开游标后立刻写一个注释// 记得关闭,提醒自己。
4.3 多语句模式下部分执行
现象:用CLIENT_MULTI_STATEMENTS一次发送三条 INSERT,返回false,但查数据库发现第一条已经插入了,后两条没有。
原因:多语句模式不是事务。MySQL 逐条执行,遇到错误就停止,前面的语句已经生效,不会自动回滚。
解决:对数据一致性要求高的场景,不要依赖多语句模式做批量操作。改用事务:START TRANSACTION、多条 INSERT、COMMIT,出错时ROLLBACK。或者用INSERT ... ON DUPLICATE KEY UPDATE让单条语句具备幂等性。
4.4 中文和特殊字符乱码
现象:插入包含中文的字符串,数据库里显示为问号或乱码。
原因:连接字符集和数据库字符集不一致。libmysql.dll默认可能用latin1,而数据库表用的是utf8mb4。
解决:在连接后立即执行SET NAMES utf8mb4。或者建表时明确指定DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci。连接参数里如果有字符集选项也一并设置。
4.5 DLL 位数不匹配
现象:MetaTrader 报错 "cannot load DLL" 或 "DLL is not compatible"。
原因:MetaTrader 5 是 64 位程序,需要 64 位的MQLMySQL.dll和libmysql.dll。如果下载的是 32 位版本,加载会失败。
解决:确认终端版本,下载对应位数的 DLL。MetaTrader 4 通常是 32 位,MetaTrader 5 是 64 位。把 DLL 放到正确的Libraries目录下,并在终端设置里允许 DLL 导入。
5. 进阶技巧:用视图简化查询、用追踪模式定位问题
5.1 服务端视图替代复杂 SELECT
游标取数时字段按编号访问,SELECT 清单越长,编号越容易搞混。一个实用技巧是在 MySQL 服务端创建视图,把复杂的 JOIN、聚合、计算逻辑封装起来。MQL 侧只写SELECT * FROM my_view,字段顺序固定,编号不会变。
CREATE VIEW `v_recent_ticks` AS SELECT id, ask, bid, tick_time FROM `eurusd_ticks` WHERE tick_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR) ORDER BY tick_time DESC;MQL 侧查询变成:
Query = "SELECT * FROM `v_recent_ticks`"; Cursor = MySqlCursorOpen(DB, Query); // 字段编号: 0=id, 1=ask, 2=bid, 3=tick_time视图的另一个好处是权限控制。可以只给 MQL 程序访问视图的权限,不暴露底层表结构。
5.2 开启 SQLTrace 追踪所有数据库调用
接口库内置了追踪模式。在 MQL 程序开头设置SQLTrace = true,所有数据库调用都会通过Print输出到终端控制台。调试阶段非常有用,能看到实际发送的 SQL 语句、返回的错误信息、游标打开和关闭的时机。
SQLTrace = true; // 开启追踪 // ... 数据库操作 ... SQLTrace = false; // 调试完成后关闭,避免日志过多追踪模式会输出每条 SQL 的完整内容,包括参数拼接后的最终语句。如果发现 SQL 语法错误,直接复制到 MySQL 客户端里执行,能快速定位问题。
5.3 查询先在客户端验证
我个人的习惯是:任何在 MQL 里要用的 SQL 语句,先在 MySQL 客户端里跑一遍。常用的有 phpMyAdmin、MySQL 命令行、或者 Quest TOAD for MySQL。确认语法正确、返回结果符合预期,再嵌入 MQL 代码。这样能把 SQL 语法错误和 MQL 调用错误分开排查,省很多时间。
5.4 连接池与多账户场景
如果同时跑多个 EA,每个 EA 都开自己的数据库连接,连接数会很快上去。MySQL 默认最大连接数是 151,超过就拒绝新连接。两个办法:一是把多个 EA 的数据库操作集中到一个服务里,EA 通过进程间通信把数据发过去;二是用连接池,复用连接而不是每次新建。MQLMySQL.dll本身支持多连接,但连接池需要自己在 MQL 侧实现——维护一个连接标识符数组,按需分配和回收。
从那以后我每次部署 EA 到实盘前,都强制走一遍连接超时测试、游标泄漏检查和字符集验证。这三项过了,数据库这块基本不会出玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取