SQL 与 MQL5: 与 SQLite 数据库集成(基础篇)
📘

SQL 与 MQL5: 与 SQLite 数据库集成(基础篇)

第 1/3 篇

为什么在 MT5 里直接挂 SQLite

在 MT5 里做策略研发,最容易被忽视的一环是本地数据存储。很多人习惯把 tick 或信号写进 CSV,文件一大,读取就卡,回测时还要反复解析字符串。 MQL5 从 build 490 起内置了 SQLite 支持,不需要额外 DLL。数据库文件就是一个独立 .sqlite 文件,体积小、读写快,还能用标准 SQL 做聚合查询。 官方 2014 年 6 月 25 日发布的集成示例帖,至今仍有 4574 次查看、46 条讨论,说明这套轻量方案在长期实盘里站得住脚。小、快、灵活,三者可以兼得,不必为了性能牺牲便携性。 开 MT5 按 F4 进编辑器,新建 EA 后直接 Include <SQLite.mqh>,就能在 OnInit 里建表写数据,下一步该考虑的是把你现在的日志模块迁过去。

「MT5 里绕开 DBMS 的本地 SQL 方案」

做 EA 或指标的人常卡在一个选择上:想把运行数据落盘,却不想让用户装 SQL 服务器。对普通交易者而言,部署 MySQL 或 PostgreSQL 几乎是劝退门槛,于是多数项目退回到 CSV、JSON 或多文件二进制,结构一复杂就难维护。 其实 SQLite 能直接嵌进 MT5 项目,不需要额外装服务,所有读写都在客户端本地完成,还能用标准 SQL 做查询和事务。它本质是一个文件级数据库引擎,适合存储成交、参数集或中间计算结果。 本文只帮你快速跑通,不展开 API 参数全集。我会写一个轻量连接包装类,封装打开、执行、取数、绑定和事务,你拷进 MT5 就能调。 动手前先备三样:把文章附件的存档解压到 MetaTrader 5 客户端目录;装一个 SQLite 观察器(如 SQLiteStudio)方便肉眼查表;把 sqlite.org 官方文档收进收藏夹,后面查语法用得着。外汇与贵金属交易本身高风险,任何本地数据存储方案都不构成交易信号或收益保证。

◍ 为什么 MT5 里能直接塞进一个文件型数据库

SQLite 本质是关系型数据库,但和 MySQL、SQL Server 不同,它不依赖独立运行的服务器进程。在 MT5 这类客户端程序里集成 SQLite,实际就是读写一个本地文件——可以落在磁盘,也可以放在内存里。整个数据库可随文件复制、迁移到别的机器,不用做任何环境配置。 对交易工具开发者来说,这种形态有几个实在好处:不装额外服务、数据以本地文件形式存在因而能用第三方工具直接查看编辑、表结构可导入导出到其他关系型库、查询走标准 SQL 所以以后换 DBMS 成本很低。 和 SQLite 打交道通常三条路:调用带完整 API 的 DLL、用命令行 EXE 跑脚本、把 SQLite 源码编进自己项目。本文只走第一条,也就是在 MQL5 里直接调 DLL 那套接口,这对写 EA 和指标的人来说最顺手。

MT5 里直接调 SQLite3 的底层门道

要在 MT5 里用 SQLite 做本地数据存储,得先认清楚底层要 import 哪些函数。除了 sqlite3_open / prepare / step / finalize / close / exec 这一组核心 API,还要从 msvcrt.dll 拿 strlen、strcpy、memcpy 来搬指针和字符串。因为 MT5 客户端分 32 位和 64 位,指针宽度不同,连接器里所有 API 函数都按指针尺寸重载,连接器内部统一用 64 位指针,进 32 位重载函数时再强转。 SQLite 本身只有五种存储类型:NULL、INTEGER(1/2/3/4/6/8 字节按值自适应)、REAL(8 字节)、TEXT(UTF-8 或 UTF-16 以 \0 结尾)、BLOB(裸二进制)。建表时写 BIGINT、INT 这类别名,引擎会映射到 INTEGER,细节在 sqlite.org/datatype3 文档里。 打开数据库就是打开一个文件拿句柄,sqlite3_open 完事,关掉调 sqlite3_close。跑一段调试脚本,数据库文件会落在 MT5 安装目录下,逐个字符串看返回码就能验证通路。 执行 SQL 走三段式:prepare 校验语句、step 执行、finalize 清内存。纯写表或删表这么干就够了;要读数据或写二进制,就在 step 阶段插 column_xx 或 bind_xx。下面这段代码是连接器要 import 的函数清单,注意 32 位终端还要单独处理指针宽度。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//--- general functions</span>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/open.html" href="http://www.sqlite.org/c3ref/open.html" target="_blank">sqlite3_open</a>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/prepare.html" href="http://www.sqlite.org/c3ref/prepare.html" target="_blank">sqlite3_prepare</a>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/step.html" href="http://www.sqlite.org/c3ref/step.html" target="_blank">sqlite3_step</a>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/finalize.html" href="http://www.sqlite.org/c3ref/finalize.html" target="_blank">sqlite3_finalize</a>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/close.html" href="http://www.sqlite.org/c3ref/close.html" target="_blank">sqlite3_close</a>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/exec.html" href="http://www.sqlite.org/c3ref/exec.html" target="_blank">sqlite3_exec</a>
<span class="comment">class=class="str">"cmt">//--- functions for getting error descriptions</span>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/errcode.html" href="http://www.sqlite.org/c3ref/errcode.html" target="_blank">sqlite3_errcode
sqlite3_extended_errcode
sqlite3_errmsg</a>
<span class="comment">class=class="str">"cmt">//--- functions for saving data</span>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/bind_blob.html" href="http://www.sqlite.org/c3ref/bind_blob.html" target="_blank">sqlite3_bind_null
sqlite3_bind_int
sqlite3_bind_int64
sqlite3_bind_double
sqlite3_bind_text
sqlite3_bind_blob</a>
<span class="comment">class=class="str">"cmt">//--- functions for getting data</span>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/column_count.html" href="http://www.sqlite.org/c3ref/column_count.html" target="_blank">sqlite3_column_count</a>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/column_name.html" href="http://www.sqlite.org/c3ref/column_name.html" target="_blank">sqlite3_column_name</a>
<a title="http:class=class="str">"cmt">//www.sqlite.org/c3ref/column_blob.html" href="http://www.sqlite.org/c3ref/column_blob.html" target="_blank">sqlite3_column_type
sqlite3_column_bytes
sqlite3_column_int
sqlite3_column_int64
sqlite3_column_double
sqlite3_column_text
sqlite3_column_blob</a>
<a href="/zh/articles/class="num">364#class="num">4" target="_blank">strlen
strcpy
memcpy</a>
<span class="comment">class=class="str">"cmt">// for a class="num">32 bit terminal</span>

「把 SQLite 句柄封进 CSQLite3Base 类」

在 64 位 MT5 终端里接 SQLite,第一步是把指针类型按位数理顺。32 位下用 int 当指针(PTR32),64 位下改用 long(PTR64),sqlite3_p64 就是数据库句柄的别名,避免到处写裸类型。 [CODE] #define PTR32 int #define sqlite3_stmt_p32 PTR32 #define sqlite3_p32 PTR32 #define PTRPTR32 PTR32 // for a 64 bit terminal #define PTR64 long #define sqlite3_stmt_p64 PTR64 #define sqlite3_p64 PTR64 #define PTRPTR64 PTR64 int sqlite3_open(const uchar &filename[], sqlite3_p64 &ppDb); int sqlite3_close(sqlite3_p64 ppDb); [/CODE] sqlite3_open 的 filename 是入参(UTF-8 路径或当前目录文件名),ppDb 是出参,用来接文件句柄;返回 SQLITE_OK 表示成功,否则返回错误码。sqlite3_close 只吃句柄,同样用返回码判断成败。 真正省事的做法是包一层 CSQLite3Base:m_db 存句柄,m_bopened 标记句柄是否有效,m_dbfile 记路径。构造时把 m_db 置 NULL、m_bopened 置 false;析构直接调 Disconnect,防止 EA 卸载时漏关库文件。 [CODE] class CSQLite3Base { sqlite3_p64 m_db; // pointer to database file bool m_bopened; // flag "Is m_db handle valid" string m_dbfile; // path to database file public: CSQLite3Base(); // constructor virtual ~CSQLite3Base(); // destructor public: bool IsConnected(); int Connect(string dbfile); void Disconnect(); int Reconnect(); }; CSQLite3Base::CSQLite3Base() { m_db=NULL; m_bopened=false; } CSQLite3Base::~CSQLite3Base() { Disconnect(); } [/CODE] 做价格行为回测时把 tick 落库,用这个类管连接比裸调 API 稳。外汇和贵金属杠杆高、滑点跳空频繁,落库失败可能丢样本,建议在 Connect 返回非 SQLITE_OK 时直接 Print 错误码再退出。

MQL5 / C++
class="macro">#define PTR32                class="type">int
class="macro">#define sqlite3_stmt_p32     PTR32
class="macro">#define sqlite3_p32          PTR32
class="macro">#define PTRPTR32             PTR32
class=class="str">"cmt">// for a class="num">64 bit terminal
class="macro">#define PTR64                class="type">long
class="macro">#define sqlite3_stmt_p64     PTR64
class="macro">#define sqlite3_p64          PTR64
class="macro">#define PTRPTR64             PTR64
class="type">int sqlite3_open(const class="type">uchar &filename[], sqlite3_p64 &ppDb);
class="type">int sqlite3_close(sqlite3_p64 ppDb);
class CSQLite3Base
  {
   sqlite3_p64       m_db;          class=class="str">"cmt">// pointer to database file
   class="type">bool              m_bopened;     class=class="str">"cmt">// flag "Is m_db handle valid"
   class="type">class="kw">string            m_dbfile;      class=class="str">"cmt">// path to database file
class="kw">public:
                     CSQLite3Base();  class=class="str">"cmt">// constructor
   class="kw">virtual          ~CSQLite3Base();  class=class="str">"cmt">// destructor
class="kw">public:
   class="type">bool              IsConnected();
   class="type">int               Connect(class="type">class="kw">string dbfile);
   class="type">void              Disconnect();
   class="type">int               Reconnect();
  };
CSQLite3Base::CSQLite3Base()
  {
   m_db=NULL;
   m_bopened=false;
  }
CSQLite3Base::~CSQLite3Base()
  {
   Disconnect();
  }

常见问题

可以,用文件型数据库把数据写进单个文件,EA启动时直接读写,不依赖任何外部数据库服务。
在本地建一个文件库表,回测中每根K线算完就插一行,跑完直接用SQL查,比写CSV好检索。
小布可把触发的异常波动自动落盘到本地文件库,你后续用SQL按时间或幅度筛出复盘,不用手动记。
封类能统一管打开、关闭和报错,避免裸调时忘了释放句柄导致卡死,也方便多个EA复用。
单机跑策略、存几万到几十万行记录完全够,但别拿它做多终端并发共享,那是服务端数据库的事。