您应当知道的 MQL5 向导技术(第 21 部分):配以财经日历数据进行测试(基础篇)
◍ 用财经日历给 EA 做历史回测
在 MT5 里跑 EA 回测,默认是不带新闻冲击的,这让很多策略在样本外表现失真。MQL5 向导生成的框架若想接财经日历,核心是把外部 CSV 或内置宏事件序列注入 OnTick 前的预处理环节。
具体做法是在专家顾问初始化阶段用 FileOpen 读取日历文件,把事件时间、影响等级存进一个结构体数组;回测时若当前 tick 落在事件前后 N 分钟窗口内,就令风控模块降仓或禁开新单。这样测出来的权益曲线,比裸价格回测更接近实盘受数据行情干扰的情形。
需要提醒的是,外汇与贵金属属高杠杆品种,新闻行情滑点可能瞬间放大数倍,即便日历过滤也只「可能」降低尾部风险,而非消除。开 MT5 把任意向导 EA 接一份 2024 年非农日历 CSV,先跑一段 EURUSD M15,对比有无日历约束的回撤,就能验证这套思路。
用 SQLite 把财经日历塞进 EA 测试
技术面指标看的是价格和成交量,但基本面新闻往往才是波动的真正触发器。通胀率、央行利率、失业率这些宏观数据点,在发布瞬间造成的跳空和滑点,比多数技术指标更猛。最典型的例子是美国非农就业数据,几乎每月第一个周五出炉,行情波动经常远超普通交易日。 很多关键新闻没被散户盯上,长期被忽略,反而可能藏着还没被榨干的信息优势。把这类数据接进由 MQL5 向导拼出来的智能系统,能在回测里验证“新闻前离场 / 新闻后跟随”这类思路是否真的有用。 SQLite 数据库可以直接在 MetaEditor 里建。它本质是个本地仓库,能存财经日历,充当 EA 的数据源甚至指标缓冲区。本地存档意味着断网也能跑离线测试;当新闻源损坏、或老数据随时间失效时,你手里还有一份可控备份。本文就落地一件事:怎么用 SQLite 归档财经日历,让向导生成的 EA 拿它发信号。 外汇和贵金属受新闻冲击极快,这类策略实测前务必用历史数据充分回测,真实账户高风险。
「测试器读库卡点与 CSV 迂回」
在 MT5 策略测试器里直接读财经日历数据库,目前仍是个卡点。我实测用上面的 CSignalEconData::Read 在测试器中跑,会抛出错误 5601,提示要访问的数据表不存在;但同一段 SQL 在 MetaEditor 的数据库 IDE 或挂图脚本里都能正常回值。要么是测试器需要补额外代码,要么就是环境层面不允许访问库——服务台机器人给不出有用答复。 变通思路是把库里的财经数据导出成 CSV,让测试器改读文件。但这只是权宜,CSV 远不能替代数据库。数据库靠主键外键做完整性约束,CSV 完全没有;面对大数据,数据库的索引检索效率远高于 CSV 的线性扫描,后者在万行以上就会明显变慢。 并发、安全、备份也都是数据库的强项。多用户实时读写、角色权限与加密、自动恢复工具,CSV 默认一概不提供;SQL 的复杂并表查询也得靠第三方脚本在 CSV 上硬凑。还有规范化减少冗余、版本控制做审计追踪,CSV 扁平结构天生冗余且更新易损坏。做长周期财经研究,这些差异会直接拖垮策略验证质量。 下面这段代码是原本走数据库的方案,可看清字段结构和取数逻辑。测试器跑不通时,再考虑把同样查询改写成读 CSV 的版本。
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">string DB, class="type">class="kw">datetime Time, class="type">class="kw">string Event) { class="type">class="kw">double _data = class="num">0.0; class=class="str">"cmt">//--- create or open the database in the common terminal folder ResetLastError(); class="type">int _db_handle = DatabaseOpen(DB, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(_db_handle == INVALID_HANDLE) { Print("DB: ", DB, " open failed with err: ", GetLastError()); class="kw">return(_data); } class="type">class="kw">string _sql = " SELECT ACTUAL " + " FROM " + " ( " + " SELECT ACTUAL " + " FROM PRICES " + " WHERE DATE <= &class="macro">#x27;" + TimeToString(Time) + "&class="macro">#x27; " + " AND EVENT = &class="macro">#x27;" + Event + "&class="macro">#x27; " + " ORDER BY DATE DESC " + " LIMIT class="num">1 " + " )"; class="type">int _request = DatabasePrepare(_db_handle, _sql); if(_request == INVALID_HANDLE) { Print("request failed with err: ", GetLastError()); DatabaseClose(_db_handle); class="kw">return(_data); } while(DatabaseRead(_request)) { class=class="str">"cmt">//--- read the values of each field from the obtained entry ResetLastError(); if(!DatabaseColumnDouble(_request, class="num">0, _data)) { Print(" DatabaseRead() failed with err: ", GetLastError()); DatabaseFinalize(_request); DatabaseClose(_db_handle); } } class="kw">return(_data); }
◍ 把财经日历塞进本地 SQLite 的笨办法
在 MT5 里落库财经日历,核心是先开一个数据库句柄,逻辑跟读写文件句柄差不多。下面这段脚本给每个货币对单独建一个 .sqlite 文件,作者自己也承认这是浪费空间的做法——更合理的结构是把所有跨货币数据放进同一个库,但懒得改,读者可以自己合并。 句柄拿到后必须检查是不是 INVALID_HANDLE,有效才说明是空白库能继续建表。本文只盯“板块价格”这一类日历事件(通胀、CPI、PPI 都归在这把伞下),所以表名直接叫 PRICES,目标是后面用两国通胀差做多空信号,这条路是几种替代方案里最省事的。 PRICES 表固定 4 列:DATE 文本存发布时间,FORECAST 实数存预期值,ACTUAL 实数存实际值,EVENT 文本存事件代码。因为同一货币同一天可能有多个价格类事件,EVENT 标签靠 CalendarValueHistoryByEvent 返回的描述码来填,检索时才能对号入座。 写入前先用 DatabaseTransactionBegin 和 DatabaseCommit 包住,比单条插快得多;再用 DatabaseBind 把多维字符串数组 _data 绑进表,第二维长度刚好等于 4 列。插完库发现在策略测试器里读不到,只能再导出 CSV 给回测用——MetaEditor 里直接跑脚本查 USD 库能打印出结果,但测试器环境隔离了数据库访问。 外汇与贵金属杠杆高、新闻跳空风险大,用日历数据建模前先在 demo 跑通读写链路。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Sourced from: [MQL5官方文档] class=class="str">"cmt">//| and here: [MQL5官方文档] class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- create or open a database class="type">class="kw">string _db_file = __currency + "_econ_cal.sqlite"; class="type">int _db_handle = DatabaseOpen(_db_file, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(_db_handle == INVALID_HANDLE) { Print("DB: ", _db_file, " open failed with code ", GetLastError()); class="kw">return; } else Print("Database: ", _db_file, " opened successfully"); ... ... } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { ... class=class="str">"cmt">//--- if the PRICES table exists, class="kw">delete it if(DatabaseTableExists(_db_handle, "PRICES")) { class=class="str">"cmt">//--- class="kw">delete the table if(!DatabaseExecute(_db_handle, "DROP TABLE PRICES")) { Print("Failed to drop table PRICES with code ", GetLastError()); DatabaseClose(_db_handle); class="kw">return; } } class=class="str">"cmt">//--- create the PRICES table if(!DatabaseExecute(_db_handle, "CREATE TABLE PRICES(" "DATE TEXT ," "FORECAST REAL ,"