您应当知道的 MQL5 向导技术(第 21 部分):配以财经日历数据进行测试·进阶篇
「用内置日历接口抓取币种事件并落库」
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,避免空值写入后统计失真。外汇与贵金属受此类事件冲击波动可能放大,实盘前务必在策略测试器用历史日历回放验证命中率。
"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 里快速验证数据完整性。
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 终端跑通验证回滚逻辑。
{ 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 里打开库之后调一次即可:
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
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 标志开库。 外汇和贵金属杠杆高、事件跳空频繁,这类基本面+本地库的做法只该当分析脚手架,真上资金前自己跑尽调。