在 MQL5 中实现其他语言的实用模块(第 01 部分):构建受 Python 启发的 SQLite3 库·进阶篇
🗄️

在 MQL5 中实现其他语言的实用模块(第 01 部分):构建受 Python 启发的 SQLite3 库·进阶篇

(2/3)· 当 MQL5 原生 SQLite 函数不够顺手,如何用 Python 风格封装降低开发心智负担

偏理论 第 2/3 篇
不少交易者直接在 EA 里调 MQL5 原生 SQLite 函数,写一堆连接与语句拼接代码,稍改表结构就满屏报错。把 Python 里现成的 sqlite3 体验硬搬过来,能少走很多弯路,但封装不当反而会埋下隐蔽的事务漏洞。

把 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 报错。

MQL5 / C++
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 就能验证转型是否如预期。

MQL5 / C++
         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 拿到的行数可能和预期不符,建议先在小周期样本库跑通再接实盘数据。

MQL5 / C++
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 跑一遍就能验证哪些列没被正确处理。

MQL5 / C++
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 的格式是否符合预期,重点核对文本列强转后的数值有没有悄悄失真。

MQL5 / C++
            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——说明表里有脏名或测试数据混写,接数据时得自己过滤。外汇与贵金属查询涉及真实行情环境,脚本连库跑历史有断点风险,验证前先在模拟盘过一遍。

MQL5 / C++
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();
  }
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到数据库落库异常提醒,把重复劳动交给小布,你专注决策。

常见问题

原生接口偏底层,建表、插值、事务都要手动拼 SQL 与错误判断;Python 模块把常用动作封装成对象方法,本篇进阶思路即在 MQL5 侧复刻这种调用体验。
可以,但外汇贵金属属高风险,落库失败不应阻断下单逻辑,建议异步写库并保留本地文本备份。
需转义单引号并统一编码,否则含特殊字符的日志会写坏语句;本篇小节专门讲 MQL5 侧文本值的安全处理。
能,小布盯盘品种页内置 AIGC 诊断,可提示写库失败或事务未提交等异常,减少人工查日志。
未提交的事务在连接关闭时可能整段丢失,盘后复盘数据出现断层,排查起来极费时。