SQL 与 MQL5: 与 SQLite 数据库集成·进阶篇
(2/3)· 不必装 SQL 服务器也能在本地用 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() 的自检而非假设长连。
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 或订单快照属高风险自行扩展,数据库损坏可能导致历史丢失,验证前请在模拟盘跑通。
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 丢失,属高风险操作。
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 开始计。
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 长度防止越界。
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,否则是一个错误代码。