精通日志记录(第六部分):数据库日志存储方案·进阶篇
「MT5里怎么把数据库句柄攥在手里」
在MT5里做本地日志或信号存储,第一道坎就是打开数据库拿到句柄。核心函数是 DatabaseOpen(),它吃两个参数:相对 MQL5\Files 的路径,以及访问标识的组合。 常用标识里,DATABASE_OPEN_READWRITE 允许读写,DATABASE_OPEN_CREATE 在文件不存在时直接落盘新建,两者按位或就能省掉手动判存在的麻烦。还有 DATABASE_OPEN_MEMORY 走内存临时库、DATABASE_OPEN_COMMON 进终端共享目录,按场景挑。 DatabaseOpen() 返回的是一个 int 句柄,不是对象,后续所有表操作都靠它寻址。忘了 DatabaseClose() 关连接,MT5 退出时可能锁文件,下次打开易报句柄无效。 下面这段初始化函数演示了标准姿势:打开失败打错误码并 INIT_FAILED,成功则打印后关闭。实盘里建议把 dbHandle 存成全局变量,别在 OnInit 里关掉,不然后续 OnTick 写不进。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Opening a database connection class="type">int dbHandle = DatabaseOpen(path,DATABASE_OPEN_READWRITE|DATABASE_OPEN_CREATE); if(dbHandle == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Database error(Code: "+IntegerToString(GetLastError())+")"); class="kw">return(INIT_FAILED); } Print("Open database file"); class=class="str">"cmt">//--- Closing database after use DatabaseClose(handle_db); Print("Closed database file"); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 建表前先查存在性
在 MT5 里落库日志,别上来就 CREATE TABLE。先用 DatabaseTableExists() 探一下目标表是否已存在,避免重复建表抛错或覆盖旧数据。 SQL 就是给数据库下的指令集,建表只需一条 CREATE TABLE,后面跟表名和列定义:列名 + 数据类型(TEXT、BIGINT、DATETIME 等)。我们的 logs 表按 MqlLogifyModel 结构铺了 12 个字段,从自增 id 到代码行号 line 全覆盖。 下面这段 OnInit 可直接抄进 EA 验证:开库用 DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE|DATABASE_OPEN_CREATE),句柄为 INVALID_HANDLE 就 INIT_FAILED;表不存在才建。 建完去文件浏览器 Files 文件夹,能看见生成的 logs.sqlite。点开它 MetaEditor 原生支持,红色高亮的交互界面能跑任意 SQL,调试时极实用。外汇与贵金属策略跑这套日志有概率因频繁写盘拖慢回测,属高风险调试手段,按需开关。
CREATE TABLE {table_name} ({column_name} {type_data}, …);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
class=class="str">"cmt">//--- Open the database connection
class="type">int dbHandle = DatabaseOpen("db\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
if(dbHandle == INVALID_HANDLE)
{
Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database(Error Code: " + IntegerToString(GetLastError()) + ")");
class="kw">return(INIT_FAILED);
}
Print("[INFO] Database connection opened successfully");
class=class="str">"cmt">//--- Create the &class="macro">#x27;logs&class="macro">#x27; table if it does not exist
if(!DatabaseTableExists(dbHandle, "logs"))
{
DatabaseExecute(dbHandle,
"CREATE TABLE logs("
"id INTEGER PRIMARY KEY AUTOINCREMENT," class=class="str">"cmt">// Auto-incrementing unique ID
"formated TEXT," class=class="str">"cmt">// Formatted log message
"levelname TEXT," class=class="str">"cmt">// Log level(INFO, ERROR, etc.)
"msg TEXT," class=class="str">"cmt">// Main log message
"args TEXT," class=class="str">"cmt">// Additional details
"timestamp BIGINT," class=class="str">"cmt">// Log event timestamp(Unix time)
"date_time DATETIME," class=class="str">"cmt">// Human-readable date and time
"level BIGINT," class=class="str">"cmt">// Log level as an integer
"origin TEXT," class=class="str">"cmt">// Module or component name
"filename TEXT," class=class="str">"cmt">// Source file name
"function TEXT," class=class="str">"cmt">// Function where the log was recorded
"line BIGINT);"); class=class="str">"cmt">// Source code line number
Print("[INFO] &class="macro">#x27;logs&class="macro">#x27; table created successfully");
}
class=class="str">"cmt">//--- Close the database connection
DatabaseClose(dbHandle);
Print("[INFO] Database connection closed successfully");
class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+用占位符把日志写进 MT5 数据库
在 MT5 里做本地日志持久化,别直接拼 SQL 字符串。MQL5 给了一套预编译接口,先把语句编译成句柄,再用索引占位符绑值,能明显降低语法拼错和注入风险。 核心就四个函数:DatabasePrepare 拿查询句柄并预编译;DatabaseBind 按 ?1、?2 的索引把真实值绑进去;DatabaseRead 执行不返回结果集的指令(如 INSERT);DatabaseFinalize 用完释放,否则句柄堆积可能拖慢终端。 下面这段示例向 logs 表插一条 EURUSD 买单日志,id 列交给数据库自增,其余 11 个字段全用 ?1–?11 占位。绑值时索引从 0 起算,?1 对应 Bind 的 0 参数,顺序错一位就会写串行。 跑完 EA 后控制台会打印确认或错误;在 MetaEditor 的数据库视图里能直接看到这条 2025.02.13 18:24:00 的 INFO 记录。外汇与贵金属交易本身高风险,本地日志只辅助复盘,不构成任何方向判断。
INSERT INTO {table_name} ({column}, ...) VALUES({value}, ...)
INSERT INTO logs(formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) VALUES(?class="num">1, ?class="num">2, ?class="num">3, ?class="num">4, ?class="num">5, ?class="num">6, ?class="num">7, ?class="num">8, ?class="num">9, ?class="num">10, ?class="num">11);
class=class="str">"cmt">//--- Prepare SQL statement for inserting a log entry
class="type">class="kw">string sql = "INSERT INTO logs(formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) "
"VALUES(?class="num">1, ?class="num">2, ?class="num">3, ?class="num">4, ?class="num">5, ?class="num">6, ?class="num">7, ?class="num">8, ?class="num">9, ?class="num">10, ?class="num">11);";
class="type">int sqlRequest = DatabasePrepare(dbHandle, sql);
if(sqlRequest == INVALID_HANDLE)
{
Print("[ERROR] Failed to prepare SQL statement for log insertion");
}
class=class="str">"cmt">//--- Bind values to the SQL statement
DatabaseBind(sqlRequest, class="num">0, "class="num">06:class="num">24:class="num">00 [INFO] Buy order sent successfully"); class=class="str">"cmt">// Formatted log message
DatabaseBind(sqlRequest, class="num">1, "INFO"); class=class="str">"cmt">// Log level name
DatabaseBind(sqlRequest, class="num">2, "Buy order sent successfully"); class=class="str">"cmt">// Main log message
DatabaseBind(sqlRequest, class="num">3, "Symbol: EURUSD, Volume: class="num">0.1"); class=class="str">"cmt">// Additional details
DatabaseBind(sqlRequest, class="num">4, class="num">1739471040); class=class="str">"cmt">// Unix timestamp
DatabaseBind(sqlRequest, class="num">5, "class="num">2025.02.class="num">13 class="num">18:class="num">24:class="num">00"); class=class="str">"cmt">// Readable date and time
DatabaseBind(sqlRequest, class="num">6, class="num">1); class=class="str">"cmt">// Log level as integer
DatabaseBind(sqlRequest, class="num">7, "Order Management"); class=class="str">"cmt">// Module or component name
DatabaseBind(sqlRequest, class="num">8, "File.mq5"); class=class="str">"cmt">// Source file name
DatabaseBind(sqlRequest, class="num">9, "OnInit"); class=class="str">"cmt">// Function name「在 OnInit 里把日志库和表先建起来」
EA 启动阶段就要把 SQLite 连接和日志表准备好,否则后续写盘会直接失败。下面这段放在 OnInit 里,先尝试打开 db\logs.sqlite,若文件不存在则按读写的标志位顺手建出来;句柄拿到 INVALID_HANDLE 就打印带当前时间的错误并回 INIT_FAILED。 表结构用 DatabaseExecute 跑一段 CREATE TABLE,字段覆盖了格式化消息、日志级别名与整数级别、Unix 时间戳、可读时间、来源模块、文件名、函数名和行号,共 11 个业务列加一个自增 id。实测在 MT5 终端的 MQL5\Files\db 目录下生成 sqlite 文件,首次运行建表后 Print 会吐出 'logs' table created successfully。 插日志用预编译语句更稳:DatabasePrepare 吃一段带 ?1 到 ?11 占位符的 INSERT,失败同样走错误打印。占位符顺序和前面建表的列顺序一一对应,后面绑定参数时数错位置就会写串列。
class="type">int OnInit() { class=class="str">"cmt">//--- Open the database connection class="type">int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(dbHandle == INVALID_HANDLE) { Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database(Error Code: " + IntegerToString(GetLastError()) + ")"); class="kw">return(INIT_FAILED); } Print("[INFO] Database connection opened successfully"); class=class="str">"cmt">//--- Create the &class="macro">#x27;logs&class="macro">#x27; table if it does not exist if(!DatabaseTableExists(dbHandle, "logs")) { DatabaseExecute(dbHandle, "CREATE TABLE logs(" "id INTEGER PRIMARY KEY AUTOINCREMENT," class=class="str">"cmt">// Auto-incrementing unique ID "formated TEXT," class=class="str">"cmt">// Formatted log message "levelname TEXT," class=class="str">"cmt">// Log level(INFO, ERROR, etc.) "msg TEXT," class=class="str">"cmt">// Main log message "args TEXT," class=class="str">"cmt">// Additional details "timestamp BIGINT," class=class="str">"cmt">// Log event timestamp(Unix time) "date_time DATETIME," class=class="str">"cmt">// Human-readable date and time "level BIGINT," class=class="str">"cmt">// Log level as an integer "origin TEXT," class=class="str">"cmt">// Module or component name "filename TEXT," class=class="str">"cmt">// Source file name "function TEXT," class=class="str">"cmt">// Function where the log was recorded "line BIGINT);"); class=class="str">"cmt">// Source code line number Print("[INFO] &class="macro">#x27;logs&class="macro">#x27; table created successfully"); } class=class="str">"cmt">//--- Prepare SQL statement for inserting a log entry class="type">class="kw">string sql = "INSERT INTO logs(formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) " "VALUES(?class="num">1, ?class="num">2, ?class="num">3, ?class="num">4, ?class="num">5, ?class="num">6, ?class="num">7, ?class="num">8, ?class="num">9, ?class="num">10, ?class="num">11);"; class="type">int sqlRequest = DatabasePrepare(dbHandle, sql); if(sqlRequest == INVALID_HANDLE) { Print("[ERROR] Failed to prepare SQL statement for log insertion"); } class=class="str">"cmt">//--- Bind values to the SQL statement
◍ 把一条交易日志拆成结构化字段写进库
MT5 里用 DatabaseBind 往 SQL 请求绑参时,最好别把整条日志当成一个字符串硬塞。上面这段把一笔 EURUSD 0.1 手买单的日志拆成 11 个字段:时间戳给到 Unix 值 1739471040,同时存可读的 2025.02.13 18:24:00,日志级别既存字符串 "INFO" 也存整数 1,方便后续按等级过滤。 模块名、源文件、函数名和行号分别绑到 "Order Management"、"File.mq5"、"OnInit"、100,这种粒度在回查某次异常下单时比全文检索快得多。外汇与贵金属杠杆高,这类结构化落库能帮你定位滑点发生在哪个环节,但只是辅助,不预示任何盈亏方向。 绑定完用 DatabaseRead 执行,失败就 Print 报错,成功则关连接返回 INIT_SUCCEEDED。终端里会依次吐出建表、插入、关库的成功信息,开 MT5 跑一遍就能验证字段是否落对。
DatabaseBind(sqlRequest, class="num">0, "class="num">06:class="num">24:class="num">00 [INFO] Buy order sent successfully"); class=class="str">"cmt">// Formatted log message DatabaseBind(sqlRequest, class="num">1, "INFO"); class=class="str">"cmt">// Log level name DatabaseBind(sqlRequest, class="num">2, "Buy order sent successfully"); class=class="str">"cmt">// Main log message DatabaseBind(sqlRequest, class="num">3, "Symbol: EURUSD, Volume: class="num">0.1"); class=class="str">"cmt">// Additional details DatabaseBind(sqlRequest, class="num">4, class="num">1739471040); class=class="str">"cmt">// Unix timestamp DatabaseBind(sqlRequest, class="num">5, "class="num">2025.02.class="num">13 class="num">18:class="num">24:class="num">00"); class=class="str">"cmt">// Readable date and time DatabaseBind(sqlRequest, class="num">6, class="num">1); class=class="str">"cmt">// Log level as integer DatabaseBind(sqlRequest, class="num">7, "Order Management"); class=class="str">"cmt">// Module or component name DatabaseBind(sqlRequest, class="num">8, "File.mq5"); class=class="str">"cmt">// Source file name DatabaseBind(sqlRequest, class="num">9, "OnInit"); class=class="str">"cmt">// Function name DatabaseBind(sqlRequest, class="num">10, class="num">100); class=class="str">"cmt">// Line number class=class="str">"cmt">//--- Execute the SQL statement if(!DatabaseRead(sqlRequest)) { Print("[ERROR] SQL insertion request failed"); } else { Print("[INFO] Log entry inserted successfully"); } class=class="str">"cmt">//--- Close the database connection DatabaseClose(dbHandle); Print("[INFO] Database connection closed successfully"); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ [INFO] Database file opened successfully [INFO] Table &class="macro">#x27;logs&class="macro">#x27; created successfully [INFO] Log entry inserted successfully [INFO] Database file closed successfully
把日志表从 sqlite 里捞出来
在 MT5 里读库和写库前半段一模一样:先用 DatabasePrepare() 拿查询句柄,再靠 DatabaseRead() 把游标顶到结果集首行。之后按列类型调对应的取数函数——文本走 DatabaseColumnText(),整型走 DatabaseColumnInteger(),长整型用 DatabaseColumnLong(),浮点用 DatabaseColumnDouble(),二进制块用 DatabaseColumnBlob() 以数组形式返回。 要拉 logs 表全部记录,SQL 就是一句 SELECT * FROM logs,不需要任何过滤条件。下面这段 OnInit 里先开 db\\logs.sqlite(读写并允许新建),表不存在就建;建表语句里 id 设成 INTEGER PRIMARY KEY AUTOINCREMENT,timestamp 用 BIGINT 存 Unix 毫秒级时间,date_time 用 DATETIME 留可读字符串,方便后续按两种口径检索。 跑完这套,日志库就处于可读状态,后面接数据库处理器时直接复用 dbHandle 即可,不用每次重连。外汇与贵金属 EA 接 sqlite 做本地日志有断网不丢数据的好处,但 sqlite 文件锁在多终端同时写时可能冲突,属高风险配置项,实盘前请在模拟环境压一遍并发。
SELECT * FROM logs class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Open the database connection class="type">int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(dbHandle == INVALID_HANDLE) { Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database(Error Code: " + IntegerToString(GetLastError()) + ")"); class="kw">return INIT_FAILED; } Print("[INFO] Database connection opened successfully."); class=class="str">"cmt">//--- Create the &class="macro">#x27;logs&class="macro">#x27; table if it doesn&class="macro">#x27;t exist if(!DatabaseTableExists(dbHandle, "logs")) { class="type">class="kw">string createTableSQL = "CREATE TABLE logs(" "id INTEGER PRIMARY KEY AUTOINCREMENT," class=class="str">"cmt">// Auto-incrementing unique ID "formated TEXT," class=class="str">"cmt">// Formatted log message "levelname TEXT," class=class="str">"cmt">// Log level name(INFO, ERROR, etc.) "msg TEXT," class=class="str">"cmt">// Main log message "args TEXT," class=class="str">"cmt">// Additional arguments/details "timestamp BIGINT," class=class="str">"cmt">// Timestamp of the log event "date_time DATETIME," class=class="str">"cmt">// Human-readable date and time "level BIGINT," class=class="str">"cmt">// Log level as an integer "origin TEXT," class=class="str">"cmt">// Module or component name "filename TEXT," class=class="str">"cmt">// Source file name