SQLite: MQL5 原生 SQL 数据库操纵·进阶篇
🗄️

SQLite: MQL5 原生 SQL 数据库操纵·进阶篇

(2/3)· 从 MetaEditor 调试到事务批量提交,6 个被忽略的 SQLite 提速与落地细节

进阶 第 2/3 篇

不少交易者把 SQLite 当成本地 CSV 的替代品,却在 EA 里逐条 INSERT 把回测结果写库,单次十万行优化数据能卡掉半分钟。更隐蔽的问题是,手写 SQL 字符串拼错只能在运行时崩,MetaEditor 里根本看不到哪列 bind 丢了。接上篇铺垫的建库与基础读写,这一篇直接进工程层。

◍ 用 MetaEditor 直连 sqlite 验证查询再写代码

在 MT5 里跑数据库相关逻辑,出错时所有操纵函数都会返回错误代码。四条底线先记牢:DatabaseFinalize() 销毁查询控柄后才能算干净;收工前必须用 DatabaseClose() 关库;每次执行都要查结果;一旦报错,先销毁查询再关库,顺序反了容易句柄泄漏。 最麻烦的是查询还没建起来时,你根本不知道错在哪。MetaEditor 本身能直接开 *.sqlite 文件做交互式 SQL,不用先写代码。以终端公用文件夹里的 company.sqlite 为例:双击导航器里的 COMPANY 表,状态栏会自动拼出 “SELECT * FROM COMPANY”,按 F9 或点执行就能跑,结果和错误都落在编辑器日志里。 先在编辑器把统计类查询(比如求和、平均)跑通,再搬进 MQL5 才稳。下面这段就是求和与平均工资的实跑代码,回测打印出来的硬数据是:Total salary=125000.0,Average salary=31250.0。 别把正态当圣经 编辑器里能跑通不代表 EA 里一定顺。DatabasePrepare 返回 INVALID_HANDLE 时,先打 GetLastError() 看码,再关库 return,别硬往下读。

MQL5 / C++
  Print("Some statistics:");
class=class="str">"cmt">//--- prepare a new query about the sum of salaries
  request=DatabasePrepare(db, "SELECT SUM(SALARY) FROM COMPANY");
  if(request==INVALID_HANDLE)
    {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      DatabaseClose(db);
      class="kw">return;
    }
  class="kw">while(DatabaseRead(request))
    {
      class="type">class="kw">double total_salary;
      DatabaseColumnDouble(request, class="num">0, total_salary);
      Print("Total salary=", total_salary);
    }
class=class="str">"cmt">//--- remove the query after use
  DatabaseFinalize(request);
 
class=class="str">"cmt">//--- prepare a new query about the average salary
  request=DatabasePrepare(db, "SELECT AVG(SALARY) FROM COMPANY");
  if(request==INVALID_HANDLE)
    {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      ResetLastError();
      DatabaseClose(db);
      class="kw">return;
    }
  class="kw">while(DatabaseRead(request))
    {
      class="type">class="kw">double aver_salary;
      DatabaseColumnDouble(request, class="num">0, aver_salary);
      Print("Average salary=", aver_salary);
    }
class=class="str">"cmt">//--- remove the query after use
  DatabaseFinalize(request);

「用 DatabaseReadBind 把整行塞进结构」

逐列调用 DatabaseColumnInteger、DatabaseColumnText 这类函数虽然能统一处理任意查询结果,但代码量会迅速膨胀。当查询返回表的字段顺序和类型提前确定时,这种逐列读取方式性价比很低。 DatabaseReadBind() 的思路是直接把当前记录整体映射到事先声明好的结构里。下面这段声明了 Person 结构,字段依次是 id(int)、name(string)、age(int)、address(string)、salary(double),字段排列必须和 SQL 结果集的列顺序严格对应。 调用处用一个 for 循环配合 DatabaseReadBind(request, person) 作为条件,每次迭代自动把下一行读进 person,随后直接打印各成员即可,不再出现任何 ColumnXXX 调用。相比逐列读取,行数大约能砍掉一半,且字段访问变成了 person.salary 这种直观写法。 实际在 MT5 里跑时,若结果集某列类型与结构成员不匹配,绑定会失败并返回 false,循环自然终止。外汇与贵金属相关的本地数据缓存用这套逻辑处理批量历史报价时,注意 SQLite 返回 double 的精度问题,高风险品种 Tick 级数据建议先小批量验证。

MQL5 / C++
class="kw">struct Person
  {
   class="type">int                id;
   class="type">class="kw">string             name;
   class="type">int                age;
   class="type">class="kw">string             address;
   class="type">class="kw">double             salary;
  };
class=class="str">"cmt">//--- display obtained query results
   Person person;
   Print("Persons with salary > class="num">15000:");
   for(class="type">int i=class="num">0; DatabaseReadBind(request, person); i++)
      Print(i, ":  ", person.id, " ", person.name, " ", person.age, " ", person.address, " ", person.salary);
class=class="str">"cmt">//--- remove the query after use
   DatabaseFinalize(request);

用事务锁把成交表批量写入压到极限

往 SQLite 里灌历史成交时,逐条 INSERT 会反复抢锁、刷盘,延迟随行数线性膨胀。把整批写操作包进 DatabaseTransactionBegin 和 DatabaseTransactionCommit 之间,数据库只在首尾各锁一次、落盘一次,中间命令纯内存排队。 官方示例里这套写法能把批量表操作提速数百倍,量级差异在万行以上成交回放时最明显。出错就调 DatabaseTransactionRollback 整批回退,比单行容错干净得多。 下面这段是从历史库捞成交并批量落表的骨架,注意高亮那行才是提速开关: [CODE] //--- auxiliary variables ulong deal_ticket; // deal ticket long order_ticket; // a ticket of an order a deal was executed by long position_ticket; // ID of a position a deal belongs to datetime time; // deal execution time long type ; // deal type long entry ; // deal direction string symbol; // a symbol a deal was executed for double volume; // operation volume double price; // price double profit; // financial result double swap; // swap double commission; // commission long magic; // Magic number (Expert Advisor ID) long reason; // deal execution reason or source //--- go through all deals and add them to the database bool failed=false; int deals=HistoryDealsTotal(); // --- lock the database before executing transactions DatabaseTransactionBegin(database); for(int i=0; i<deals; i++) { deal_ticket= HistoryDealGetTicket(i); order_ticket= HistoryDealGetInteger(deal_ticket, DEAL_ORDER); position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); time= (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); [/CODE] 逐行拆一下:前面 14 行只是声明成交字段容器,deal_ticket 到 reason 覆盖票号、订单号、持仓 ID、时间、类型、方向、品种、量、价、利、息、费、魔法码、来源。 HistoryDealsTotal 取总数,DatabaseTransactionBegin(database) 才是关键——此后 for 循环里所有 HistoryDealGet* 读取加后续 INSERT 都不再单条锁库,直到你显式 Commit。外汇与贵金属行情跳空多、成交密集,回测落表不开事务可能卡到超时,开事务后写入耗时倾向下降 1~2 个数量级。

MQL5 / C++
class=class="str">"cmt">//--- auxiliary variables
  class="type">class="kw">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=class="kw">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);

◍ 用事务批写把成交入库提速五百倍

把历史成交逐笔写进本地 SQLite,若不用事务包裹,2737 笔成交的写入耗时会冲到 25818.9 毫秒,基本卡死主线程。 套上 DatabaseTransactionBegin / Commit 后,同一批数据只花 48.5 毫秒,加速比 532.8 倍。外汇与贵金属交易回测数据量大,这种延迟差异会直接拖垮 EA 的盘后统计。 下面这段是核心落库循环里的关键片段:先取每笔成交的佣金、魔术码、触发原因,再拼 SQL 插入,失败就回滚。 commission= HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); magic= HistoryDealGetInteger(deal_ticket, DEAL_MAGIC); reason= HistoryDealGetInteger(deal_ticket, DEAL_REASON); //--- add each deal to the table using the following query 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, '%s', %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; break; } } //--- check for transaction execution errors if(failed) { //--- roll back all transactions and unlock the database DatabaseTransactionRollback(database); PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError()); return(false); } //--- all transactions have been performed successfully - record changes and unlock the database DatabaseTransactionCommit(database); 逐行看:前 three 行用 HistoryDealGetDouble / Integer 抓佣金、magic、reason 三个字段;StringFormat 拼出带 14 个列的 INSERT 语句,%G 对应浮点成交量与价格;DatabaseExecute 执行失败就打印错误并置 failed,循环 break。 外层 failed 判断里调 DatabaseTransactionRollback 整体回滚,成功路径上 DatabaseTransactionCommit 一次性落盘。开 MT5 把这段塞进你的历史扫描 EA,对比有无事务包裹的 Print 耗时即可验证。

MQL5 / C++
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 class="kw">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(class="kw">false);
   }
class=class="str">"cmt">//--- all transactions have been performed successfully - record changes and unlock the database
 DatabaseTransactionCommit(database);

「用一条 SQL 把成交拼成完整交易」

SQL 查询的实用价值在于,不用写循环遍历代码,就能直接对源数据排序、筛选和重组。在 MT5 的历史成交表里,开仓与平仓成交靠同一个 position_id 关联,对冲账户上这种成对结构尤其干净,一次 INNER JOIN 就能把两笔 deal 并成一条交易记录。 下面这段脚本先判断 DEALS 表存在,再执行一条 INSERT INTO … SELECT 把入场(entry=0)和出场(entry=1)按 position_id 自连接,抽取时间、品种、成交量、双边手续费等字段写进 TRADES 表。比起早年手搓 for 循环比对成交,单查询的耗时通常能压到微秒级。 实测某账户历史成交 2741 笔,脚本跑完首 10 条记录即呈现 ticket、position_ticket、time、type、entry、symbol、volume、price、profit 等字段。把附件 trades.sqlite 在 MetaEditor 打开,和你终端历史里的仓位逐条比对,就能验证配对是否漏单或错配。 开 MT5 挂上这段逻辑前,注意外汇和贵金属杠杆交易本身高风险,历史成交配对只反映已平仓结果,不代表任何未来收益倾向。

MQL5 / C++
class=class="str">"cmt">//--- fill in the TRADES table class="kw">using an SQL query based on DEALS table data
  class="type">class="kw">ulong start=GetMicrosecondCount();
  if(DatabaseTableExists(db, "DEALS"))
  {
    class=class="str">"cmt">//--- fill in the TRADES table
      if(!DatabaseExecute(db, "INSERT INTO TRADES(TIME_IN,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) "
                          "SELECT "
                          "  d1.time as time_in,"
                          "  d1.position_id as ticket,"
                          "  d1.type as type,"
                          "  d1.volume as volume,"
                          "  d1.symbol as symbol,"
                          "  d1.price as price_in,"
                          "  d2.time as time_out,"
                          "  d2.price as price_out,"
                          "  d1.commission+d2.commission as commission,"
                          "  d2.swap as swap,"
                          "  d2.profit as profit "
                          "FROM DEALS d1 "
                          "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id "
                          "WHERE d1.entry=class="num">0 AND d2.entry=class="num">1"))
      {
        Print("DB: fillng the TRADES table failed with code ", GetLastError());
        class="kw">return;
      }
  }
  class="type">class="kw">ulong transaction_time=GetMicrosecondCount()-start;

从成交明细看多品种同频挂单

上面这组 MT5 历史订单_dump 记录了 2019.09.05 22:39 到次日 08:00 的六笔动作,首行是账户初始化(余额 2000.00000,无品种、无仓位),其后五笔全是 0.10 手的标准化建仓。 细看时间戳:06:00:03 同时打出 USDCAD 多单(1.32320)与 USDCHF 空单(0.98697),07:00:00 又同步落 EURUSD 空(1.10348)和 AUDUSD 多(0.68203),08:00 再加一笔 USDCHF 空(0.98701)。这种整点批量触发,倾向是某个多品种网格或相关性对冲 EA 在按小时节拍扫单。 每笔 commission 固定在 -0.11 到 -0.18 之间、magic 统一为 3,说明不是手动操作。外汇与贵金属杠杆交易高风险,这类同频挂单在美元指数单边时会让相关性头寸同时浮亏,复制前建议先在策略测试器用 2019.09 这段数据跑一遍。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到历史成交按策略分组的 SQL 视图,你不必自己从头拼 DatabaseReadBind 结构映射。

常见问题

可用内置的 SQL 调试面板单独跑查询,不必每次编译 EA 才看到报错,能快速确认 SELECT 的字段名与表结构是否匹配。
结构成员顺序需与 SELECT 列序一致,且 MQL5 基础类型要和表列亲和类型对应,否则读出的值是空或截断,建议在调试阶段先打印行数。
单条提交每次都要刷盘并写日志,批量事务只在中途保持内存缓冲,一次落盘,对于优化结果落库这种写多读少场景收益最明显。
目前小布内置的是平台侧聚合视图,若你用自己的 EA 写库,可把表结构对齐到标准成交字段,后续版本可能开放本地库接入。
在成交时间列建单列索引即可覆盖大多数区间扫描,避免在高频写入表上堆过多复合索引,写入性能会明显退化。