
您应当知道的 MQL5 向导技术(第 21 部分):配以财经日历数据进行测试
概述
我们继续关于由向导组装智能系统的系列,看看在测试期间如何将财经日历新闻集成到智能系统之中,从而即可确认一个思路、亦或构建一个更强壮的交易系统,鸣谢这篇文章的不小部分。那篇文章是系列的一部分,且因它是第一篇,因此我鼓励读者阅读、并跟进它;然而,我们在此严格择选由向导组装的智能交易如何从这些 MQL5 IDE工具中受益。对于新读者,这里和这里都有关于如何利用 MQL5 向导开发和组装智能系统的介绍性文章。
财经数据可以成为交易系统中优势的来源,因为相较于“技术面”,它更倾向于从证券的“基本面”来学习,这些指标以传统指标、自定义指标、和其它价格动作工具的形式更为普遍。这些“基本面”的表现形式可以为通货膨胀率、央行利率、失业率、生产力数据、以及许多其它新闻数据点,这些数据典型情况下会对证券价格产生很大影响,每当有数据发布时,它们的波动性就证明了这一点。其中最著名的可能就是几乎每个月第一个周五发布的非农就业数据。此外,肯定还有其它关键新闻数据点没有受到应有的关注,故被大多数交易者忽视,这就是为什么基于这些财经新闻数据点的测试策略有助于发掘其中一些,从而为寻觅商机的交易者传达优势。
SQLite 数据库可在 MetaEditor IDE 内创建,由于它们是数据存储库,纸面上,我们应该能够用其作为智能系统的数据源,如此它们就可充当指标缓冲区。不仅如此,它们能把财经数据存储在本地,这可轻松地允许离线测试,并且还可在新闻数据源因未知原因损坏时使用,随着某些(或不可避免地大多数)数据点老旧过时,这是一个持续的风险。故此,在本文中,我们会探讨如何使用 SQLite 数据库来存档财经日历新闻,如此这般,由向导组装的智能系统就可以用它来生成交易信号。
当前限制和变通方法
不过,仍有一个卡点。除了在策略测试器中读取财经日历数据无能为力之外,据我在策略测试器中读取数据库的测试来看,似乎也有类似的约束。在撰写本文时,这可能是我的编码错误,但尝试使用以下清单读取数据库数据:
//+------------------------------------------------------------------+ //| Read Data //+------------------------------------------------------------------+ double CSignalEconData::Read(string DB, datetime Time, string Event) { double _data = 0.0; //--- create or open the database in the common terminal folder ResetLastError(); int _db_handle = DatabaseOpen(DB, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(_db_handle == INVALID_HANDLE) { Print("DB: ", DB, " open failed with err: ", GetLastError()); return(_data); } string _sql = " SELECT ACTUAL " + " FROM " + " ( " + " SELECT ACTUAL " + " FROM PRICES " + " WHERE DATE <= '" + TimeToString(Time) + "' " + " AND EVENT = '" + Event + "' " + " ORDER BY DATE DESC " + " LIMIT 1 " + " ) "; int _request = DatabasePrepare(_db_handle, _sql); if(_request == INVALID_HANDLE) { Print("request failed with err: ", GetLastError()); DatabaseClose(_db_handle); return(_data); } while(DatabaseRead(_request)) { //--- read the values of each field from the obtained entry ResetLastError(); if(!DatabaseColumnDouble(_request, 0, _data)) { Print(" DatabaseRead() failed with err: ", GetLastError()); DatabaseFinalize(_request); DatabaseClose(_db_handle); } } return(_data); }
生成错误 5601,并显示消息,我尝试访问的数据表不存在!然而,运行精确的 SQL 脚本,无论从 MetaEditor 数据库 IDE、亦或附加到图表的脚本,都不会给我带来这些麻烦,因它会返回预期的结果。故此,这可能是我的疏忽,我需要在策略测试器中包含一些额外的代码才能运行,或者在策略测试器中不允许访问数据库。服务台的聊天机器人帮不上忙!
那么,在这种状况下我们能干啥?显然,如上所述,在本地数据库中留下财经数据存档有诸多益处,是故若不去进一步测试和开发基于它的智能系统,那就是一种耻辱。我提议的变通之法是将财经数据导出为 CSV 文件,并在策略测试器期间读取它。
尽管在这种情况下要依赖并使用 CSV 文件作为变通方法,但如果有人认为它们可以取代数据库,它们就会面临许多挑战和限制。有人也许会争辩说,与其将数据导出到数据库,然后再导出到 CSV 文件,为何不简单地将其直接导出到 CSV 文件呢?嗯,这就是原因。
CSV 文件在存储数据方面比数据库效率低得多。这经由许多因素来展现,首当其冲在于数据完整性和可验证。数据库通过主键和外键强制执行完整性和约束检查,而 CSV 文件显然缺乏这种能力。其次,在此基础上,性能和伸缩性是数据库的强项,这要归功于索引,据其可以非常有效地搜索大型数据集,而 CSV 文件则始终依赖于线性搜索,这在面对大数据时会非常慢。
第三,在大多数数据库中内置并发访问,这可允许多个用户实时访问,而在 CSV 文件这边无法做到这点。甚而,数据库提供了安全访问,其功能包括用户认证、基于角色的访问控制、和加密。默认情况下,CSV 文件不提供安全性,这令保护敏感数据变得棘手。
此外,数据库提供了 CSV 所没有的自动备份和恢复工具;数据库支持 SQL 并表和数据操纵进行复杂查询,从而全面分析,而 CSV 文件需要第三方脚本才能达到相同的功能。数据库为其事务提供 ACID 合规性,而 CSV 文件做不到。
继续说,数据库还支持规范化,这减少了数据冗余,由此允许更紧凑、更高效的存储,且存储中重复较少,而 CSV 固有的扁平结构必然会滋生大量冗余。数据库还支持版本控制(这很重要,因为大量数据会随着时间推移而更新),这是审计的关键功能,而 CSV 文件不提供。CSV 在数据更新时容易损坏数据,并在管理复杂的数据结构时面临挑战。与 CSV 文件相比,数据库还有许多其它重大优势,不过我们仅保留这些列举的突出优势就够了。这些所提及益处中的每一个都可在策划财经数据分析和研究方面扮演至关重要的角色,尤其当覆盖较长时间区间,有些事对于 CSV 文件来说运转不灵。
在我们将数据导出为 CSV 文件以供策略测试器访问之前,我们应该构建数据库,并将财经数据加载到其中,如此这般就是我们接下来要涵盖的内容。
构建 SQLite 数据库
为了构建我们的 SQLite 数据库,我们要用到一个脚本。下面分享了这份清单:
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ //| Sourced from: https://www.mql5.com/en/articles/7463#database_functions //| and here: https://www.mql5.com/en/docs/database/databasebind //+------------------------------------------------------------------+ void OnStart() { //--- create or open a database string _db_file = __currency + "_econ_cal.sqlite"; 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()); return; } else Print("Database: ", _db_file, " opened successfully"); ... ... }
这段代码大部分来源于这里,经少量修改。创建数据库要通过一个句柄,就像声明文件读取或写入的句柄一样。我们正在为每个货币对创建一个数据库,我承认这是浪费、且非常笨拙的,更好的方式应当是将所有这些跨货币的财经数据点放在单一数据库之中,不过我尚未那么勤奋,我很抱歉。读者可以做出修改。一旦我们创建了句柄,在继续之前,我们需要检查该句柄是否有效。若它有效,则示意我们有一个空白数据库,因此可以继续创建数据表来存放我们的数据。我们之所以为我们的表格价格命名,是因为在本文中我们将只关注'板块价格'类型的日历事件板块。这是一个伞形板块,不仅包括通货膨胀率数据,还有消费者和生产者价格指数也受到关注,因为我们正在寻求开发一个自定义信号类,基于所交易货币对的相对通货膨胀率上的多头或空头条件。在开发这些多头和空头条件信号时,有许多替代方法可供采取,此处选择的这条路线可能是最简单的一条。在创建表数据时,就像大多数数据库对象一样,我们首先检查它是否存在,如果存在,它将被删除(抛弃),如此我们就能创建我们将要填充和使用的数据表。这般行事的清单分享如下:
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { ... //--- if the PRICES table exists, delete it if(DatabaseTableExists(_db_handle, "PRICES")) { //--- delete the table if(!DatabaseExecute(_db_handle, "DROP TABLE PRICES")) { Print("Failed to drop table PRICES with code ", GetLastError()); DatabaseClose(_db_handle); return; } } //--- create the PRICES table if(!DatabaseExecute(_db_handle, "CREATE TABLE PRICES(" "DATE TEXT ," "FORECAST REAL ," "ACTUAL REAL ," "EVENT TEXT);")) { Print("DB: ", _db_file, " create table failed with code ", GetLastError()); DatabaseClose(_db_handle); return; } //--- display the list of all fields in the PRICES table if(DatabasePrint(_db_handle, "PRAGMA TABLE_INFO(PRICES)", 0) < 0) { PrintFormat("DatabasePrint(\"PRAGMA TABLE_INFO(PRICES)\") failed, error code=%d at line %d", GetLastError(), __LINE__); DatabaseClose(_db_handle); return; } ... }
我们创建的数据表仅包含 4 列,名为 “DATE” 列,其为文本类型,记录财经新闻发布时间,“FORECAST” 列用于预测的财经数据点,该列将为实数类型,“ACTUAL” 列也为实数类型,并将包括该日期的实际财经数据点, 最后是 'EVENT' 列,其为文本类型,有助于正确标记该数据点,因为给定货币的任何日期,我们在事件板块价格类别中都能拥有多个数据点。故此,每个数据点的标签类型,将与数据的事件代码相对应。这是因为在检索财经日历数据时,我们调用 'CalendarValueHistoryByEvent' 函数来返回与特定事件配对的日历新闻值。这些事件中的每一个都有一个字符串描述性代码,当存储在数据库中时,我们会把这些代码指派给数据。检索财经日历数据的 'Get' 函数的清单如下所示:
//+------------------------------------------------------------------+ //| Get Currency Events //+------------------------------------------------------------------+ bool Get(string Currency, datetime Start, datetime Stop, ENUM_CALENDAR_EVENT_SECTOR Sector, string &Data[][4]) { ResetLastError(); MqlCalendarEvent _event[]; int _events = CalendarEventByCurrency(Currency, _event); printf(__FUNCSIG__ + " for Currency: " + Currency + " events are: " + IntegerToString(_events)); // MqlCalendarValue _value[]; int _rows = 1; ArrayResize(Data, __COLS * _rows); for(int e = 0; e < _events; e++) { int _values = CalendarValueHistoryByEvent(_event[e].id, _value, Start, Stop); // if(_event[e].sector != Sector) { continue; } printf(__FUNCSIG__ + " Calendar Event code: " + _event[e].event_code + ", belongs to sector: " + EnumToString(_event[e].sector)); // _rows += _values; ArrayResize(Data, __COLS * _rows); for(int v = 0; v < _values; v++) { // printf(__FUNCSIG__ + " Calendar Event code: " + _event[e].event_code + ", for value: " + TimeToString(_value[v].period) + " on: " + TimeToString(_value[v].time) + ", has... "); // Data[_rows - _values + v - 1][0] = TimeToString(_value[v].time); // if(_value[v].HasForecastValue()) { Data[_rows - _values + v - 1][1] = DoubleToString(_value[v].GetForecastValue()); } if(_value[v].HasActualValue()) { Data[_rows - _values + v - 1][2] = DoubleToString(_value[v].GetActualValue()); } // Data[_rows - _values + v - 1][3] = _event[e].event_code; } } return(true); }
我们用到一个名为 '_data' 的多维字符串数组来检索财经日历数据,它的第二个维度与我们存储数据的 'PRICES' 表中的列数匹配,这意味着它的行数与我们要插入到 'PRICES' 表中的数据行数相等。为了加快从数组到表的数据加载速度,我们首先调用 'DatabaseTransactionBegin()' 和 'DatabaseTransactionCommit()' 函数,分别启动和终止数据写入操作。这在上面引用的文章中的此处进行了解释,相较于没有它们的操作,这是一条更有效的路径。其次,我们调用数据绑定函数将数组内数据实际写入数据库。由于我们的数据列与目标数据表匹配,因此该过程也相对直截了当、且非常高效,尽管有点冗长,如下清单所示:
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ //| Sourced from: https://www.mql5.com/en/articles/7463#database_functions //| and here: https://www.mql5.com/en/docs/database/databasebind //+------------------------------------------------------------------+ void OnStart() { ... //--- create a parametrized _sql_request to add _points to the PRICES table string _sql = "INSERT INTO PRICES (DATE,FORECAST,ACTUAL,EVENT)" " VALUES (?1,?2,?3,?4);"; // _sql_request parameters 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); return; } //--- go through all the _points and add them to the PRICES table string _data[][__COLS]; Get(__currency, __start_date, __stop_date, __event_sector, _data); int _points = int(_data.Size() / __COLS); bool _request_err = false; DatabaseTransactionBegin(_db_handle); for(int i = 0; i < _points; i++) { //--- set the values of the parameters before adding a data point ResetLastError(); string _date = _data[i][0]; if(!DatabaseBind(_sql_request, 0, _date)) { PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError()); _request_err = true; break; } //--- if the previous DatabaseBind() call was successful, set the next parameter if(!DatabaseBind(_sql_request, 1, _data[i][1])) { PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError()); _request_err = true; break; } if(!DatabaseBind(_sql_request, 2, _data[i][2])) { PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError()); _request_err = true; break; } if(!DatabaseBind(_sql_request, 3, _data[i][3])) { PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError()); _request_err = true; break; } //--- 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; break; } else PrintFormat("%d: added data for %s", i + 1, _date); //--- 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; break; } } //--- done going through all the data points //--- transactions status if(_request_err) { PrintFormat("Table PRICES: failed to add %s data", _points); DatabaseTransactionRollback(_db_handle); DatabaseClose(_db_handle); return; } else { DatabaseTransactionCommit(_db_handle); PrintFormat("Table PRICES: added %d data", _points); } ... }
将数据插入 'PRICE' 表中后,我们现在必须据我们的数据库创建一个 CSV 文件,因为策略测试器似乎禁止访问数据库。回顾一下,我们的 'Read()' 函数包含读取数据库的 SQL,在 MetaEditor 中运行良好,如下图所示:
此外,如果我们将脚本 'sql_read'(完整源代码如下)附加到任何具有相似时间输入的图表,并查询 USD 数据库,我们会得到相同的结果,这意味着在 MetaEditor IDE 或 MT5 终端环境下访问数据库没有问题。请参阅下面的日志打印图片:
脚本附加和运行,潜在意味着附加到图表的智能系统能够毫无问题地读取数据库数值。然而,出于我们现在的目的,我们在运行策略测试器时无法读取数据库值,这意味着我们需要依赖 CSV 文件。
导出财经日历数据
为了将数据导出为 CSV,我们只需调用内置函数之一 'DatabaseExport()',如下面的代码所示:
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { ... //--- save the PRICES table to a CSV file 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()); //--- close the database file and inform of that DatabaseClose(_db_handle); PrintFormat("Database: %s created and closed", _db_file); }
这种方式是代码密集度最低的路径,因为如果我们首先把数据选出到一个对象(例如数组)之中,然后循环遍历所有数组值,并把它们保存到一个由逗号分隔的字符串当中,然后我们导出,它会达成相同的结果,但几乎可以肯定,撇开我们在此忙乱的编码不谈,执行时间比 for 循环方式短得多。这可能是因为 SQLite 是一个 C-语言库,而 MQL5 也很多基于 C。
我们的数据库表设计,对于 'PRICES',缺少一个显式主键,这些主键对于大型数据集的索引创建非常重要,可令数据库成为快速而强大的工具。该表的修改可以是添加一个用作主键的自动增量列,或者将 'EVENT' 和 'DATE' 列配对作为主键,因为站在设计的立场,两列的组合值在所有数据行中都是唯一的。在 'EVENT' 列中存储的事件标签其代码的歧义需要一些额外的努力,以便确保您感兴趣的数据点是您实际所求。
举例,在本文中,我们专注于 GBPUSD 货币对,这意味着我们感兴趣的两种货币是 GBP 和 USD。(注意,我们避开了 EUR 的多个数据点,因为这些不光来自欧元区,还有其成员国!)如果我们查看这些歧义较低的货币的通胀数据的事件代码,我们得到 GBP 是 'cpi-yy',USD 是 'core-pce-price-index-yy'。记住,我们不会参考 USD 的其它年度同比消费者通胀代码,因此在做出选择时应仔细考虑。而且,这种标签本身并非标准,这意味着几年、甚至更短的时间内,它可能会被修订,以至于任何自动系统也需要更新其代码。这指的是某个人拥有自己的自定义标签的想法,其中包含来自日历数据的数据验证筛选器,以帮助确保正确的数据得到正确编码,但如前所述,有时需要一些人工检查,因为此一时彼一时,编码可能会在瞬间更改。
MQL5 信号类
如前所述,我们为此用到了 CSV 文件,虽然这应该是一个直截了当的过程,但如果人们不知道其中的差异,在读取这些 ANSI 和 UTF8 格式数据时可能会带来一些挑战。我们采用了此处的标准 CSV 阅读期函数来读取导出的 CSV 数据,并将其加载到每种货币指标的初始化函数之中。保证金货币(GBP)和盈利货币(USD)。行事当中,读取大型 CSV 文件肯定会受到限制,因为它们会给内存带来压力。一个潜在的变通是按时间对 CSV 文件进行分区,以便在初始化时只加载其中一个文件,而当其最新数据点对于策略测试器中的当前时间来说太陈旧时,该文件将被“释放”,并加载一个更新的 CSV 文件。
这些变通都解决了在策略测试器中不能访问数据库的问题。故此,我们的信号类,由于它并非从数据库中读取,是故我们简单地取指定名称的 CSV 文件作为输入,其中保证金货币和盈利货币两者皆有之。配以我们的信号类,我们仅用到的序列缓冲区将是 'm_time' 类,严格来说,我们甚至不需要缓冲区,因为当前时间已经足够了,不过,它在此处用于获取索引为零的时间。基于所加载 CSV 文件检索日历数据值是由 'Read' 函数完成,其代码如下:
//+------------------------------------------------------------------+ //| Read Data //+------------------------------------------------------------------+ double CSignalEconData::Read(datetime Time, SLine &Data[]) { double _data = 0.0; int _rows = ArraySize(Data); _data = StringToDouble(Data[0].field[1]); for(int i = 0; i < _rows; i++) { if(Time >= StringToTime(Data[i].field[0])) { _data = StringToDouble(Data[i].field[1]); } else if(Time < StringToTime(Data[i].field[0])) { break; } } return(_data); }
它是迭代的,因为它使用 for 循环,不过,若我们能够从一个已索引的数据库访问相同的数据,则相同的操作应当执行地更快。在小型数据集(例如本文中所用的数据集)上,这种性能差异可能会被忽略,但随着数据集大小的增加,并且要查看的历史数据更多,那么在策略测试器中读取 SQLite 的情况会变得更强。
保证金货币和盈利货币都调用 “read” 函数,并返回最新的通货膨胀率。我们的信号简单地基于这些膨胀率的相对大小而生成。如果保证金货币的通货膨胀率高于盈利货币,那么我们将做空该货币对。另一方面,如果保证金货币的通货膨胀率较低,那么我们将做多。该逻辑如下所示,作为 'LongCondition()' 函数的一部分:
//+------------------------------------------------------------------+ //| "Voting" that price will grow. | //+------------------------------------------------------------------+ int CSignalEconData::LongCondition(void) { int result = 0; m_time.Refresh(-1); double _m = Read(m_time.GetData(0), m_margin_data); double _p = Read(m_time.GetData(0), m_profit_data); if(_m < _p) { result = int(100.0 * ((_p - _m) / _p)); } return(result); }
如果我们在运行智能系统时完全未经优化,在经向导组装、并编译后使用最初的默认设置,我们会得到以下结果:
通货膨胀显然是货币对趋势的决定因素。我们所用的通货膨胀数据是每月发布的,故此我们的测试时间帧也是月度。但情况不一定如此,因为在更小的时间帧内,可以在寻找更锋锐或更好的入场点时,也可维持相同的仓位。测试的货币对是 GBPUSD。
结束语
总而言之,SQLite 数据库在如何允许人们存储和管理自定义数据集方面带来了很多益处和优势。财经日历数据是这样一组数据,可以存档以供进一步分析,以帮助了解市场行为的主要驱动因素。一个非常简单的策略,例如本文中考察的,专注于通货膨胀,可能会对使用其它技术指标的系统产生重大影响。如常,这不是投资建议,我们鼓励读者在采用本文、或系列中分享的任何思路之前,自己要进行尽职地调研。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/14993


