SQLite: MQL5 原生 SQL 数据库操纵(基础篇)
◍ MT5 里直接跑 SQLite 的底层入口
MT5 从 build 2085 起在 MQL5 标准库内置了 SQLite 访问接口,这意味着策略回测与实盘 EA 不再依赖外部数据库桥接,可直接在终端内做结构化存储。对做多周期因子库、信号日志落盘的交易者来说,少一层 ODBC / CSV 中转就少一处延迟与丢数据风险。
实际可用的类封装在 Database.mqh 里,核心对象 CDatabase 提供 Open、Execute、Read 等原生方法。下面这段最小可用代码演示了建表与插入一条 tick 记录:
高风险提示:外汇与贵金属杠杆交易可能在一夜之间击穿本金,以下代码仅作技术验证,不构成任何交易建议。
class="macro">#include <Database.mqh> CDatabase db; if(db.Open("ticklog.sqlite")) { db.Execute("CREATE TABLE IF NOT EXISTS ticks(symbol TEXT, bid REAL, ask REAL, tm INTEGER)"); db.Execute("INSERT INTO ticks VALUES(&class="macro">#x27;XAUUSD&class="macro">#x27;,class="num">1503.20,class="num">1503.45,class="num">1582538520)"); db.Close(); }
MT5 里用 SQL 管历史成交的实操路径
MT5 的 MQL5 自带 SQLite 接口,EA 和脚本可直接用 DatabaseOpen() 挂库,不必外挂中间件。把历史成交落库后,按策略、品种、入场时间三个维度切分,能快速看出哪段行情下胜率塌了——这是纯读成交报告做不到的。 查询调试别在终端盲跑。MetaEditor 里写 SQL 后用 DatabasePrint() 把结果打到 EA 日志,配合 DatabaseReadBind() 把行直接绑到结构体,省掉手写字段映射的错漏。 批量写库记得用 DatabaseTransactionBegin() 和 DatabaseTransactionCommit() 包一层。实测 1 万条成交逐条 INSERT 约 3.2 秒,包事务后降到 0.4 秒左右,回测落盘不再卡。 给常用过滤列建索引,按品种查成交的耗时从全表扫描的 180ms 降到 6ms 内。优化结果也能存库,方便跨参数组横向比。外汇与贵金属杠杆高、滑点跳空频繁,历史统计只反映过去概率,实盘须控仓。
「MT5 里的算法交易兵器库」
MQL5 在语法和计算速度上贴近 C++,这是它适合写交易机器人的底层原因。对老手来说,它不只是写个 EA 那么简单,而是能搭出任意复杂度的分析系统。 平台内置的异步交易函数、数学库之外,还给了网络函数、Python 数据互通、OpenCL 并行计算这几样硬货。原生支持 .NET 库智能导入,能接 MS Visual Studio 调试,还能用 DirectX 做可视化。 这些工具意味着你不用跳出 MT5 就能把数据工程、算力加速和图形界面全跑通。想验证这套能力,开个 MT5 随便挂个 OpenCL 样本看 GPU 占用就知道了,外汇贵金属杠杆高,跑实盘前先用策略测试器压一遍。
◍ 在 MQL5 里直接跑 SQLite 做策略回测数据管理
做策略研发绕不开大量品种的历史测试与优化结果,光靠数组和文件读写,到十万级数据行就开始卡手。MQL5 现在能直接挂 SQLite 引擎,把测试产出落库,用 SQL 做聚合和筛选,比手写循环采样省事得多。 官方在 Xeon E5-2690 v3 @ 2.60GHz、Win10 x64 的环境下对比了 MQL5 与 LLVM 9.0.0 编译的 SQLite 执行耗时(毫秒,越少越好)。1000 次插入 MQL5 用 8488ms 快过 LLVM 的 11572ms;5000 次索引检索 307ms 对 385ms;创建索引 33ms 对 43ms。少数项如 25000 行索引更新 MQL5 略慢(165ms vs 161ms),但整体差距在个位数百分比。 没碰过 SQL 的交易者可以把它当成一种声明式工具:一句 SELECT 就能按品种、周期、夏普比捞出行,不用嵌套 for 循环。外汇和贵金属波动大、滑点难复现,回测库里的数字只是概率参考,不能直接当实盘依据。 随文给的 SqLiteTest.zip 含全部 16 项测试源码,开 MT5 把文件丢进 MQL5/Files 跑一遍,就能确认你机器上的冷热路径是否和官方曲线一致。
用 MT5 脚本建表并捞高薪记录
在 MT5 里玩 SQLite,第一步是拿到数据库句柄:DatabaseOpen 用 DATABASE_OPEN_COMMON 把 company.sqlite 丢进终端公共目录,读写加创建标志一并给上。句柄若等于 INVALID_HANDLE 就直接 Print 错误码走人,这步不检查后面全白干。 表结构得自己下 SQL 建。COMPANY 表五个字段——ID 做 PRIMARY KEY 且 NOT NULL,NAME、AGE 同理,ADDRESS 是定长 CHAR(50),SALARY 用 REAL 存小数。ID 这唯一索引相当于订单里的仓位 ID,把零散记录锚成一条线,跨表关联就靠它。 每次跑脚本前先 DatabaseTableExists 探一下,有就 DROP TABLE COMPANY。不然重复插相同 ID 会报错,脚本启动即从草创开始最省心。 插数据用 DatabaseExecute 拼多条 INSERT,原文示例一次性写了 Paul(25000)、Allen(15000) 等四条,字段顺序和值严格对应。脚本结束数据库落盘,下次再跑就是覆盖逻辑。 查 SALARY>15000 的记录走 DatabasePrepare 编译查询拿句柄,再循环 DatabaseRead:首次调用执行并跳首行,之后每次顺移一条,到底返回 false。这样能把高薪行逐条吐出来供策略做二次计算。外汇与贵金属环境接这类本地库做回测,仍属高风险,参数误用可能扭曲样本。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">class="kw">string filename="company.sqlite"; class=class="str">"cmt">//--- create or open the database in the common terminal folder class="type">int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE |DATABASE_OPEN_COMMON); if(db==INVALID_HANDLE) { Print("DB: ", filename, " open failed with code ", GetLastError()); class="kw">return; } ... working with the database class=class="str">"cmt">//--- close the database DatabaseClose(db); } class=class="str">"cmt">//--- if the COMPANY table exists, class="kw">delete it if(DatabaseTableExists(db, "COMPANY")) { class=class="str">"cmt">//--- class="kw">delete the table if(!DatabaseExecute(db, "DROP TABLE COMPANY")) { Print("Failed to drop table COMPANY with code ", GetLastError()); DatabaseClose(db); class="kw">return; } } class=class="str">"cmt">//--- create the COMPANY table if(!DatabaseExecute(db, "CREATE TABLE COMPANY(" "ID INT PRIMARY KEY NOT NULL," "NAME TEXT NOT NULL," "AGE INT NOT NULL," "ADDRESS CHAR(class="num">50)," "SALARY REAL );") { Print("DB: ", filename, " create table failed with code ", GetLastError()); DatabaseClose(db); class="kw">return; } class=class="str">"cmt">//--- enter data to the table if(!DatabaseExecute(db, "INSERT INTO COMPANY(ID,NAME,AGE,ADDRESS,SALARY) VALUES(class="num">1,&class="macro">#x27;Paul&class="macro">#x27;,class="num">32,&class="macro">#x27;California&class="macro">#x27;,class="num">25000.00); " "INSERT INTO COMPANY(ID,NAME,AGE,ADDRESS,SALARY) VALUES(class="num">2,&class="macro">#x27;Allen&class="macro">#x27;,class="num">25,&class="macro">#x27;Texas&class="macro">#x27;,class="num">15000.00); "
「用 SQL 句柄捞出高薪记录」
在 MT5 里往本地 SQLite 塞完数据后,真正有用的是把符合条件的结果读出来。上面这段直接对 COMPANY 表做了一次 SALARY>15000 的筛选查询,拿到 request 句柄后靠 DatabaseRead 循环往外吐字段。 DatabasePrepare 返回 INVALID_HANDLE 时必须先 DatabaseClose 再 return,否则句柄泄漏会在反复回测时拖慢终端。循环内用 DatabaseColumnInteger / DatabaseColumnText / DatabaseColumnDouble 按列序号 0~4 取 id、name、age、address、salary,五个读取全为真才 Print,否则立刻 DatabaseFinalize 收尾。 实跑输出只有 3 条:ID1 Paul 25000.0、ID3 Teddy 20000.0、ID4 Mark 65000.0,说明插入的 4 行里薪资过万的恰好是这三笔。把 15000 阈值改成 30000,输出会只剩 Paul 和 Mark 两条,你可以马上开 MT5 改这个数验证。 DatabaseFinalize(request) 放在循环外,查询句柄用毕即焚,这是避免 SQLite 文件锁死的基本动作。
class="type">int request=DatabasePrepare(db, "SELECT * FROM COMPANY WHERE SALARY>class="num">15000"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); DatabaseClose(db); class="kw">return; } class=class="str">"cmt">//--- print all entries with the salary greater than class="num">15000 class="type">int id, age; class="type">class="kw">string name, address; class="type">class="kw">double salary; Print("Persons with salary > class="num">15000:"); for(class="type">int i=class="num">0; DatabaseRead(request); i++) { class=class="str">"cmt">//--- read the values of each field from the obtained entry if(DatabaseColumnInteger(request, class="num">0, id) && DatabaseColumnText(request, class="num">1, name) && DatabaseColumnInteger(request, class="num">2, age) && DatabaseColumnText(request, class="num">3, address) && DatabaseColumnDouble(request, class="num">4, salary)) Print(i, ": ", id, " ", name, " ", age, " ", address, " ", salary); else { Print(i, ": DatabaseRead() failed with code ", GetLastError()); DatabaseFinalize(request); DatabaseClose(db); class="kw">return; } } class=class="str">"cmt">//--- remove the query after use DatabaseFinalize(request);