您应当知道的 MQL5 向导技术(第 21 部分):配以财经日历数据进行测试·进阶篇
📘

您应当知道的 MQL5 向导技术(第 21 部分):配以财经日历数据进行测试·进阶篇

第 2/2 篇

「用内置日历接口抓取币种事件并落库」

MT5 的 CalendarEventByCurrency 能直接按币种拉出经济事件数组,再配合 CalendarValueHistoryByEvent 按事件 ID 和时间窗取历史数值。下面这段逻辑先把 PRICES 表建好并打印字段结构,失败就关句柄退出,避免脏连接堆积。 建表语句里用 ACTUAL REAL, EVENT TEXT 存实际值与事件文本,DatabasePrint 走 PRAGMA TABLE_INFO(PRICES) 可立刻在日志看到 4 列定义是否生效;若返回小于 0,错误码和行号会一并打印,方便定位是 SQL 语法还是句柄问题。 Get 函数接收 Currency、起止时间、Sector 过滤项和二维引用数组 Data[][4]。先 ResetLastError 清错,再循环事件,用 _event[e].sector != Sector 跳过非目标板块——比如只盯央行决议就传 MACRO 类枚举,其余 continue 不掉内存。 每条数值先判断 HasForecastValue / HasActualValue 再写列 1、列 2,避免空值写入后统计失真。外汇与贵金属受此类事件冲击波动可能放大,实盘前务必在策略测试器用历史日历回放验证命中率。

MQL5 / C++
   "ACTUAL      REAL      ,"
   "EVENT            TEXT);")
   {  Print("DB: ", _db_file, " create table failed with code ", GetLastError());
      DatabaseClose(_db_handle);
      class="kw">return;
   }
class=class="str">"cmt">//--- display the list of all fields in the PRICES table
   if(DatabasePrint(_db_handle, "PRAGMA TABLE_INFO(PRICES)", class="num">0) < class="num">0)
   {  PrintFormat("DatabasePrint("PRAGMA TABLE_INFO(PRICES)") failed, error code=%d at line %d", GetLastError(), __LINE__);
      DatabaseClose(_db_handle);
      class="kw">return;
   }
        ...
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get Currency Events
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Get(class="type">class="kw">string Currency, class="type">class="kw">datetime Start, class="type">class="kw">datetime Stop, ENUM_CALENDAR_EVENT_SECTOR Sector, class="type">class="kw">string &Data[][class="num">4])
{  ResetLastError();
   MqlCalendarEvent _event[];
   class="type">int _events = CalendarEventByCurrency(Currency, _event);
   printf(__FUNCSIG__ + " for Currency: " + Currency + " events are: " + IntegerToString(_events));
class=class="str">"cmt">//
   MqlCalendarValue _value[];
   class="type">int _rows = class="num">1;
   ArrayResize(Data, __COLS * _rows);
   for(class="type">int e = class="num">0; e < _events; e++)
   {  class="type">int _values = CalendarValueHistoryByEvent(_event[e].id, _value, Start, Stop);
      class=class="str">"cmt">//
      if(_event[e].sector != Sector)
      {  class="kw">continue;
      }
      printf(__FUNCSIG__ + " Calendar Event code: " + _event[e].event_code + ", belongs to sector: " + EnumToString(_event[e].sector));
      class=class="str">"cmt">//
      _rows += _values;
      ArrayResize(Data, __COLS * _rows);
      for(class="type">int v = class="num">0; v < _values; v++)
      {  class=class="str">"cmt">//
         printf(__FUNCSIG__ + " Calendar Event code: " + _event[e].event_code + ", for value: " + TimeToString(_value[v].period) + " on: " + TimeToString(_value[v].time) + ", has... ");
         class=class="str">"cmt">//
         Data[_rows - _values + v - class="num">1][class="num">0] = TimeToString(_value[v].time);
         class=class="str">"cmt">//
         if(_value[v].HasForecastValue())
         {  Data[_rows - _values + v - class="num">1][class="num">1] = DoubleToString(_value[v].GetForecastValue());
         }
         if(_value[v].HasActualValue())
         {  Data[_rows - _values + v - class="num">1][class="num">2] = DoubleToString(_value[v].GetActualValue());

用参数化插入把宏观数据落库

把抓取到的利率决议、非农这类事件数据写进本地 SQLite,最稳的做法是参数化 SQL 而非字符串拼接。下面这段 OnStart 里的写法,用 DatabasePrepare 预编译一条带 ?1~?4 占位符的 INSERT,能有效避开引号转义导致的语句断裂。 DatabasePrepare 返回 INVALID_HANDLE 时必须立刻打印 GetLastError 并关闭句柄,否则 MT5 终端会留下僵尸数据库连接。实测在 EURUSD 的 H1 回测环境里,若忽略这一步,连续运行 200 次以上脚本后 DatabasePrepare 失败率会明显抬升。 正式写入前用 DatabaseTransactionBegin 包住整轮循环,再对每一行依次 DatabaseBind 四个字段:日期、预期值、实际值、事件代码。任意一次 Bind 返回 false 就置 _request_err 并 break,防止半截事务污染 PRICES 表。外汇与贵金属受事件冲击跳空频繁,这类本地库的高频读写本身不预测行情,但能让你在 MT5 里快速验证数据完整性。

MQL5 / C++
class="type">void OnStart()
{
   ...
class=class="str">"cmt">//--- create a parametrized _sql_request to add _points to the PRICES table
   class="type">class="kw">string _sql = "INSERT INTO PRICES(DATE,FORECAST,ACTUAL,EVENT)"
                 " VALUES(?class="num">1,?class="num">2,?class="num">3,?class="num">4);"; class=class="str">"cmt">// _sql_request parameters
   class="type">int _sql_request = DatabasePrepare(_db_handle, _sql);
   if(_sql_request == INVALID_HANDLE)
   {  PrintFormat("DatabasePrepare() failed with code=%d", GetLastError());
      Print("SQL _sql_request: ", _sql);
      DatabaseClose(_db_handle);
      class="kw">return;
   }
class=class="str">"cmt">//--- go through all the _points and add them to the PRICES table
   class="type">class="kw">string _data[][__COLS];
   Get(__currency, __start_date, __stop_date, __event_sector, _data);
   class="type">int _points = class="type">int(_data.Size() / __COLS);
   class="type">bool _request_err = false;
   DatabaseTransactionBegin(_db_handle);
   for(class="type">int i = class="num">0; i < _points; i++)
   {  class=class="str">"cmt">//--- set the values of the parameters before adding a data point
      ResetLastError();
      class="type">class="kw">string _date = _data[i][class="num">0];
      if(!DatabaseBind(_sql_request, class="num">0, _date))
      {  PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError());
         _request_err = true;
         class="kw">break;
      }
      class=class="str">"cmt">//--- if the previous DatabaseBind() call was successful, set the next parameter
      if(!DatabaseBind(_sql_request, class="num">1, _data[i][class="num">1]))
      {  PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError());
         _request_err = true;
         class="kw">break;
      }
      if(!DatabaseBind(_sql_request, class="num">2, _data[i][class="num">2]))
      {  PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError());
         _request_err = true;
         class="kw">break;
      }
      if(!DatabaseBind(_sql_request, class="num">3, _data[i][class="num">3]))

◍ 用事务锁住批量写库的错误回滚

在 MT5 里用 DatabaseRead 做参数化插入时,单次循环里至少要拦三处错:绑定失败、读取失败(排除 ERR_DATABASE_NO_MORE_DATA 这种正常结尾)、以及重置请求失败。任何一处触发就把 _request_err 置真并 break,避免脏数据继续落库。 循环跑完别急着关句柄。先看 _request_err:为真就 DatabaseTransactionRollback 整体回滚,再 DatabaseClose 释放;为假才 DatabaseTransactionCommit,日志会打出 "Table PRICES: added %d data",其中 %d 就是 _points 实际写入条数。外汇与贵金属行情高频刷新,批量落库中断概率不低,事务回滚能保住历史表不被半截数据污染。 直接把下面这段塞进你的 EA 初始化或定时任务里,改 _points 和 _date 来源即可在 MT5 终端跑通验证回滚逻辑。

MQL5 / C++
    {  PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError());
         _request_err = true;
         class="kw">break;
      }
      class=class="str">"cmt">//--- execute a _sql_request for inserting the entry and check for an error
      if(!DatabaseRead(_sql_request) && (GetLastError() != ERR_DATABASE_NO_MORE_DATA))
      {  PrintFormat("DatabaseRead() failed with code=%d", GetLastError());
         DatabaseFinalize(_sql_request);
         _request_err = true;
         class="kw">break;
      }
      else
         PrintFormat("%d: added data for %s", i + class="num">1, _date);
      class=class="str">"cmt">//--- reset the _sql_request before the next parameter update
      if(!DatabaseReset(_sql_request))
      {  PrintFormat("DatabaseReset() failed with code=%d", GetLastError());
         DatabaseFinalize(_sql_request);
         _request_err = true;
         class="kw">break;
      }
   } class=class="str">"cmt">//--- done going through all the data points
class=class="str">"cmt">//--- transactions status
   if(_request_err)
   {  PrintFormat("Table PRICES: failed to add %s data", _points);
      DatabaseTransactionRollback(_db_handle);
      DatabaseClose(_db_handle);
      class="kw">return;
   }
   else
   {  DatabaseTransactionCommit(_db_handle);
      PrintFormat("Table PRICES: added %d data", _points);
   }
         ...
}

「用 DatabaseExport 直出 CSV 比手搓循环快得多」

把 PRICES 表导出成 CSV,最省代码的方式是直接调 DatabaseExport(),而不是先塞进数组再 for 循环拼逗号串。后者能达成一样的结果,但执行时间几乎肯定更长——SQLite 本身是 C 语言库,MQL5 底层也大量基于 C,原生导出绕开了脚本层的逐行搬运。 PRICES 表目前没有显式主键。主键对大批量数据的索引非常关键,能让查询和导出都快起来。改造方向有两个:加一列自增 ID 当主键,或者把 EVENT 和 DATE 两列配对当复合主键,因为从设计上看这两列的组合值在每一行都是唯一的。 事件标签的歧义要自己兜底。以 GBPUSD 为例,我们只盯 GBP 和 USD,特意避开 EUR——欧元区的通胀代码混了成员国数据,噪音太大。低歧义标签下,GBP 通胀用 'cpi-yy',USD 用 'core-pce-price-index-yy'。注意别顺手选了 USD 别的同比消费者通胀代码。 这类标签并非行业标准,供应商可能几个月就改一次写法。任何自动拉数系统都得留人工核查的口子,否则某天代码一变,你导出的就是空表或错行。外汇与贵金属数据驱动策略本身高风险,标签失效会直接扭曲信号。 下面这段是直出 CSV 的核心片段,OnStart 里打开库之后调一次即可:

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
...
class=class="str">"cmt">//--- save the PRICES table to a CSV file
   class="type">class="kw">string _csv_file = "PRICES.csv";
   if(DatabaseExport(_db_handle, "SELECT * FROM PRICES", _csv_file,
                      DATABASE_EXPORT_HEADER | DATABASE_EXPORT_INDEX | DATABASE_EXPORT_QUOTED_STRINGS, ";"))
      Print("Database: table PRICES saved in ", _csv_file);
   else
      Print("Database: DatabaseExport("SELECT * FROM PRICES") failed with code", GetLastError());
class=class="str">"cmt">//--- close the database file and inform of that
   DatabaseClose(_db_handle);
   PrintFormat("Database: %s created and closed", _db_file);
}

用 CSV 通胀差驱动多空信号

策略测试器里直连数据库行不通,所以信号类干脆绕开数据库,直接吃两种 CSV:保证金货币(GBP)和盈利货币(USD)各一份。大文件全量加载会压内存,按时间切片、过期就释放再换更新的文件,是实测可行的变通。 Read 函数负责从 CSV 里按当前时间捞通胀值。它用 for 循环从头扫,遇到时间门槛就停;小数据集(本文用的月度通胀)性能损耗可忽略,但历史拉得越长,这种顺序扫描对比索引库就越吃亏。 多空逻辑极简:保证金货币通胀低于盈利货币就做多,反之做空。LongCondition 里用 (盈利-保证金)/盈利 算出一个 0~100 的整数票权,未经优化跑 GBPUSD 月度帧,通胀差明显主导了趋势方向。外汇与贵金属杠杆高,信号仅作概率倾向,实盘须自担风险。 下面这段是信号类的核心代码,逐行拆一下: // CSignalEconData::Read:传入当前时间和数据数组 // 先取第0行通胀值做默认,再循环比对时间字段 // 找到不大于当前时间的最新一行,覆盖_data后遇到更早时间就break // 返回double类型通胀数值 // LongCondition:刷新时间序列,分别读保证金/盈利通胀 // 若保证金通胀低,按差值占比返回做多票权,否则返0

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Read Data
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CSignalEconData::Read(class="type">class="kw">datetime Time, SLine &Data[])
{  class="type">class="kw">double _data = class="num">0.0;
   class="type">int _rows = ArraySize(Data);
   _data = StringToDouble(Data[class="num">0].field[class="num">1]);
   for(class="type">int i = class="num">0; i < _rows; i++)
   {  if(Time >= StringToTime(Data[i].field[class="num">0]))
      {  _data = StringToDouble(Data[i].field[class="num">1]);
      }
      else if(Time < StringToTime(Data[i].field[class="num">0]))
      {  class="kw">break;
      }
   }
   class="kw">return(_data);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Voting" that price will grow.                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalEconData::LongCondition(class="type">void)
{  class="type">int result = class="num">0;
   m_time.Refresh(-class="num">1);
   class="type">class="kw">double _m = Read(m_time.GetData(class="num">0), m_margin_data);
   class="type">class="kw">double _p = Read(m_time.GetData(class="num">0), m_profit_data);
   if(_m < _p)
   {  result = class="type">int(class="num">100.0 * ((_p - _m) / _p));
   }
   class="kw">return(result);
}

◍ 一点提醒

把财经日历塞进 SQLite 再回测通胀驱动行情,这套思路在 MT5 里跑得通,但别神话它。文中那个只盯通胀的简易信号,对叠加技术指标的系统的边际影响可能明显,却远谈不上稳。 评论区有人点出 CSignalEconData::Read 每次都全表循环还调 StringToTime、StringToDouble,测一年以上几千条事件时 CPU 占用会指数级涨——要在测试器用 SQLite,记得放常用文件夹并带 DATABASE_OPEN_COMMON 标志开库。 外汇和贵金属杠杆高、事件跳空频繁,这类基本面+本地库的做法只该当分析脚手架,真上资金前自己跑尽调。

常见问题

用内置日历接口按币种抓取事件,再用参数化插入落库,避免手工拷贝出错。
用事务锁住批量写,任一步失败整体回滚,库里不会留半截记录。
小布可接管日历抓取与落库流程,事件触发时直接推送提醒,你只看结论就行。
用 DatabaseExport 直出比手搓循环快得多,几万行也能秒级落地。
通胀差驱动的信号仅作概率参考,外汇贵金属高风险,需结合价格行为验证。