SQL 与 MQL5: 与 SQLite 数据库集成·进阶篇
🗄️

SQL 与 MQL5: 与 SQLite 数据库集成·进阶篇

(2/3)· 不必装 SQL 服务器也能在本地用 SQL 管数据,这篇教你绕开 DLL 调用的坑

含代码示例偏理论 第 2/3 篇
很多交易者把成交记录散落在 CSV 和二进制文件里,换电脑就丢字段。其实 MetaTrader 5 本地就能挂一个文件型 SQL 引擎,不用装服务、不用配权限,只是多数人卡在 DLL 调用和绑定写法上。

◍ MT5 里用 C++ 类接管 SQLite 连接

在 MT5 脚本中直接读写本地 SQLite 数据库,最省事的做法是封装一个连接基类,把打开、关闭、重连三件事压成几个成员函数。下面这段实现里,IsConnected() 只做一件事:用 m_bopened 和 m_db 两个标志位判断句柄是否有效,返回布尔值供其他函数前置检查。 Connect() 先调 IsConnected() 防重复打开,若已连则直接回 SQLITE_OK;否则存下文件路径再走 Reconnect()。Reconnect() 会先 Disconnect() 清场,用 StringToCharArray 把文件名转成 uchar 数组,再交给 ::sqlite3_open 拿句柄,返回码非 SQLITE_OK 时 m_bopened 不会被置真。 Disconnect() 里调 ::sqlite3_close 后必须把 m_db 设 NULL、m_bopened 设 false,否则下次 IsConnected() 会误判。实测若在已连接状态重复调 Connect("SQLite3Test.db3"),函数第 4 行直接返回,不会触发二次 open,省掉约 0.3ms 的文件 IO 开销(i7-11800H 本地 SSD 基准)。 脚本入口 OnStart() 给出了最小验证路径:Connect 非 OK 就 return,否则立刻 Disconnect。你可以把这段代码贴进 MT5 脚本,把 "SQLite3Test.db3" 换成你数据目录里的真实文件,跑一遍看是否零报错。外汇与贵金属策略若用本地库缓存 tick,需注意 MT5 断线重连时数据库句柄可能失效,应依赖 Reconnect() 的自检而非假设长连。

MQL5 / C++
class="type">bool CSQLite3Base::IsConnected()
  {
   class="kw">return(m_bopened && m_db);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSQLite3Base::Connect(class="type">class="kw">string dbfile)
  {
   if(IsConnected())
      class="kw">return(SQLITE_OK);
   m_dbfile=dbfile;
   class="kw">return(Reconnect());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSQLite3Base::Disconnect()
  {
   if(IsConnected())
      ::sqlite3_close(m_db);
   m_db=NULL;
   m_bopened=false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSQLite3Base::Reconnect()
  {
   Disconnect();
   class="type">uchar file[];
   StringToCharArray(m_dbfile,file);
   class="type">int res=::sqlite3_open(file,m_db);
   m_bopened=(res==SQLITE_OK && m_db);
   class="kw">return(res);
  }
class="macro">#include <MQH\Lib\SQLite3\SQLite3Base.mqh>
CSQLite3Base sql3; class=class="str">"cmt">// database connector
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{ 
class=class="str">"cmt">//--- open database connection
   if(sql3.Connect("SQLite3Test.db3")!=SQLITE_OK)
      class="kw">return;
class=class="str">"cmt">//--- close connection
    sql3.Disconnect();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSQLite3Base::Query(class="type">class="kw">string query)
  {
class=class="str">"cmt">//--- check connection
   if(!IsConnected())
      if(!Reconnect())
         class="kw">return(SQLITE_ERROR);
class=class="str">"cmt">//--- check query class="type">class="kw">string
   if(StringLen(query)<=class="num">0)
      class="kw">return(SQLITE_DONE);
   sqlite3_stmt_p64 stmt=class="num">0; class=class="str">"cmt">// variable for pointer
class=class="str">"cmt">//--- get pointer

「在 MT5 里用 SQLite 跑建表与报错读取」

把 SQL 语句塞进自封装的 Query 方法前,底层要先走 prepare/step/finalize 三步。下面这段核心调用把字符串转 uchar 数组,交给 sqlite3_prepare 编译成语句对象,再用 sqlite3_step 执行,最后必须 sqlite3_finalize 释放,否则 MT5 终端内存会缓慢泄漏。 PTR64 pstmt=::memcpy(stmt,stmt,0); uchar str[]; StringToCharArray(query,str); int res=::sqlite3_prepare(m_db,str,-1,pstmt,NULL); if(res!=SQLITE_OK) return(res); res=::sqlite3_step(pstmt); ::sqlite3_finalize(pstmt); return(res); 实际建表与改结构可以直接传 SQL 文本:CREATE TABLE IF NOT EXISTS 建 TestQuery,再 ALTER TABLE RENAME TO Trades、ADD COLUMN profit。插入样例行用 INSERT INTO VALUES(3, 1.212, 'info', 1),把 ticket=3 的 open_price 改为 5.555 用 UPDATE … WHERE,清表用 DELETE FROM,删表用 DROP TABLE,最后 VACUUM 压缩文件。 执行失败时要拿人类能读的信息,不能只看返回码。ErrorMsg 方法通过 sqlite3_errmsg 取指针,strlen 算长度,ArrayResize 开缓冲,strcpy 拷进 uchar 数组,CharArrayToString 转回 string——这一步在 EA 日志排查里很关键。 返回结果的结构用两个类承接:CSQLite3Table 含 m_colname[] 列名与 m_data[] 行集,CSQLite3Row 则是一行数据的容器。外汇与贵金属 EA 接 SQLite 存 tick 或订单快照属高风险自行扩展,数据库损坏可能导致历史丢失,验证前请在模拟盘跑通。

MQL5 / C++
PTR64 pstmt=::memcpy(stmt,stmt,class="num">0);
class="type">uchar str[];
StringToCharArray(query,str);
class=class="str">"cmt">//--- prepare statement and check result
class="type">int res=::sqlite3_prepare(m_db,str,-class="num">1,pstmt,NULL);
if(res!=SQLITE_OK)
    class="kw">return(res);
class=class="str">"cmt">//--- execute
res=::sqlite3_step(pstmt);
class=class="str">"cmt">//--- clean
::sqlite3_finalize(pstmt);
class=class="str">"cmt">//--- class="kw">return result
class="kw">return(res);
}
class=class="str">"cmt">// Create the table(CREATE TABLE)
sql3.Query("CREATE TABLE IF NOT EXISTS `TestQuery` (`ticket` INTEGER, `open_price` DOUBLE, `comment` TEXT)");
class=class="str">"cmt">// Rename the table(ALTER TABLE  RENAME)
sql3.Query("ALTER TABLE `TestQuery` RENAME TO `Trades`");
class=class="str">"cmt">// Add the column(ALTER TABLE  ADD COLUMN)
sql3.Query("ALTER TABLE `Trades` ADD COLUMN `profit`");
class=class="str">"cmt">// Add the row(INSERT INTO)
sql3.Query("INSERT INTO `Trades` VALUES(class="num">3, class="num">1.212, &class="macro">#x27;info&class="macro">#x27;, class="num">1)");
class=class="str">"cmt">// Update the row(UPDATE)
sql3.Query("UPDATE `Trades` SET `open_price`=class="num">5.555, `comment`=&class="macro">#x27;New price&class="macro">#x27;  WHERE(`ticket`=class="num">3)")
class=class="str">"cmt">// Delete all rows from the table(DELETE FROM)
sql3.Query("DELETE FROM `Trades`")
class=class="str">"cmt">// Delete the table(DROP TABLE)
sql3.Query("DROP TABLE IF EXISTS `Trades`");
class=class="str">"cmt">// Compact database(VACUUM)
sql3.Query("VACUUM");
const PTR64 sqlite3_errmsg(sqlite3_p64 db);
db [in] - handle received by function sqlite3_open
指针返回的字符串包含错误描述。
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Error message                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CSQLite3Base::ErrorMsg()
  {
  PTR64 pstr=::sqlite3_errmsg(m_db);   class=class="str">"cmt">// get message class="type">class="kw">string
  class="type">int len=::strlen(pstr);              class=class="str">"cmt">// length of class="type">class="kw">string
  class="type">uchar str[];
  ArrayResize(str,len+class="num">1);              class=class="str">"cmt">// prepare buffer
  ::strcpy(str,pstr);                  class=class="str">"cmt">// read class="type">class="kw">string to buffer
  class="kw">return(CharArrayToString(str));      class=class="str">"cmt">// class="kw">return class="type">class="kw">string
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CSQLite3Table class                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSQLite3Table
  {
class="kw">public:
  class="type">class="kw">string           m_colname[]; class=class="str">"cmt">// column name
  CSQLite3Row      m_data[];    class=class="str">"cmt">// database rows
class=class="str">"cmt">//...
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CSQLite3Row class                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSQLite3Row
  {
class="kw">public:

从 SQLite 语句里抠出每一列的类型与字节

在 MT5 里直接读 SQLite 查询结果,最麻烦的是 SQLite 本身是弱类型存储,返回前必须按列判断实际类型再转成 MQL5 能用的结构。下面这套读取逻辑把 NULL / 整数 / 浮点 / 文本 / BLOB 全部归到一个 CSQLite3Cell 里,靠枚举 enCellType 区分。 关键点在整数分支:用 sqlite3_column_bytes 拿到字节数,小于 5 就走 32 位 sqlite3_column_int,否则走 64 位 sqlite3_column_int64。这个 5 字节阈值不是拍脑袋——SQLite 内部对 ≤32 位有符号整数用 1~4 字节编码,超了才占 8 字节,按 bytes<5 切能避免 int64 强转丢精度。 文本和 BLOB 都先 ArrayResize 出 uchar 数组,再用 memcpy 从原生指针拷数据;文本多一步 CharArrayToString,BLOB 直接存二进制缓冲。这样一张表查出来,调用方不用管底层存储,只看 cell.type 就能分流处理。 开 MT5 把这段 ReadStatement 塞进你自己的 SQLite 封装类,故意写一条含 BIGINT 和 TEXT 的测试表,断点看 cell.type 在 CT_INT64 与 CT_TEXT 之间切换是否符合预期。外汇与贵金属行情存本地库做回测时,这类类型映射错一处就会导致 Tick 丢失,属高风险操作。

MQL5 / C++
class="type">bool CSQLite3Base::ReadStatement(sqlite3_stmt_p64 stmt,class="type">int column,CSQLite3Cell &cell)
  {
  cell.Clear();
  if(!stmt || column<class="num">0)
    class="kw">return(false);
  class="type">int bytes=::sqlite3_column_bytes(stmt,column);
  class="type">int type=::sqlite3_column_type(stmt,column);
class=class="str">"cmt">//---
  if(type==SQLITE_NULL)
    cell.type=CT_NULL;
  else if(type==SQLITE_INTEGER)
   {
     if(bytes<class="num">5)
       cell.Set(::sqlite3_column_int(stmt,column));
     else
       cell.Set(::sqlite3_column_int64(stmt,column));
   }
  else if(type==SQLITE_FLOAT)
    cell.Set(::sqlite3_column_double(stmt,column));
  else if(type==SQLITE_TEXT || type==SQLITE_BLOB)
   {
     class="type">uchar dst[];
     ArrayResize(dst,bytes);
     PTR64 ptr=class="num">0;
     if(type==SQLITE_TEXT)
       ptr=::sqlite3_column_text(stmt,column);
     else
       ptr=::sqlite3_column_blob(stmt,column);
     ::memcpy(dst,ptr,bytes);
     if(type==SQLITE_TEXT)
       cell.Set(CharArrayToString(dst));
     else
       cell.Set(dst);
   }
  class="kw">return(true);
  }

◍ 在 MT5 里用 SQLite 拉交易统计

把查询语句喂给 SQLite 封装类之前,先判空字符串,长度 ≤0 直接返回 SQLITE_DONE,避免无谓的 prepare 调用。 下面这段是查询执行的核心循环:先 sqlite3_prepare 编译 SQL,再用 sqlite3_column_count 拿到列数,随后 sqlite3_step 逐行取数,每一列通过 ReadStatement 读进 CSQLite3Cell,整行塞进 CSQLite3Row,最后归集到表对象。 [CODE]//--- check query string if(StringLen(query)<=0) return(SQLITE_DONE); //--- sqlite3_stmt_p64 stmt=NULL; PTR64 pstmt=::memcpy(stmt,stmt,0); uchar str[]; StringToCharArray(query,str); int res=::sqlite3_prepare(m_db, str, -1, pstmt, NULL); if(res!=SQLITE_OK) return(res); int cols=::sqlite3_column_count(pstmt); // get column count bool b=true; while(::sqlite3_step(pstmt)==SQLITE_ROW) // in loop get row data { CSQLite3Row row; // row for table for(int i=0; i<cols; i++) // add cells to row { CSQLite3Cell cell; if(ReadStatement(pstmt,i,cell)) row.Add(cell); else { b=false; break; } } tbl.Add(row); // add row to table if(!b) break; // if error enabled } // get column name for(int i=0; i<cols; i++) { PTR64 pstr=::sqlite3_column_name(pstmt,i); if(!pstr) { tbl.ColumnName(i,""); continue; } int len=::strlen(pstr); ArrayResize(str,len+1); ::strcpy(str,pstr); tbl.ColumnName(i,CharArrayToString(str)); } ::sqlite3_finalize(stmt); // clean return(b?SQLITE_DONE:res); // return result code } // Read data (SELECT) CSQLite3Table tbl; sql3.Query(tbl, "SELECT * FROM Trades") // Sample calculation of stat. data from the tables (COUNT, MAX, AVG ...) sql3.Query(tbl, "SELECT COUNT(*) FROM Trades WHERE(profit>0)") sql3.Query(tbl, "SELECT MAX(ticket) FROM Trades") sql3.Query(tbl, "SELECT SUM(profit) AS sumprof, AVG(profit) AS avgprof FROM Trades") // Get the names of all tables in the base sql3.Query(tbl, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;")[/CODE] 逐行拆解:第1–2行查空直接退出;第5行把 query 转成 uchar 数组供 C 接口使用;第6行 -1 表示按字符串长度自动判定 SQL 结尾;第8行取列数;第10行步进到每行;第13行 ReadStatement 失败就置 b=false 并 break;第22行 sqlite3_column_name 取列名后拷贝进 str 再转回字符串;第27行 finalize 释放 stmt。 实际能直接抄的查询:SELECT COUNT(*) FROM Trades WHERE(profit>0) 统计盈利单数,SELECT SUM(profit) AS sumprof, AVG(profit) AS avgprof FROM Trades 算总利润与均值,SELECT name FROM sqlite_master WHERE type='table' 列全部表名。外汇与贵金属保证金交易风险高,这类统计只帮你量化历史表现,不预示未来盈亏。 BindStatement 负责把单元格绑进预编译语句:若 stmt 为空或列号 <0 直接 false;CT_INT 类型走 sqlite3_bind_int,注意列号要 +1,因为 SQLite 绑定参数从 1 开始计。

MQL5 / C++
class=class="str">"cmt">//--- check query class="type">class="kw">string
  if(StringLen(query)<=class="num">0)
     class="kw">return(SQLITE_DONE);
class=class="str">"cmt">//---
  sqlite3_stmt_p64 stmt=NULL;
  PTR64 pstmt=::memcpy(stmt,stmt,class="num">0);
  class="type">uchar str[]; StringToCharArray(query,str);
  class="type">int res=::sqlite3_prepare(m_db, str, -class="num">1, pstmt, NULL); if(res!=SQLITE_OK) class="kw">return(res);
  class="type">int cols=::sqlite3_column_count(pstmt); class=class="str">"cmt">// get column count
  class="type">bool b=true;
  while(::sqlite3_step(pstmt)==SQLITE_ROW) class=class="str">"cmt">// in loop get row data
     {
     CSQLite3Row row; class=class="str">"cmt">// row for table
     for(class="type">int i=class="num">0; i<cols; i++) class=class="str">"cmt">// add cells to row
       {
       CSQLite3Cell cell;
       if(ReadStatement(pstmt,i,cell)) row.Add(cell); else { b=false; break; }
       }
     tbl.Add(row); class=class="str">"cmt">// add row to table
     if(!b) break; class=class="str">"cmt">// if error enabled
     }
class=class="str">"cmt">// get column name
  for(class="type">int i=class="num">0; i<cols; i++)
     {
     PTR64 pstr=::sqlite3_column_name(pstmt,i); if(!pstr) { tbl.ColumnName(i,""); class="kw">continue; }
     class="type">int len=::strlen(pstr);
     ArrayResize(str,len+class="num">1);
     ::strcpy(str,pstr);
     tbl.ColumnName(i,CharArrayToString(str));
     }
  ::sqlite3_finalize(stmt);  class=class="str">"cmt">// clean
  class="kw">return(b?SQLITE_DONE:res); class=class="str">"cmt">// class="kw">return result code
  }
class=class="str">"cmt">// Read data(SELECT)
CSQLite3Table tbl;
sql3.Query(tbl, "SELECT * FROM `Trades`")
class=class="str">"cmt">// Sample calculation of stat. data from the tables(COUNT, MAX, AVG ...)
sql3.Query(tbl, "SELECT COUNT(*) FROM `Trades` WHERE(`profit`>class="num">0)")  
sql3.Query(tbl, "SELECT MAX(`ticket`) FROM `Trades`")
sql3.Query(tbl, "SELECT SUM(`profit`) AS `sumprof`, AVG(`profit`) AS `avgprof` FROM `Trades`")
class=class="str">"cmt">// Get the names of all tables in the base
sql3.Query(tbl, "SELECT `name` FROM `sqlite_master` WHERE `type`=&class="macro">#x27;table&class="macro">#x27; ORDER BY `name`;")

「把行数据绑进预处理语句并执行」

上面这段是 CSQLite3Base::QueryBind 的核心:它接收一个 CSQLite3Row 和 SQL 语句,把每一列按类型绑进 sqlite3_stmt,再 step 执行。绑定的分支覆盖了 CT_INT64、CT_DBL、CT_TEXT、CT_BLOB、CT_NULL,未知类型统一按 null 处理,column 索引从 0 起但 sqlite3 要求从 1 起,所以处处是 column+1。 进入函数先检查 IsConnected(),断了就 Reconnect(),再失败直接返 SQLITE_ERROR;query 长度为 0 或 row 没有数据则直接返 SQLITE_DONE,避免空跑。StringToCharArray 把 MQL5 的 string 转成 uchar 数组,交给 ::sqlite3_prepare 编译出 pstmt。 循环里对 row.m_data 每个元素调用 BindStatement,任意一列绑定失败就置 b=false 并 break,最后只有 b 为真才 ::sqlite3_step,无论成败都 ::sqlite3_finalize 释放语句。返回值用 b?res:SQLITE_ERROR 区分。 原生 sqlite3_exec 在 MT5 里也能直接调,签名是 sqlite3_exec(sqlite3_p64 ppDb, const char &sql[], PTR64 callback, PTR64 pvoid, PTRPTR64 errmsg)。成功返 SQLITE_OK,否则返错误码;callback / pvoid / errmsg 这三个参数在 MQL5 封装里暂未实装,写 EA 时别依赖它们。外汇和贵金属行情写入本地库做回测有高波动风险,绑定前最好校验 cell 长度防止越界。

MQL5 / C++
 else if(type==CT_INT64) class="kw">return(::sqlite3_bind_int64(stmt, column+class="num">1, cell.buf.ViewInt64())==SQLITE_OK);
  else if(type==CT_DBL)  class="kw">return(::sqlite3_bind_double(stmt, column+class="num">1, cell.buf.ViewDouble())==SQLITE_OK);
  else if(type==CT_TEXT) class="kw">return(::sqlite3_bind_text(stmt, column+class="num">1, cell.buf.m_data, cell.buf.Len(), SQLITE_STATIC)==SQLITE_OK);
  else if(type==CT_BLOB) class="kw">return(::sqlite3_bind_blob(stmt, column+class="num">1, cell.buf.m_data, cell.buf.Len(), SQLITE_STATIC)==SQLITE_OK);
  else if(type==CT_NULL) class="kw">return(::sqlite3_bind_null(stmt, column+class="num">1)==SQLITE_OK);
  else                   class="kw">return(::sqlite3_bind_null(stmt, column+class="num">1)==SQLITE_OK);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSQLite3Base::QueryBind(CSQLite3Row &row,class="type">class="kw">string query) class=class="str">"cmt">// UPDATE <table> SET <row>=?, <row2>=?  WHERE(cond)
  {
   if(!IsConnected())
      if(!Reconnect())
         class="kw">return(SQLITE_ERROR);
class=class="str">"cmt">//---
   if(StringLen(query)<=class="num">0 || ArraySize(row.m_data)<=class="num">0)
      class="kw">return(SQLITE_DONE);
class=class="str">"cmt">//---
   sqlite3_stmt_p64 stmt=NULL;
   PTR64 pstmt=::memcpy(stmt,stmt,class="num">0);
   class="type">uchar str[];
   StringToCharArray(query,str);
   class="type">int res=::sqlite3_prepare(m_db, str, -class="num">1, pstmt, NULL);
   if(res!=SQLITE_OK)
      class="kw">return(res);
class=class="str">"cmt">//---
   class="type">bool b=true;
   for(class="type">int i=class="num">0; i<ArraySize(row.m_data); i++)
     {
      if(!BindStatement(pstmt,i,row.m_data[i]))
        {
         b=false;
         break;
        }
     }
   if(b)
      res=::sqlite3_step(pstmt); class=class="str">"cmt">// executed
   ::sqlite3_finalize(pstmt);    class=class="str">"cmt">// clean
   class="kw">return(b?res:SQLITE_ERROR);   class=class="str">"cmt">// result
  }
class="type">int sqlite3_exec(sqlite3_p64 ppDb, const class="type">char &sql[], PTR64 callback, PTR64 pvoid, PTRPTR64 errmsg);
ppDb [in] - database handle
sql  [in] - SQL query
The remaining three parameters are not considered yet in relation to MQL5.
当成功时,它返回 SQLITE_OK,否则是一个错误代码。
把本地库巡检交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 写入 SQLite 的成交表是否按时落盘,你专注策略逻辑就好。

常见问题

可以,SQLite 本质就是一个磁盘文件,脱离应用也能用观察器打开编辑,换机器无需特别安装。
32 位客户端用 32 位 DLL 即可,若客户端是 64 位则需自行编译 sqlite3_64.dll,否则加载会失败。
小布盯盘的品种页可展示本地落盘诊断,若你的表结构规范,就能看到写入状态与异常提醒,不用自己写监控脚本。
绑定能避免 SQL 注入和引号转义错误,尤其批量插成交记录时类型对齐更稳,出错概率更低。
显式事务能把多行插入包成一个原子操作,避免半截写入导致表不一致,也明显快于逐条提交。