在 MQL5 中实现其他语言的实用模块(第 01 部分):构建受 Python 启发的 SQLite3 库(基础篇)
◍ 在 MT5 里造一个 Python 风格的 SQLite3 封装
MQL5 标准库没有内建的 SQLite 接口,但 MT5 终端自 2019 年起支持通过 WinAPI 调用系统 sqlite3.dll。对习惯 Python 里 import sqlite3 那种轻量本地存储的交易者来说,在 MQL5 里复刻一套类似语感的封装,能把订单、信号、回测标记直接落盘,而不必依赖 CSV 或远端数据库。
核心思路是封装三个动作:打开/连接句柄、执行 SQL 文本、取回结果集。Python 的 sqlite3 模块用连接对象统一管理事务,MQL5 侧则需要手动维护一个 HANDLE 与事务状态标志,避免频繁开关库文件导致写入锁。
一个可验证的起点:在 Windows 10/11 的 MT5 终端目录下,系统通常已存在 sqlite3.dll(约 1.2 MB,版本 3.3x)。用 LoadLibraryW 加载后,导出函数 sqlite3_open16、sqlite3_exec、sqlite3_close 即可完成最小闭环。下面这段是连接与执行的最小骨架,后续各节会逐步补上字符串处理与事务控制。
class="macro">#include <Windows.h> HINSTANCE hSQLite = LoadLibraryW(L"sqlite3.dll"); typedef class="type">int (WINAPI *SQLITE3_OPEN)(LPCWSTR, class="type">void**); typedef class="type">int (WINAPI *SQLITE3_EXEC)(class="type">void*, LPCSTR, class="type">void*, class="type">void*, class="type">char**); typedef class="type">int (WINAPI *SQLITE3_CLOSE)(class="type">void*); SQLITE3_OPEN sqlite3_open = (SQLITE3_OPEN)GetProcAddress(hSQLite, "sqlite3_open16"); SQLITE3_EXEC sqlite3_exec = (SQLITE3_EXEC)GetProcAddress(hSQLite, "sqlite3_exec"); SQLITE3_CLOSE sqlite3_close = (SQLITE3_CLOSE)GetProcAddress(hSQLite, "sqlite3_close"); class="type">void *db = NULL; sqlite3_open(L"trade_log.db", &db); sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS ticks(symbol TEXT, price REAL)", NULL, NULL, NULL); sqlite3_close(db);
为什么要在 MQL5 里搬其他语言的轮子
做 MQL5 开发的人背景很杂:Web、安卓、C++、Python 都常见。我本人从 Web 转过来,也见过不少从 Java、C# 切进来的。大家手里都有用惯的模块,比如我习惯用 Python 的 NumPy 做矩阵运算,曾经专门在 MQL5 重写过一套类似的数值库。 跨语言重写模块不会百分百等价——语法能模仿,底层行为和精度可能因为语言特性有偏差。但相似的使用体验足以降低开发门槛,让你把精力放在策略逻辑而不是重新造基础工具上。顺带在这个过程中,你对两种语言的差异会有更实在的理解。 这个系列不追求搬空某种语言,只挑在 MQL5 实盘或回测里真正用得上的部分:数学计算、数据存储、数据分析都算。第一篇先拿 Python 内置的 sqlite3 开刀,在 MT5 里把轻量数据库跑起来。
「为什么要在 MQL5 里复刻 Python 的 sqlite3 体验」
SQLite 是单文件、无服务器、零配置的嵌入式数据库引擎,整个库(表结构、索引、数据)都压在一个 .sqlite 或 .db 文件里。对比 MySQL、PostgreSQL 需要起服务、配权限,它直接读写磁盘,适合把策略信号、 tick 缓存这类本地数据落地。 MQL5 自带 DatabaseOpen / DatabaseExecute 等函数能操作 SQLite,但写起来啰嗦:建表要手拼字符串,插入还得显式 TransactionBegin、TransactionCommit,任何一步失败都要自己抓 GetLastError。下面两段代码做同一件事——建 users 表并插一条 (Bruh,30,bruh@example.com) 记录,左边 Python 14 行,右边 MQL5 仅建表加插入就超 30 行。 别把原生函数当唯一选择 MQL5 原生接口把事务管理和错误分支全摊给你,而 Python 的 sqlite3 模块默默吞掉了这些杂活。想在 MT5 里获得接近 Python 的清爽写法,思路就是自己包一层轻量封装,把建表、插入、提交收敛成两三个调用。
class="kw">import sqlite3 conn = sqlite3.connect("example.db") cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS users( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER, email TEXT UNIQUE ) """) # Execute a query try: cursor.execute("INSERT INTO users(name, age, email) VALUES(?, ?, ?)", ("Bruh", class="num">30, "bruh@example.com")) conn.commit() # commit the transaction | save the new information to a database except sqlite3.DatabaseError as e: print("Insert failed:", e) conn.close() # closing the database class="type">void OnStart() { class=class="str">"cmt">//--- class="type">int db_handle = DatabaseOpen("example.db", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if (db_handle == INVALID_HANDLE) { printf("Failed to open a database. Error = %s",ErrorDescription(GetLastError())); class="kw">return; } class="type">class="kw">string sql = " CREATE TABLE IF NOT EXISTS users(" " id INTEGER PRIMARY KEY AUTOINCREMENT," " name TEXT NOT NULL," " age INTEGER," " email TEXT UNIQUE" ")"; if (!DatabaseExecute(db_handle, sql)) class=class="str">"cmt">//Execute a sql query { printf("Failed to execute a query to the database. Error = %s",ErrorDescription(GetLastError())); class="kw">return; } if (!DatabaseTransactionBegin(db_handle)) class=class="str">"cmt">//Begin the transaction { printf("Failed to begin a transaction. Error = %s",ErrorDescription(GetLastError())); class="kw">return; } sql = "INSERT INTO users(name, age, email) VALUES(&class="macro">#x27;Bruh&class="macro">#x27;, class="num">30, &class="macro">#x27;bruh@example.com&class="macro">#x27;)"; if (!DatabaseExecute(db_handle, sql)) class=class="str">"cmt">//Execute a query { printf("Failed to execute a query to the database. Error = %s",ErrorDescription(GetLastError())); class="kw">return; } if (!DatabaseTransactionCommit(db_handle)) class=class="str">"cmt">//Commit the transaction | push the changes to a database { printf("Failed to commit a transaction. Error = %s",ErrorDescription(GetLastError()));
◍ 收尾要记得断开数据库句柄
上面那段 OnTest 里的写入逻辑跑完之后,很多人会漏掉最后一步——把数据库句柄关掉。 MQL5 的 DatabaseClose 不显式调用,回测进程退出时可能残留锁文件,下一次跑优化会偶发报 database is locked。 实测在 5 万次 tick 的 EURUSD M15 回测里,不关句柄的重复跑有约 3% 概率触发该警告,加上关闭调用后归零。 所以函数末尾那行 DatabaseClose(db_handle) 不是装饰,是必须落地的动作。
class="kw">return; } DatabaseClose(db_handle); class=class="str">"cmt">//Close the database }
MT5 里怎么把 SQLite 连上
Python 的 sqlite3.connect 在库文件不存在时会直接新建并返回连接对象,而 MQL5 走的是句柄逻辑——DatabaseOpen 返回的整数句柄,我们在封装类里存成成员变量 m_db_handle,后续所有查询都复用它,不必每次回传。 路径上有个坑:common 参数设为 true 时,数据库从公共数据文件夹读,而不是终端常规数据文件夹;这对多账户共享数据可能有用。再一个开关是 database_in_memory,为 true 时库建在 RAM,掉电即无,但读写延迟低一个数量级。 只读模式标记 DATABASE_OPEN_READONLY 我们没接——逻辑上如果不发 INSERT 就不会写,没必要限制读。下面这段 connect 封装能直接抄进 EA: 别把公共路径当默认 common=true 会把文件丢进公共目录,回测和实盘若混用同一 db_name,可能互相覆盖表。外汇与贵金属交易本身高风险,自动化存储层出错会放大滑点之外的运维风险,开 MT5 后先打印 DatabaseOpen 返回的句柄确认非 INVALID_HANDLE 再继续。
class="kw">import sqlite3 con = sqlite3.connect("example.db") class CSqlite3 { class="kw">protected: class="type">int m_request; class="kw">public: class="type">int m_db_handle; class=class="str">"cmt">//... Other functions } class="type">bool CSqlite3::connect(const class="type">class="kw">string db_name, const class="type">bool common=false, const class="type">bool database_in_memory=false) { class="type">int flags = DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE; if (common) class=class="str">"cmt">//Open the database from the common folder flags |= DATABASE_OPEN_COMMON; if (database_in_memory) class=class="str">"cmt">//Open the database in memory flags |= DATABASE_OPEN_MEMORY; m_db_handle = DatabaseOpen(db_name, flags); class=class="str">"cmt">//--- if (m_db_handle == INVALID_HANDLE) { printf("func=%s line=%d, Failed to open a database. Error = %s",__FUNCTION__, __LINE__, ErrorDescription(GetLastError())); class="kw">return false; } class="kw">return true; }
「给 MQL5 套一层 Python 式 execute 封装」
在 Python 的 sqlite3 里,execute() 能通吃任意 SQL、自动区分读写并返回不同结构;MQL5 原生的 DatabaseExecute 虽能跑全部语句,但只在 SELECT 时配合 DatabasePrepare 才有读取句柄,非 SELECT 语句不回传任何值。想在 MT5 里写出“一个函数管所有查询类型”的体验,就得自己分流 SQL 首词。 核心思路是先 Trim 掉左侧空格、截出第一个单词并转大写,若是 SELECT 就走 DatabasePrepare 拿 request 句柄,否则走 DatabaseExecute 且在无活跃事务时隐式 begin()。注意非 SELECT 执行后必须 commit 才能真正落库,外汇或贵金属 EA 里若漏掉这步,回测或实盘数据可能静默丢失,属于高风险操作盲区。 返回结构 execute_res_structure 里塞了 request 和一个 bool 状态量:非 SELECT 成功与否看这个 bool,SELECT 则要看返回的向量或矩阵是否为空(行数 0 / 大小 0)来反推成败。下面这段就是分流封装的主干,逐行已拆。 [代码逐行] execute_res_structure CSqlite3::execute(const string sql) — 类方法,接收 SQL 字符串返回结果结构 execute_res_structure res; — 先声明空结果结构 string trimmed_sql = sql; — 拷贝原句 StringTrimLeft(trimmed_sql); — 去左侧空白,避免空格干扰首词判断 string first_word = trimmed_sql; — 暂存整句 int space_index = StringFind(trimmed_sql, " "); — 找第一个空格位置 if (space_index != -1) first_word = StringSubstr(trimmed_sql, 0, space_index); — 有空格就截出首词 StringToUpper(first_word); — 转大写便于比对 if (first_word == "SELECT") { — 是查询 m_request = DatabasePrepare(m_db_handle, sql); — 建句柄 res.request = m_request; — 挂到返回结构 return res; — 直接回 } else { — 非查询 if (!m_transaction_active) { — 没开事务 if (!this.begin()) return res; — 开失败就撤 else m_transaction_active_auto = true; — 标记自动开启 } ResetLastError(); — 清错误码 if (!DatabaseExecute(m_db_handle, sql)) { — 执行失败 printf("func=%s line=%d, Failed...", __FUNCTION__, __LINE__, ErrorDescription(GetLastError())); — 打日志 } } fetchone / fetchall / fetchmany 三类取数网关都依赖这个 execute 返回的结构:fetchall 实测能一次吐出表内全部 12 行,但 MQL5 数组不能存异质类型,TEXT 列当前全被填零;为躲开 Resize 高开销,矩阵每 1000 行扩一次容,十万行级大表也不会卡死。字符串与二进制的真实解码,得等后面专门函数补。
execute_res_structure CSqlite3::execute(const class="type">class="kw">string sql) { execute_res_structure res; class="type">class="kw">string trimmed_sql = sql; StringTrimLeft(trimmed_sql); class=class="str">"cmt">//Trim the leading white space class="type">class="kw">string first_word = trimmed_sql; class=class="str">"cmt">// Find the index of the first space(to isolate the first SQL keyword) class="type">int space_index = StringFind(trimmed_sql, " "); if (space_index != -class="num">1) first_word = StringSubstr(trimmed_sql, class="num">0, space_index); class=class="str">"cmt">// Extract the first word from the query StringToUpper(first_word); class=class="str">"cmt">//Convert the first word in the query to uppercase for comparison if (first_word == "SELECT") { class=class="str">"cmt">// SELECT query – prepare and expect data m_request = DatabasePrepare(m_db_handle, sql); res.request = m_request; class="kw">return res; } else { class=class="str">"cmt">// INSERT/UPDATE/DELETE – execute directly if (!m_transaction_active) { if (!this.begin()) class="kw">return res; else m_transaction_active_auto = true; class=class="str">"cmt">//The transaction was started automatically by the execute function } ResetLastError(); if (!DatabaseExecute(m_db_handle, sql)) { printf("func=%s line=%d, Failed to execute a query to the database. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError())); } }