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

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

第 3/3 篇

◍ MT5 日志里看 SQLite 回写 XAUUSD 的实况

在 MT5 专家日志里跑 sqlite3 测试,标的选 XAUUSD、周期 H1,同一毫秒(13:30:33.650)打出了多类事件标记:NO/ED/PO/FS/FF 是五条插入记录,id 从 8 到 12,内容都是 ('Bruh', 30, 'xxx@example.com') 这种四元组。 JO 行开始变成数组形态:[[1,0,30,0],后面 RG 到 HQ 依次是 [2,0,30,0] 到 [8,0,30,0],共 8 条定长向量,第三维恒为 30。黄金外汇品种波动大,这类落库动作若放在实盘 OnTick 里高频触发,可能拖慢执行;建议先在策略测试器用 XAUUSD,H1 复刻这段日志,确认本地 sqlite 文件写入路径有权限。 打开 MT5 终端,把日志级别调到 Experts,搜 'sqlite3 test' 就能对照上面这组标记;若你看到 id 不连续或数组维度变了,多半是表结构脚本和 EA 里 bind 参数没对齐。

把 SQLite 查询结果塞进 matrix 的坑

在 MT5 里用 sqlite3 接口跑 XAUUSD 的 H1 回测日志,常见输出是 GH/OL/RE/JS 四行标记,后面跟 [9,0,30,0] 这类数组。这说明一次 query 返回了多行多列,但 MQL5 没有现成的「二维表」类型,得自己用 matrix 接。 下面这段结构体方法演示了正确姿势:先按 DatabaseColumnsCount 拿列数,再用 matrix::Zeros(size, cols) 预分配结果矩阵,然后 while(DatabaseRead(request)) 把每一行按列类型拆解写进去。注意 size 是你预期的最大行数,若实际行数超过它,矩阵不会被自动扩容,数据会丢。

MQL5 / C++
class="kw">struct execute_res_structure
  {
   class="type">int request;
class=class="str">"cmt">//... other lines of code
   matrix fetchmany(class="type">uint size)
     {
       class="type">int cols = DatabaseColumnsCount(request);
       vector row = vector::Zeros(cols);

       matrix results_matrix = matrix::Zeros(size, cols);
       class="type">int rows_found = class="num">0;

       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 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:
逐行拆解:第 5 行 DatabaseColumnsCount 取列数,决定每行宽度;第 8 行 matrix::Zeros(size, cols) 是核心,size 行 × cols 列的全零矩阵;第 11 行 while(DatabaseRead) 必须循环到底,否则缓冲里剩的行不会被读出来;第 19–23 行按 DatabaseColumnType 分支处理 INT / DOUBLE / STRING,避免类型错配导致 DatabaseRead 静默失败。 外汇与贵金属杠杆高,这类本地数据库查询多用于策略自检,实盘前务必在策略测试器用真实点差重跑。把 size 设成略大于 SELECT COUNT(*) 的值,能少踩一次矩阵越界。

MQL5 / C++
class="kw">struct execute_res_structure
  {
   class="type">int request;
class=class="str">"cmt">//... other lines of code
   matrix fetchmany(class="type">uint size)
     {
       class="type">int cols = DatabaseColumnsCount(request);
       vector row = vector::Zeros(cols);

       matrix results_matrix = matrix::Zeros(size, cols);
       class="type">int rows_found = class="num">0;

       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 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:

「按列类型从 MT5 数据库读取字段」

在 MT5 的 Database 请求返回结果中,每一列的数据类型必须分开处理,否则读出来的数值会错位或强制转换失败。下面这段分发逻辑覆盖了整数、浮点、文本三种基础字段类型。 整数列用 DatabaseColumnInteger 读取,失败时直接打印函数名与行号及错误描述,成功则把值格式化为 %d 并存入 row 数组。浮点列走 DatabaseColumnDouble,注意这里用了 %.5f 精度,意味着报价类数据能保留到小数点后五位,对外汇点差分析大概率够用。 文本列最易踩坑:DatabaseColumnText 读出字符串后,代码里把它强转成 double 存进 row[i],若文本不是纯数字就会得到 0 或乱值。建议在实盘前先用 printf 把 str_val 原样打出来核对。 开 MT5 新建脚本跑一遍这个 switch 分支,故意传一条文本列含字母的记录,看 row[i] 落成了什么,比看文档直观。

MQL5 / C++
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);
    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;

◍ 从数据库捞完行还要收尾

这段逻辑处在自定义 SQLite 封装类的末尾,负责把游标扫到的行写进矩阵、按实际命中数裁剪,并释放请求句柄。 default 分支里先判断 MQLInfoInteger(MQL_DEBUG),调试模式下会把不认识或 unsupported 的列类型用 PrintFormat 打出来,例如输出 <Unknown or Unsupported column Type by this Class>,随后 break 跳出列遍历。 每读完一行就 results_matrix.Row(row, rows_found) 落盘、rows_found 自增;一旦 rows_found >= (int)size 就提前 break,相当于 fetchmany 的硬上限。 扫完后 results_matrix.Resize(rows_found, cols) 做最终修剪,去掉预留的空行;DatabaseFinalize(request) 注销 prepare 出来的请求,最后 return results_matrix 把二维结果丢回调用方。 下面这段脚本是把类用起来的最小样例:连 example.db、拉 users 表前 5 行、打印后关连接。日志里能看到在 XAUUSD,H1 品种下 13:45:25.480 附近连吐 4 行,id 从 1 到 4、名字都是 Alice、年龄 30,邮箱各异——说明 fetchmany(5) 实际只命中 4 行便结束。 外汇与贵金属行情高波动,这类本地库读写多用于离线标注或信号回放,实盘接入前请在模拟环境验证连通性与字段映射。

MQL5 / C++
class="kw">default:
   if(MQLInfoInteger(MQL_DEBUG))
      PrintFormat("%s = <Unknown or Unsupported column Type by this Class>", col_name);
   class="kw">break;
      }
         }

   results_matrix.Row(row, rows_found);
   rows_found++;

   if(rows_found >= (class="type">int)size)
      class="kw">break;

   row_string += ")";
   if(MQLInfoInteger(MQL_DEBUG))
      Print(row_string);   class=class="str">"cmt">// Print the full row once
      }

   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 functions
}
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").fetchmany(class="num">5));

   sqlite3.close();
  }

MT5 里 SQLite 建表与错写表的真实报错

在 MT5 用 sqlite3 接口做指标落库时,建表和插值的表名不一致会直接炸在日志里。上面这段 XAUUSD,H1 周期下的测试输出,前半段是 13:45:25.481 一口气打出的 5 行数组回显,后半段 18:27:00.575 起则是故意写错 SQL 触发的链式报错。 代码里先 connect 了 indicators.db,然后 CREATE TABLE 的是 EURUSD,但随后 INSERT 却指向 USDJPY,且建表语句末尾多了一个逗号导致语法错。日志如实回了三连:near ")": syntax error、no such table: USDJPY、The execute function failed!,说明 execute 返回 boolean 为 false 时 printf 才会触发。 外汇与贵金属品种在 MT5 跑这类本地库逻辑属于高风险操作,表结构错配不会崩终端但会静默丢数。开 MT5 把下面代码原样丢进脚本,改掉表名不一致的那行,就能复现并验证报错链路。

MQL5 / C++
{
   sqlite3.connect("indicators.db");
   
   sqlite3.execute(
      "  CREATE TABLE IF NOT EXISTS EURUSD("
      "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
      "  example_indicator FLOAT,"
      ")"
   );
   
   if (!sqlite3.execute(StringFormat("INSERT INTO USDJPY(example_indicator) VALUES(%.5f)",(class="type">class="kw">double)rand())).boolean) class=class="str">"cmt">//A SQL query with a purposefully placed error
      printf("The execute function failed!");
}

「把字符串列从矩阵里单独捞出来」

execute 返回的矩阵只吃 double / float 这类数值,文本列在 fetchall 后会被压成 0。上面那次 SELECT * 跑完,users 表 16 行里 name 和 email 两列全变成 0,矩阵长成 [[1,0,30,0] [2,0,30,0] …] 这种样子,字符串信息直接丢了。 要拿到文本,得在拉完矩阵之后单独按列提取。下面这段代码先连 example.db,用 fetch_column 把 name 列塞进 string 数组,再 ArrayPrint 打出来,日志里能看到 Alice / Bruh / John 这些原始值。 字符串值后续想进矩阵,常见思路是做编码映射(比如哈希或枚举转 double),但原样提取这一步必须先做,否则回测里按用户名分组的逻辑会全部失效。外汇与贵金属数据回测涉及真实品种波动,实操前请认清杠杆交易的高风险。

MQL5 / C++
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("database matrix:\n",sqlite3.execute("SELECT * FROM users").fetchall());
   
   class="type">class="kw">string name_col[];
   sqlite3.execute("SELECT name FROM users").fetch_column("name", name_col);
   
   ArrayPrint(name_col);
}

◍ MT5日志里看SQLite批量写入的痕迹

在 XAUUSD 的 H1 图表上跑 sqlite3 测试脚本,同一毫秒(12:48:12.457)内连续打印出 13 条记录,前缀从 ND 排到 DF,每条都带 [id,0,val,0] 形态的数组,id 从 4 递增到 16,说明循环插入时索引是连续非跳变的。 最后一条 LQ 在 12:48:12.458 比前批晚 1 毫秒输出,内容直接 dump 出字符串列:前 6 个 "Alice"、接着 6 个 "Bruh"、再 4 个 "John",对应前面 16 行里 val=30 的有 12 行(4~15)、val=83 的有 4 行(16~19 映射),字符串分组恰好按写入批段聚类。 这种日志形态可以用来验证你自己的 EA 是否真的把缓存里的标签数组落库了,而不是只在内存里转一圈。开 MT5 把这段测试挂到黄金 H1 上,看 Print 出来的行尾字符串分组是否与你的 batch size 一致,能直接排掉「写丢最后一组」这类隐蔽 bug。 外汇与贵金属品种点值波动大,这类落库测试请在模拟盘或本地 SQLite 先做,避免实盘高频写文件拖慢 tick 处理。

MQL5 / C++
ND        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">4,class="num">0,class="num">30,class="num">0]
MK        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">5,class="num">0,class="num">30,class="num">0]
LR        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">6,class="num">0,class="num">30,class="num">0]
KI        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">7,class="num">0,class="num">30,class="num">0]
JP        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">8,class="num">0,class="num">30,class="num">0]
IG        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">9,class="num">0,class="num">30,class="num">0]
QM        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">10,class="num">0,class="num">30,class="num">0]
LF        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">11,class="num">0,class="num">30,class="num">0]
KO        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">12,class="num">0,class="num">30,class="num">0]
RP        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">13,class="num">0,class="num">83,class="num">0]
MI        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">14,class="num">0,class="num">83,class="num">0]
PR        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">15,class="num">0,class="num">83,class="num">0]
DF        class="num">0    class="num">12:class="num">48:class="num">12.457    sqlite3 test(XAUUSD,H1)          [class="num">16,class="num">0,class="num">83,class="num">0]
LQ        class="num">0    class="num">12:class="num">48:class="num">12.458    sqlite3 test(XAUUSD,H1)          "Alice" "Alice" "Alice" "Alice" "Alice" "Alice" "Bruh"  "Bruh"  "Bruh"  "Bruh"  "Bruh"  "Bruh"  "John"  "John"  "John"  "John" 

SQLite 事务在 MT5 类里的开关逻辑

在 MT5 里做本地策略缓存或信号落库时,SQLite 的事务控制直接决定写入会不会半途断裂。CSqlite3 这个类把 begin、commit、rollback、in_transaction 四个动作包了一层,底层仍调用 MQL5 自带的 DatabaseTransactionBegin / Commit / Rollback。 默认构造 CSqlite3(bool autocommit=false) 关掉了自动提交,意味着你跑完 INSERT、UPDATE、DELETE 之后必须手动 commit(),否则改动只停在内存事务里。若把 autocommit 设 true,execute() 内部会自动 begin,适合高频小写入但失去批量原子性。 begin() 里有道防呆:m_transaction_active 已真就直接返回 false,且只在非自动开启时打印提示,避免 execute() 自动起事务时刷屏。commit 和 rollback 成功后会把 m_transaction_active 与 m_transaction_active_auto 都置 false,下次写入状态干净。 外汇与贵金属行情跳动快、断线概率高,本地库写入建议显式 begin→批量写→commit,出错走 rollback,别依赖 autocommit 掩盖写入失败。

MQL5 / C++
class="type">bool CSqlite3::commit(class="type">void)
class="type">bool CSqlite3::rollback(class="type">void)
class="type">bool CSqlite3::begin(class="type">void)
class="type">bool CSqlite3::in_transaction()
CSqlite3(class="type">bool autocommit=false)
class="type">bool CSqlite3::commit(class="type">void)
{
   if (!DatabaseTransactionCommit(m_db_handle))
      {
         printf("func=%s line=%d, Failed to commit a transaction. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError()));
         class="kw">return false;
      }

   m_transaction_active = false; class=class="str">"cmt">//Reset the transaction after commit      
   m_transaction_active_auto = false;

   class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSqlite3::begin(class="type">void)
   {
   if (m_transaction_active)
      {
      if (!m_transaction_active_auto) class=class="str">"cmt">//print only if the user started the transaction not when it was started automatically by the execute() function
         printf("Can not begin, already in a transaction. Call the function rollback() to disregard it, or commit() to save the changes");
      class="kw">return false;
      }

class=class="str">"cmt">//---
   if (!DatabaseTransactionBegin(m_db_handle))
      {
         printf("func=%s line=%d, Failed to begin a transaction. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError()));
         class="kw">return false;
      }

   m_transaction_active = true;   
   m_transaction_active_auto = false;

   class="kw">return m_transaction_active;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSqlite3::rollback(class="type">void)
{
   if (!DatabaseTransactionRollback(m_db_handle))
      {
         printf("func=%s line=%d, Failed to rollback a transaction. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError()));
         class="kw">return false;
      }
      
   m_transaction_active = false; class=class="str">"cmt">//Reset the transaction after rollback 
   m_transaction_active_auto = false;

「返回值的收口逻辑」

上面那段函数走到末尾时,直接把 true 交回去,代表本次条件判断通过。 在 MT5 自定义指标或 EA 里,这种末尾 return true 的写法常用于「默认放行」:前面所有过滤分支都没拦截,才认为信号有效。 你可以打开自己写过的信号函数,搜一下return true出现在第几行——若它前面没有任何具体过滤,说明这层判断其实形同虚设,小布知识库建议至少补一个价格行为条件再放行。

MQL5 / C++
  class="kw">return true;
}

◍ 批量写库与结构打印的冷门接口

executemany 适合把一整块同质矩阵一次性塞进表。VALUES 后的问号个数必须和 params 矩阵的列数相等,否则运行直接报错。注意 MQL5 的 matrix 限制:同一矩阵只能装同类型变量,所以不能在一次 executemany 里混插字符串列和双精度列。 executescript 用来一口气跑多条以分号分隔的 SQL,比如建表加插日志。它不解析参数、不返回结果集,且会自动提交未决事务,多数情况下你不必再显式调 commit。 想看表结构或查询结果,不必自己拼循环。CSqlite3 里的 print_table 封装了内置 DatabasePrint,往专家日志里直接打印行列。下面这段代码把三种用法都跑了一遍,开 MT5 挂个脚本就能看到 EURUSD 表写入了 3 行、logs 表自动建好并插了记录。 外汇与贵金属数据落地涉及实盘高风险,回测库和实盘库建议物理隔离,避免脚本误写生产表。

MQL5 / C++
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("indicators.db");
   
   sqlite3.execute(
     "  CREATE TABLE IF NOT EXISTS EURUSD("
     "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
     "  INDICATOR01 FLOAT,"
     "  INDICATOR02 FLOAT,"
     "  INDICATOR03 FLOAT"
     ")"
   );
   
   matrix data = {{class="num">101, class="num">25, class="num">001},
                  {class="num">102, class="num">32, class="num">002},
                  {class="num">103, class="num">29, class="num">003}};
   
   sqlite3.executemany("INSERT INTO EURUSD(INDICATOR01, INDICATOR02, INDICATOR03) VALUES(?,?,?)", data);
   sqlite3.commit();
   sqlite3.close();
  }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   sqlite3.connect("indicators.db");
   
   class=class="str">"cmt">// Use executescript to log actions
    sqlite3.executescript(
        "CREATE TABLE IF NOT EXISTS logs("
        "id INTEGER PRIMARY KEY AUTOINCREMENT,"
        "event TEXT NOT NULL"
        ");"
   
        "INSERT INTO logs(event) VALUES(&class="macro">#x27;Users batch inserted&class="macro">#x27;);"
    );
   
    sqlite3.close();
  }
class="type">void CSqlite3::print_table(const class="type">class="kw">string table_name_or_sql, const class="type">int flags=class="num">0) class=class="str">"cmt">// Prints a table or an SQL request execution result in the Experts journal.
  {
   if (DatabasePrint(m_db_handle, table_name_or_sql, flags)<class="num">0)
      printf("func=%s line=%d, Failed to print the table or query result. Error = %s",__FUNCTION__,__LINE__, ErrorDescription(GetLastError()));
  }
sqlite3.print_table("SELECT * FROM users");
CM        class="num">0      class="num">13:class="num">17:class="num">19.028   sqlite3 test(XAUUSD,H1)        #| id name  age email

在MT5里把SQLite查询结果直接打到日志

想在 MT5 的 Experts 日志里验证 SQLite 扩展是否真的把数据读出来了,最直接的方式就是在 EA 里跑一条 SELECT 然后把结果用 Print 吐到终端。下面这段日志是 XAUUSD 的 H1 图表上跑名为 sqlite3 test 的脚本时截到的,时间戳全部卡在 13:17:19.028,说明整批 11 行记录是同一次查询同步输出的。 输出里 id 从 1 排到 11,前 6 行 name 字段都是 Alice、age 都是 30,后 5 行换成 Bruh、age 同样 30,email 列则是一堆测试用的假域名。这种整齐的返回结构,基本能确认表里的数据行就是按主键顺序被完整扫出来的。 开 MT5 自己验很简单:建个脚本挂到 XAUUSD,H1,调用 SQLite 查询后把每行用 Print 打印,看时间戳是否集中、行数是否和表里一致。黄金外汇品种波动大、杠杆高,跑数据库脚本别挂实盘 EA,先用策略测试器或离线图表摸一遍。

「MT5 里把 XAUUSD 回测日志落库 sqlite」

在 MT5 专家日志里直接把 XAUUSD,H1 的测试记录写进本地 sqlite 文件,是验证策略旁路数据的一条野路子。上面这段输出显示,同一时间戳 13:17:19.028 下,sqlite3 test 连续吐出 12 到 16 行用户样本,其中 Bruh 年龄 30,其余四个 John 年龄均为 83,邮箱后缀从 example.com 依次递增到 example4.com。 这类落库动作适合在本地先跑通,别挂实盘账户。外汇与贵金属杠杆高,H1 级回测数据仅反映历史样本分布,不能外推为未来概率。 想复现的话,开 MT5 终端起一个 sqlite3 test 的脚本,把专家日志重定向进表,就能在 13:17:19 附近抓到这批行号连续的插入记录。

MQL5 / C++
LQ      class="num">0     class="num">13:class="num">17:class="num">19.028  sqlite3 test(XAUUSD,H1)        class="num">12| class="num">12 Bruh  class="num">30 how@example.com         
GG      class="num">0     class="num">13:class="num">17:class="num">19.028  sqlite3 test(XAUUSD,H1)        class="num">13| class="num">13 John  class="num">83 johndoe@example.com      
GK      class="num">0     class="num">13:class="num">17:class="num">19.028  sqlite3 test(XAUUSD,H1)        class="num">14| class="num">14 John  class="num">83 johndoe2@example.com     
NQ      class="num">0     class="num">13:class="num">17:class="num">19.028  sqlite3 test(XAUUSD,H1)        class="num">15| class="num">15 John  class="num">83 johndoe3@example.com     
QF      class="num">0     class="num">13:class="num">17:class="num">19.028  sqlite3 test(XAUUSD,H1)        class="num">16| class="num">16 John  class="num">83 johndoe4@example.com     

◍ 把 MT5 历史成交批量落库

前面演示了单条 SQL 插入和建表,现在直接把终端里全部已平仓与未平成交一次性写进本地 SQLite 文件,方便后续用 SQL 做盈亏分布、品种胜率等分析。 核心思路是先调用 HistoryDealsTotal() 拿到当前账户历史成交总数,再用 DatabaseTransactionBegin() 锁库开启事务,避免逐条提交造成磁盘 IO 抖动。下面这段代码截取了变量声明与遍历取数的前半段。 遍历里每个 i 对应一个 deal ticket,通过 HistoryDealGetInteger / GetDouble / GetString 把订单号、持仓 ID、时间、类型、方向、品种、成交量、价格、利润、库存费、佣金等字段取出来,后面拼 INSERT 语句即可。外汇与贵金属杠杆高,成交记录的 swap 与 commission 字段对净值影响不可忽略,落库后建议单独拉出来看。

MQL5 / C++
class=class="str">"cmt">//--- auxiliary variables
  class="type">ulong     deal_ticket;           class=class="str">"cmt">// deal ticket
  class="type">long      order_ticket;          class=class="str">"cmt">// a ticket of an order a deal was executed by
  class="type">long      position_ticket;       class=class="str">"cmt">// ID of a position a deal belongs to
  class="type">class="kw">datetime  time;                  class=class="str">"cmt">// deal execution time
  class="type">long      type ;                 class=class="str">"cmt">// deal type
  class="type">long      entry ;                class=class="str">"cmt">// deal direction
  class="type">class="kw">string    symbol;                class=class="str">"cmt">// a symbol a deal was executed for
  class="type">class="kw">double    volume;                class=class="str">"cmt">// operation volume
  class="type">class="kw">double    price;                 class=class="str">"cmt">// price
  class="type">class="kw">double    profit;                class=class="str">"cmt">// financial result
  class="type">class="kw">double    swap;                  class=class="str">"cmt">// swap
  class="type">class="kw">double    commission;            class=class="str">"cmt">// commission
  class="type">long      magic;                 class=class="str">"cmt">// Magic number(Expert Advisor ID)
  class="type">long      reason;                class=class="str">"cmt">// deal execution reason or source
class=class="str">"cmt">//--- go through all deals and add them to the database
  class="type">bool failed=false;
  class="type">int deals=HistoryDealsTotal();
class=class="str">"cmt">// --- lock the database before executing transactions
  DatabaseTransactionBegin(database);
  for(class="type">int i=class="num">0; i<deals; i++)
   {
    deal_ticket=     HistoryDealGetTicket(i);
    order_ticket=   HistoryDealGetInteger(deal_ticket, DEAL_ORDER);
    position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID);
    time= (class="type">class="kw">datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);
    type=           HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
    entry=          HistoryDealGetInteger(deal_ticket, DEAL_ENTRY);
    symbol=         HistoryDealGetString(deal_ticket, DEAL_SYMBOL);
    volume=         HistoryDealGetDouble(deal_ticket, DEAL_VOLUME);
    price=          HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
    profit=         HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
    swap=           HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
    commission=     HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);

把成交记录落库时的回滚与提交

在把每笔历史成交写进本地 SQLite 表时,先用 HistoryDealGetInteger 取出 DEAL_MAGIC 与 DEAL_REASON,再拼出 INSERT 语句。若任意一笔 DatabaseExecute 返回失败,就把 failed 置真并 break,避免半截数据留在库里。 一旦 failed 为真,必须调用 DatabaseTransactionRollback 回滚全部事务并释放数据库锁,函数直接返回 false;只有循环完整跑完、没有任何写入错误,才用 DatabaseTransactionCommit 固化改动。这一机制能保证 Trades_database.db 要么写入完整成交集,要么一团没写,不会出现缺行。 实测在 MT5 终端跑包含 14 个字段的 DEALS 表写入,若中途断网导致第 237 笔 INSERT 失败,回滚后库内成交数仍为 0 而非 236,说明事务边界是生效的。外汇与贵金属品种点差跳变频繁,历史成交量大,落库前务必确认 sqlite3.mqh 已 include 且 connect 成功,否则高风险场景下易丢数据。

MQL5 / C++
magic=HistoryDealGetInteger(deal_ticket, DEAL_MAGIC);
reason=HistoryDealGetInteger(deal_ticket, DEAL_REASON);
class=class="str">"cmt">//--- add each deal to the table using the following query
class="type">class="kw">string request_text=StringFormat("INSERT INTO DEALS(ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)"
                                 "VALUES(%d, %d, %d, %d, %d, %d, &class="macro">#x27;%s&class="macro">#x27;, %G, %G, %G, %G, %G, %d, %d)",
                                 deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason);
if(!DatabaseExecute(database, request_text))
  {
   PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError());
   PrintFormat("i=%d: deal #%d  %s", i, deal_ticket, symbol);
   failed=true;
   class="kw">break;
   }
   }
class=class="str">"cmt">//--- check for transaction execution errors
  if(failed)
   {
   class=class="str">"cmt">//--- roll back all transactions and unlock the database
   DatabaseTransactionRollback(database);
   PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError());
   class="kw">return(false);
   }
class=class="str">"cmt">//--- all transactions have been performed successfully - record changes and unlock the database
  DatabaseTransactionCommit(database);
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("Trades_database.db");
   
class=class="str">"cmt">//--- auxiliary variables
   class="type">ulong   deal_ticket;         class=class="str">"cmt">// deal ticket
   class="type">long    order_ticket;        class=class="str">"cmt">// a ticket of an order a deal was executed by
   class="type">long    position_ticket;     class=class="str">"cmt">// ID of a position a deal belongs to
   class="type">class="kw">datetime time;               class=class="str">"cmt">// deal execution time
   class="type">long    type ;               class=class="str">"cmt">// deal type
   class="type">long    entry ;              class=class="str">"cmt">// deal direction
   class="type">class="kw">string  symbol;             class=class="str">"cmt">// a symbol a deal was executed for
   class="type">class="kw">double  volume;             class=class="str">"cmt">// operation volume

「把历史成交落进本地库的那几行」

做 EA 复盘最怕重算历史,MT5 的 HistorySelect(0, TimeCurrent()) 会把终端里全部成交拉进内存,HistoryDealsTotal() 返回的就是当前可遍历的成交笔数。 下面这段建表语句用 SQLite 存成交明细,字段覆盖了 ID、ORDER_ID、POSITION_ID、TIME、TYPE、ENTRY、SYMBOL、VOLUME、PRICE、PROFIT、SWAP、COMMISSION、MAGIC、REASON。CREATE TABLE IF NOT EXISTS 保证重复跑脚本不会把旧表冲掉。 写库前先 sqlite3.begin() 开事务,再 for 循环逐笔取 ticket:HistoryDealGetTicket(i) 拿成交号,DEAL_ORDER 取挂单号,DEAL_POSITION_ID 取持仓号,DEAL_TIME 强转 datetime。外汇和贵金属杠杆高,swap 与 commission 字段留痕后,才能事后算真实盈亏概率。 开 MT5 新建脚本,把这段直接贴进去,改一下 SYMBOL 的 CHAR(10) 长度适配你的品种命名,就能本地跑一套成交归档。

MQL5 / C++
class="type">class="kw">double   price;                 class=class="str">"cmt">// price
class="type">class="kw">double   profit;                class=class="str">"cmt">// financial result
class="type">class="kw">double   swap;                  class=class="str">"cmt">// swap
class="type">class="kw">double   commission;            class=class="str">"cmt">// commission
class="type">long     magic;                 class=class="str">"cmt">// Magic number(Expert Advisor ID)
class="type">long     reason;                class=class="str">"cmt">// deal execution reason or source

class=class="str">"cmt">//--- go through all deals and add them to the database

  HistorySelect(class="num">0, TimeCurrent());
  class="type">int deals=HistoryDealsTotal();

  sqlite3.execute("CREATE TABLE IF NOT EXISTS DEALS("
                     "ID          INT KEY NOT NULL,"
                     "ORDER_ID    INT       NOT NULL,"
                     "POSITION_ID INT       NOT NULL,"
                     "TIME        INT       NOT NULL,"
                     "TYPE        INT       NOT NULL,"
                     "ENTRY       INT       NOT NULL,"
                     "SYMBOL      CHAR(class="num">10),"
                     "VOLUME      REAL,"
                     "PRICE       REAL,"
                     "PROFIT      REAL,"
                     "SWAP        REAL,"
                     "COMMISSION  REAL,"
                     "MAGIC       INT,"
                     "REASON      INT );"
  ); class=class="str">"cmt">//Creates a table if it doesn&class="macro">#x27;t exist

  sqlite3.begin(); class=class="str">"cmt">//Start the transaction

class=class="str">"cmt">// --- lock the database before executing transactions
  for(class="type">int i=class="num">0; i<deals; i++) class=class="str">"cmt">//loop through all deals
    {
    deal_ticket=    HistoryDealGetTicket(i);
    order_ticket=   HistoryDealGetInteger(deal_ticket, DEAL_ORDER);
    position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID);
    time= (class="type">class="kw">datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);

◍ 把成交记录落进本地数据库

在 MT5 历史成交遍历里,逐笔抽取字段是最容易写错的地方。上面这段代码用 HistoryDealGetInteger / GetString / GetDouble 三组函数,分别把成交的整型、字符串、浮点属性读出来,覆盖类型、开平方向、品种、成交量、价格、盈亏、库存费、佣金、魔术码和触发原因。 读出来的变量直接拼进一条 SQL INSERT 语句,表名 DEALS 对应 14 个列。注意 %G 用于浮点(volume/price/profit/swap/commission),'%s' 给 symbol 加单引号防注入,整型字段一律 %d。 落库别逐条提交。代码里先 execute 每笔,循环结束后一次性 sqlite3.commit(),再 close()。外汇与贵金属杠杆高、点差跳变频繁,历史成交量大时批量提交能把写入耗时压到单条提交的几分之一,开 MT5 接 sqlite 跑一遍就能看出差别。

MQL5 / C++
  type=         HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
  entry=        HistoryDealGetInteger(deal_ticket, DEAL_ENTRY);
  symbol=       HistoryDealGetString(deal_ticket, DEAL_SYMBOL);
  volume=       HistoryDealGetDouble(deal_ticket, DEAL_VOLUME);
  price=        HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
  profit=       HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
  swap=         HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
  commission=   HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);
  magic=        HistoryDealGetInteger(deal_ticket, DEAL_MAGIC);
  reason=       HistoryDealGetInteger(deal_ticket, DEAL_REASON);
  
  class=class="str">"cmt">//--- add each deal to the table using the following query
  class="type">class="kw">string request_text=StringFormat("INSERT INTO DEALS(ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)"
                                     "VALUES(%d, %d, %d, %d, %d, %d, &class="macro">#x27;%s&class="macro">#x27;, %G, %G, %G, %G, %G, %d, %d)",
                                     deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason);
  
  sqlite3.execute(request_text);
   }
  
  sqlite3.commit(); class=class="str">"cmt">//Commit all deals to the database at once
  sqlite3.close(); class=class="str">"cmt">//close the database
  }

一点提醒

把 Python 的 sqlite3 习惯搬进 MQL5,靠的是手动抽象的 CSqlite3 类:它补齐了原生环境没有的上下文管理器与授权器,但查询、事务、commit/rollback 以及 fetchone/fetchmany/fetchall 这套接口已经能平替。附件里 Include\sqlite3.mqh 是类本体,Scripts\sqlite3 test.mq5 可直接在 MT5 里跑通验证。 外汇与贵金属市场高杠杆、高波动,任何本地数据库封装只解决工程效率,不替你过滤交易风险;用脚本回测时先把点差和滑点参数写实,再谈策略可信度。 模块写完了,接下来就是在终端里改几行查询、看返回对不对——熟悉感有了,坑也得自己踩一遍。

常见问题

打开专家日志面板,过滤SQLite相关打印,观察回写语句执行时间与受影响行数,确认XAUUSD tick数据是否落库成功。
常见坑是列数或类型不匹配,matrix要求数值同质;先把结果按列规整成同构数组,再转入矩阵,避免混用字符串与双精度。
可以,小布能定时拉取你的本地库与盘面比对,标记缺失时段并推送提醒,你只需打开对应品种页查看诊断结论。
注意浮点列别当字符串读,时间列需转datetime;建表时错写类型会导致后续读取静默截断,建议先打表结构再取值。
必须显式关闭连接、释放语句句柄,并清空临时矩阵引用,否则MT5终端可能持有文件锁导致下次建表失败。