精通日志记录(第六部分):数据库日志存储方案·进阶篇
📘

精通日志记录(第六部分):数据库日志存储方案·进阶篇

第 2/3 篇

「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 写不进。

MQL5 / C++
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,调试时极实用。外汇与贵金属策略跑这套日志有概率因频繁写盘拖慢回测,属高风险调试手段,按需开关。

MQL5 / C++
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 记录。外汇与贵金属交易本身高风险,本地日志只辅助复盘,不构成任何方向判断。

MQL5 / C++
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,失败同样走错误打印。占位符顺序和前面建表的列顺序一一对应,后面绑定参数时数错位置就会写串列。

MQL5 / C++
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 跑一遍就能验证字段是否落对。

MQL5 / C++
  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 文件锁在多终端同时写时可能冲突,属高风险配置项,实盘前请在模拟环境压一遍并发。

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

常见问题

把数据库句柄存在全局变量里,在OnInit里初始化并建库建表,后续写日志直接复用该句柄,避免反复打开关闭。
建表前先执行查询系统表判断表是否存在,存在就跳过建表,不存在再跑CREATE TABLE,稳妥不冲突。
小布可接管结构化日志落地,你只需定义字段,它把每笔交易拆好写库,你专注看盘决策。
占位符隔离SQL与参数,避免报价单引号破坏语句,也防注入,写多字段日志更省心。
用SELECT从sqlite日志表捞全量数据,可转CSV或用查看工具翻历史,按时间字段排就行。