在 MQL5 中实现其他语言的实用模块(第 01 部分):构建受 Python 启发的 SQLite3 库·综合运用
- MT5 日志里看 SQLite 回写 XAUUSD 的实况
- 把 SQLite 查询结果塞进 matrix 的坑
- 按列类型从 MT5 数据库读取字段
- 从数据库捞完行还要收尾
- MT5 里 SQLite 建表与错写表的真实报错
- 把字符串列从矩阵里单独捞出来
- MT5日志里看SQLite批量写入的痕迹
- SQLite 事务在 MT5 类里的开关逻辑
- 返回值的收口逻辑
- 批量写库与结构打印的冷门接口
- 在MT5里把SQLite查询结果直接打到日志
- MT5 里把 XAUUSD 回测日志落库 sqlite
- 把 MT5 历史成交批量落库
- 把成交记录落库时的回滚与提交
- 把历史成交落进本地库的那几行
- 把成交记录落进本地数据库
- 一点提醒
◍ 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 是你预期的最大行数,若实际行数超过它,矩阵不会被自动扩容,数据会丢。
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:
DatabaseColumnsCount 取列数,决定每行宽度;第 8 行 matrix::Zeros(size, cols) 是核心,size 行 × cols 列的全零矩阵;第 11 行 while(DatabaseRead) 必须循环到底,否则缓冲里剩的行不会被读出来;第 19–23 行按 DatabaseColumnType 分支处理 INT / DOUBLE / STRING,避免类型错配导致 DatabaseRead 静默失败。
外汇与贵金属杠杆高,这类本地数据库查询多用于策略自检,实盘前务必在策略测试器用真实点差重跑。把 size 设成略大于 SELECT COUNT(*) 的值,能少踩一次矩阵越界。
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] 落成了什么,比看文档直观。
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 行便结束。
外汇与贵金属行情高波动,这类本地库读写多用于离线标注或信号回放,实盘接入前请在模拟环境验证连通性与字段映射。
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 把下面代码原样丢进脚本,改掉表名不一致的那行,就能复现并验证报错链路。
{
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),但原样提取这一步必须先做,否则回测里按用户名分组的逻辑会全部失效。外汇与贵金属数据回测涉及真实品种波动,实操前请认清杠杆交易的高风险。
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 处理。
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 掩盖写入失败。
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出现在第几行——若它前面没有任何具体过滤,说明这层判断其实形同虚设,小布知识库建议至少补一个价格行为条件再放行。
class="kw">return true; }
◍ 批量写库与结构打印的冷门接口
executemany 适合把一整块同质矩阵一次性塞进表。VALUES 后的问号个数必须和 params 矩阵的列数相等,否则运行直接报错。注意 MQL5 的 matrix 限制:同一矩阵只能装同类型变量,所以不能在一次 executemany 里混插字符串列和双精度列。 executescript 用来一口气跑多条以分号分隔的 SQL,比如建表加插日志。它不解析参数、不返回结果集,且会自动提交未决事务,多数情况下你不必再显式调 commit。 想看表结构或查询结果,不必自己拼循环。CSqlite3 里的 print_table 封装了内置 DatabasePrint,往专家日志里直接打印行列。下面这段代码把三种用法都跑了一遍,开 MT5 挂个脚本就能看到 EURUSD 表写入了 3 行、logs 表自动建好并插了记录。 外汇与贵金属数据落地涉及实盘高风险,回测库和实盘库建议物理隔离,避免脚本误写生产表。
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 附近抓到这批行号连续的插入记录。
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 字段对净值影响不可忽略,落库后建议单独拉出来看。
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 成功,否则高风险场景下易丢数据。
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) 长度适配你的品种命名,就能本地跑一套成交归档。
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 跑一遍就能看出差别。
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 里跑通验证。 外汇与贵金属市场高杠杆、高波动,任何本地数据库封装只解决工程效率,不替你过滤交易风险;用脚本回测时先把点差和滑点参数写实,再谈策略可信度。 模块写完了,接下来就是在终端里改几行查询、看返回对不对——熟悉感有了,坑也得自己踩一遍。