在 MQL5 中实现其他语言的实用模块(第 01 部分):构建受 Python 启发的 SQLite3 库·进阶篇
(2/3)· 当 MQL5 原生 SQLite 函数不够顺手,如何用 Python 风格封装降低开发心智负担
把 SQLite 查询结果搬进 MQL5 结构体
想在 MT5 里直接吃 SQLite 的数据,得先解决跨语言字段映射。Python 侧一条 SELECT * FROM users 能吐出 (1, 'Alice', 30, 'alice@example.com'),但 MQL5 没有原生 tuple,只能自己造结构体接住。
下面这段 MQL5 代码定义了一个 execute_res_structure,内部用 DatabaseColumnsCount 拿列数,再用 DatabaseRead 循环把整张表读完——注释里写了 Essential,漏掉这步就只读到游标当前行。
字段类型靠 DatabaseColumnType 判别,整数进 int_val、浮点进 dbl_val、文本进 str_val,再按列名 DatabaseColumnName 做键值对齐。外汇与贵金属行情数据若走这套入库,注意 SQLite 单连接无并发锁,多 EA 同时读可能概率出现 GetLastError 报错。
class="kw">struct execute_res_structure { class="type">int request; CLabelEncoder le; vector fetchone() { class="type">int cols = DatabaseColumnsCount(request); vector row = vector::Zeros(cols); while (DatabaseRead(request)) class=class="str">"cmt">// Essential, read the entire database { class="type">class="kw">string row_string = "("; for (class="type">int i = class="num">0; i < cols; i++) { class="type">int int_val; class=class="str">"cmt">//Integer variable class="type">class="kw">double dbl_val; class=class="str">"cmt">//class="type">class="kw">double class="type">class="kw">string str_val; class=class="str">"cmt">//class="type">class="kw">string variable ENUM_DATABASE_FIELD_TYPE col_type = DatabaseColumnType(request, i); class="type">class="kw">string col_name; if (!DatabaseColumnName(request, i, col_name)) { printf("func=%s line=%d, Failed to read database column name. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError())); class="kw">continue; } class="kw">switch (col_type) class=class="str">"cmt">//Detect a column datatype and assign the value read from every row of that column to the suitable variable { case DATABASE_FIELD_TYPE_INTEGER: if (!DatabaseColumnInteger(request, i, int_val)) printf("func=%s line=%d, Failed to read Integer. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError())); else {
「按字段类型拆数据库列值」
在 MT5 的 Database 读取封装里,列值必须按字段类型分支处理,否则 double 和 text 混写会直接让回测日志失真。下面这段就是针对 INT、FLOAT、TEXT 三种类型的实际取法。 INT 分支里用 DatabaseColumnInteger 取到 int_val,同时用 StringFormat("%d") 拼进 row_string 便于打印,row[i] 存原始整型。FLOAT 分支若读取失败会带 __FUNCTION__ 和 __LINE__ 报错,成功则按 %.5f 精度格式化——外汇报价小数点后 5 位在日志里才看得清。 TEXT 分支把字符串两头加单引号写进 row_string,但 row[i] 强行转 (double) 只适合纯数字文本,真放自由文本会得 0 或乱值。default 分支仅在 MQL_DEBUG 模式下提示未知列类型,正式跑 EA 时静默跳过,避免刷屏。 开 MT5 新建脚本把这段塞进你的 Database 读取循环,故意建一列 TEXT 存 "1.23456",对比 row[i] 和 row_string 就能验证转型是否如预期。
row_string += StringFormat("%d", int_val); row[i] = int_val; } class="kw">break; case DATABASE_FIELD_TYPE_FLOAT: if (!DatabaseColumnDouble(request, i, dbl_val)) printf("func=%s line=%d, Failed to read Double. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError())); else { row_string += StringFormat("%.5f", dbl_val); row[i] = dbl_val; } class="kw">break; case DATABASE_FIELD_TYPE_TEXT: if (!DatabaseColumnText(request, i, str_val)) printf("func=%s line=%d, Failed to read Text. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError())); else { row_string += "&class="macro">#x27;" + str_val + "&class="macro">#x27;"; row[i] = (class="type">class="kw">double)str_val; } class="kw">break; class="kw">default: if (MQLInfoInteger(MQL_DEBUG)) PrintFormat("%s = <Unknown or Unsupported column Type by this Class>", col_name);
◍ 把查询结果塞进矩阵的分块读法
上面这段 MQL5 代码片段演示了如何把 SQLite 查询结果按块读入 matrix,避免一次性堆爆内存。核心思路是先按列数开零向量,再设 CHUNK_SIZE = 1000 作为单批矩阵行容量,用 while(DatabaseRead(request)) 把整库扫完。
日志里能看到实跑痕迹:在 XAUUSD 的 H1 图表上跑测试脚本,Print 输出 (1, 'Alice', 30, 'zero@example.com'),同一条数据被矩阵化后变成 [1,0,30,0]——字符串列在向量里塌成了 0,这点验证时得留心。
外汇与贵金属行情高波动,回测数据库若含跳空时段,fetchall 拿到的行数可能和预期不符,建议先在小周期样本库跑通再接实盘数据。
class="kw">struct execute_res_structure { class="type">int request; class=class="str">"cmt">//... Other functions matrix fetchall() { class="type">int cols = DatabaseColumnsCount(request); vector row = vector::Zeros(cols); class="type">int CHUNK_SIZE = class="num">1000; class=class="str">"cmt">//For optimized matrix handling matrix results_matrix = matrix::Zeros(CHUNK_SIZE, cols); class="type">int rows_found = class="num">0; class=class="str">"cmt">//for counting the number of rows seen in the database while (DatabaseRead(request)) class=class="str">"cmt">// Essential, read the entire database {
按列类型拆解数据库返回行
在 MT5 的 Database API 里,一次查询返回的每一行都要按列的数据类型分别读取,否则类型错配会直接让读取函数返回 false。下面这段逻辑就是遍历 cols 个列,用 DatabaseColumnType 拿到类型后再分流到 int、double、string 三种本地变量。 读取前先用 DatabaseColumnName 取列名,失败就打印错误并 continue 跳过该列,避免后续 switch 落到未定义分支。这里 __FUNCTION__ 和 __LINE__ 是编译器宏,能精确定位报错位置,实盘排查数据库异常时比单纯看 ErrorDescription 更有用。 INTEGER 分支调用 DatabaseColumnInteger,成功就把值塞进 row[i] 数组,同时用 StringFormat("%d", int_val) 拼到 row_string 仅供打印;FLOAT 分支则走 DatabaseColumnDouble,失败同样用 printf 输出 func=… line=… 和 GetLastError 的文本。外汇与贵金属行情数据若走这套本地库缓存,需注意高频读写下的并发风险,可能引发行锁或读取延迟。 别把类型判断写死 如果后续表结构加了新字段类型(例如时间戳或 blob),现有 switch 不覆盖就会静默跳过。建议在 default 分支里也打一行未知类型告警,开 MT5 跑一遍就能验证哪些列没被正确处理。
class="type">class="kw">string row_string = "("; class=class="str">"cmt">//for printing purposes only. Similar to how Python prints for (class="type">int i = class="num">0; i < cols; i++) { class="type">int int_val; class=class="str">"cmt">//Integer variable class="type">class="kw">double dbl_val; class=class="str">"cmt">//class="type">class="kw">double variable class="type">class="kw">string str_val; class=class="str">"cmt">//class="type">class="kw">string variable ENUM_DATABASE_FIELD_TYPE col_type = DatabaseColumnType(request, i); class="type">class="kw">string col_name; if (!DatabaseColumnName(request, i, col_name)) { printf("func=%s line=%d, Failed to read database column name. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError())); class="kw">continue; } class="kw">switch (col_type) class=class="str">"cmt">//Detect a column datatype and assign the value read from every row of that column to the suitable variable { case DATABASE_FIELD_TYPE_INTEGER: if (!DatabaseColumnInteger(request, i, int_val)) printf("func=%s line=%d, Failed to read Integer. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError())); else { row_string += StringFormat("%d", int_val); class=class="str">"cmt">//For printing purposes only row[i] = int_val; } class="kw">break; case DATABASE_FIELD_TYPE_FLOAT: if (!DatabaseColumnDouble(request, i, dbl_val)) printf("func=%s line=%d, Failed to read Double. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
「数据库行解析里的类型分支处理」
这段逻辑处在遍历数据库结果集列的过程中,按字段类型把值塞进 row 数组并拼出可读字符串。double 类型用 StringFormat("%.5f", dbl_val) 控制成 5 位小数输出,仅用于打印观察,真正存进 row[i] 的是原始 double 值。 TEXT 类型先调 DatabaseColumnText 读取,失败就带函数名和行号打出错误描述;成功则把字符串包单引号追加进 row_string,同时用 (double)str_val 强转存进 row——这里若文本不是纯数字,运行期可能得到 0 或 NaN,需自己加校验。 default 分支只在 MQL_DEBUG 模式下提示未知或不支持的列类型,不中断循环。拼串时靠 if(i < cols - 1) 判断补逗号,最后统一加右括号并整体 Print 一次,避免逐列刷屏。 开 MT5 把这段嵌进你的 DatabaseRead 类,跑一笔真实查询,看 DEBUG 日志里 row_string 的格式是否符合预期,重点核对文本列强转后的数值有没有悄悄失真。
else { row_string += StringFormat("%.5f",dbl_val); class=class="str">"cmt">//For printing purposes only row[i] = dbl_val; } class="kw">break; case DATABASE_FIELD_TYPE_TEXT: if (!DatabaseColumnText(request, i, str_val)) printf("func=%s line=%d, Failed to read Text. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError())); else { row_string += "&class="macro">#x27;" + str_val + "&class="macro">#x27;"; row[i] = (class="type">class="kw">double)str_val; } class="kw">break; class="kw">default: if (MQLInfoInteger(MQL_DEBUG)) PrintFormat("%s = <Unknown or Unsupported column Type by this Class>", col_name); class="kw">break; } class=class="str">"cmt">// Add comma if not last element if (i < cols - class="num">1) row_string += ", "; } class=class="str">"cmt">//--- row_string += ")"; if (MQLInfoInteger(MQL_DEBUG)) Print(row_string); class=class="str">"cmt">// Print the full row once
◍ 把数据库结果塞进动态矩阵再回吐
这段收尾逻辑干的事很直白:每从 SQLite 拽出一行就先让 rows_found 自增,再把行写进矩阵。当已写行数超过当前矩阵容量时,按 CHUNK_SIZE 整块扩容,避免频繁重分配拖慢回测脚本。 rows_found++ 是行计数器自增;if(rows_found > (int)results_matrix.Rows()) 触发扩容,调用 Resize(rows_found+CHUNK_SIZE, cols)。真正落库是 results_matrix.Row(row, rows_found-1),把当前记录插到末尾。 循环结束后还有一次 Resize(rows_found, cols),按实际命中行数做最终裁剪,去掉预留的空位。DatabaseFinalize(request) 释放预编译句柄,函数 return results_matrix 把干净矩阵交回调用方。 下面这段 OnStart 是最薄封装:连 example.db、跑 SELECT * FROM users、fetchall 后 Print。日志里 XAUUSD,H1 品种下打出了 7 行,id 1 到 7,前 6 行 age 都是 30、名字 Alice,第 7 行名字变成 Bruh——说明表里有脏名或测试数据混写,接数据时得自己过滤。外汇与贵金属查询涉及真实行情环境,脚本连库跑历史有断点风险,验证前先在模拟盘过一遍。
class=class="str">"cmt">//--- rows_found++; class=class="str">"cmt">//Increment the rows counter if (rows_found > (class="type">int)results_matrix.Rows()) results_matrix.Resize(results_matrix.Rows()+CHUNK_SIZE, cols); class=class="str">"cmt">//Resizing the array after class="num">1000 rows results_matrix.Row(row, rows_found-class="num">1); class=class="str">"cmt">//Insert a row into the matrix } results_matrix.Resize(rows_found, cols); class=class="str">"cmt">//Resize the matrix according to the number of unknown rows found in the database | Final trim DatabaseFinalize(request); class=class="str">"cmt">//Removes a request created in DatabasePrepare(). class="kw">return results_matrix; class=class="str">"cmt">// class="kw">return the final matrix } class=class="str">"cmt">//... Other lines of code } class="macro">#include <sqlite3.mqh> CSqlite3 sqlite3; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- sqlite3.connect("example.db"); Print(sqlite3.execute("SELECT * FROM users").fetchall()); sqlite3.close(); }