MQL5 中的范畴论 (第 13 部分):数据库制程的日历事件
概述
有关范畴论的系列文章,我们在上一篇文章中,研究了秩序论如何与范畴论相结合,实验了如何在 MQL5 中实现联盟的概念,还研究了运用其中一些概念的交易系统案例。
上一篇文章里我们的重点是两个秩序论概念,即偏序和线性序。回顾一下,偏序是设置排名方法,是一种具有非对称性的集中预序类型。这意味着它们还具有反身性和传递性。另一方面,线性序是偏序的一种更集中的形式,它额外要求可比性,这意味着不允许存在未定义关系。
在本文中,我们要歇口气,暂缓引入新概念,并退后一步,重温到目前为止所涵盖的一些内容,目标是将它们集成到使用数据库制程的语言分类器当中。
对日历事件进行有效分类的必要性
日历事件几乎每天都会生成,其中大多数提前几个月就已预先标记。它们源自 MetaTrader 财经日历,高亮显示了中国、美国、日本、德国、欧盟、英国、韩国、新加坡、瑞士、加拿大、新西兰、澳大利亚、和巴西的货币及宏观经济指标。该列表似乎是动态的,故将来可能会添加更多国家/地区。这些指标往往是格式化的,但并不总是按数字形式,其主要特点是具有预测值、实际值、和前期值。注意,我提到“往往是格式化的”,这是因为并非所有指标都有数值,甚至在有些指标中,实际数值和其格式也有较大的变化。尤为不同的是,有很多不可比性,这在某种意义上代表了我们的问题陈述。
为了能使用这些货币和经济体的指标读数,交易者需要可靠且一致地读取这些数值,并准确解释其发布的文本。我们通过查看一些典型的日历事件来概括这一点。
上面我们列出了中国、美国、日本和德国的四个事件,分别捕获指数、收益率百分比、货币金额和未定义值。这些信息能以 MQL5 通过如下所示的简单方法提取。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void SampleRetrieval(string Currency) { MqlCalendarValue _value[]; datetime _stop_date=datetime(__start_date+int(PeriodSeconds(__stop_date_increment))); //--- get events MqlCalendarEvent _event[]; int _events=CalendarEventByCurrency(Currency,_event); printf(__FUNCSIG__+" for Currency: "+Currency+" events are: "+IntegerToString(_events)); // for(int e=0;e<_events;e++) { int _values=CalendarValueHistoryByEvent(_event[e].id, _value, __start_date, _stop_date); // 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... "); // if(_value[v].HasPreviousValue()) { printf(__FUNCSIG__+" Previous value: "+DoubleToString(_value[v].GetPreviousValue())); } if(_value[v].HasForecastValue()) { printf(__FUNCSIG__+" Forecast value: "+DoubleToString(_value[v].GetForecastValue())); } if(_value[v].HasActualValue()) { printf(__FUNCSIG__+" Actual value: "+DoubleToString(_value[v].GetActualValue())); } } } }
问题是在提取后浮现,即如何组织和排序提取的值以用于分析。如果我们的数值都是标准化的,比如说在 0-100 范围内的指数,那么日历事件之间的相对比较就很直接,因为这很容易就能推导出每个事件的相对重要性。不过,现在需要通过“第三方”进行分析,例如每个单独事件与特定股票或货币的价格走势之间的相关性。
此外,有些事件没有可比较的数值,例如上图所示的德国央行执行委员会成员的讲话。
但更相关的是对事件本身的文本描述,这应该取决于交易者辨别事件的方式。例如,在分析像 EURUSD 这样的外汇对时,理想情况下,您需要考虑与美元事件具有可比性的欧元事件。但是,您将如何应对像这般的事件:
它们的可比性看似与美元同调:
以及:
在选择欧元时,我们是使用欧元情绪还是德国情绪?亦或我们取两者并加权?如果加权,我们采用什么样的权重?在美元方面,密歇根州或费城,我们用哪个值?
因此,除了 MQL5 已经提供的方法之外,我们还需要一种额外的途径来进行事件分类,这不仅可以帮助我们轻松比较不同经济体和货币的数值,而且专门用于执行交易目的。
现有的分类方法非常初级。它们包括:按 ID 选择事件、按国家/地区选择事件、以及按货币选择事件。很少有其它分类考虑这些事件的值,但它们与这些分类没有太大不同。此外,按国家和货币进行选择确实会产生歧义,这在很大程度上要归结于欧元,而这无济于事。基本面是看指数回顾,还是看情绪前瞻,是欠缺的。而且,但对于同一事件,在某些情况下需要跨货币比较,而这并不像上面所示的 EURUSD 货币对那样清晰明确。
范畴论概念和数据库制程
为了快速回顾到目前为止我们所涵盖的范畴论,我们首先研究了集合的基本单位元素(在早前的文章中,集合被称为域),然后研究了集合,然后是态射及其类型,最后是众多属性的组合及其表现形式。
图像是图形的近似值,并捕获数据符合的概念布局,无需过早地关注填充数据表的独立数据,这些可以称为数据库制程。已建立的数据库,是经过索引的存储工具,可强制执行引用完整性,并避免数据重复。
范畴论和数据库制程之间的潜在协同作用在于,范畴论组合可以将其某些属性出借给数据库制程。因此,如果我们先用一个简单的数据库对日历事件进行分类,我们可以轻松地经由不同的“镜头”查看制程,无论是回调、图形、还是秩序、以及更多。此处是可以定义我们的日历事件的数据表示意图。
使用此基本布局,事件表的日期和代码列两者当作主键。回想一下代码列,其持有的事件代码数据是从日历事件中读取。可以很容易地将 “Currencies”、“Countries”、和“Events Type” 数据表的主键分别设置为 currencies_id、country_id 和 event_id 列。不过,"currency pair values" 数据表需要将 date 和 event_id 列组合作为其主键。
上述并非制程,因为没有指明数据表之间的连接,它只是展示我们数据库内的数据表。不过,我们可以有一个制程,其中一部分如下所示。
这种制程编排可以很容易地看作是范畴论的产物。这意味着我们在事件表和货币对值视图之间有一个通用属性。
回顾一下,典型余积通用属性在处理分段曲线时很有用。举一个实验为例,其中两个区域 A 和 B 中记录的温度分别呈线性和二次方变化。为了研究组合区域 A u B 的温度资料,余积通用属性允许将每个区域的独立温度资料粘合到一个函数中,该函数基于曲线数据,如果有人决定在没有明确定义行程的情况下前往该区域,这将是一个合理的中转站。
作为交易者,对于我们的目的,上述组合非常实用,因为每种独立货币的事件值永远不会同时发布。因此,例如,如果今天公布欧元的情绪数据,美元的情绪数据可能会在两周内到来。我们可以使用旧的美元(最后的)值来得出货币对值,但是依据范畴论,我们实际上能够使用通用属性来反推或预测货币值,由于示意图互倒,故尚未得到更新。
以 MQL5 实现
由于我们的范畴理论和制程可由 MQL5 表示为矩形互倒,因此我们可以稍微修改之前文章中的类 'CSquareCommute',如下所示:
//+------------------------------------------------------------------+ //| Square Commute Class to illustrate Universal Property | //+------------------------------------------------------------------+ template <typename TA,typename TB,typename TC,typename TD> class CCommuteSquare { public: CHomomorphism<TA,TB> ab; CHomomorphism<TA,TC> ac; CHomomorphism<TD,TB> db; CHomomorphism<TD,TC> dc; CHomomorphism<TD,TA> da; //universal property virtual void SquareAssert() { ab.domain=ac.domain; ab.codomain=db.codomain; dc.domain=db.domain; dc.codomain=ac.codomain; da.domain=db.domain; da.codomain=ac.domain; } CCommuteSquare(){}; ~CCommuteSquare(){}; };
我们在原代码中加入的只是通用属性的额外同态。因此,定义了这一点后,下一个关键的事情是如何为每个集合定义元素,以便捕获相应数据。可这样完成,如下列出的事件集(显示为 events 数据表):
//sample constructor for event set CElement<string> _e_event;_e_event.Cardinality(7); //
一旦定义此元素,交易者可以使用上面共享的 listing-1,或任何其它相应选项的数据轻松填充它。我并未在此费力地证明这一点,因为我相信读者能想出一种更好的方法,更加适合自己的策略。一旦一个元素由数据填充,它就可以很容易地分别添加到上面所示的互倒类的相应域中,正如我们在之前的文章中涵盖的那样。事件值,甚至货币集元素也可以构建,如下所示:
//sample constructor for type set CDomain<string> _d_type;_d_type.Cardinality(_types); //data population CElement<string> _e_type;_e_type.Cardinality(1);
//sample constructor for currency set CDomain<string> _d_currency;_d_currency.Cardinality(_currencies); //data population CElement<string> _e_currency;_e_currency.Cardinality(1);
然后,这导出了货币对价值元素。该集合将货币配对成一个互倒交易对,当从市场观察中选择时,它会有价格图表,并附带一个新的数值,其为该货币对的有效值,该值来自组合两个事件对应的货币值。因此,举例,如果我们有欧元区的零售额数值,其映射到 EUR 货币,而美国的零售额将映射到 USD,那么货币对值集合将列出 EURUSD 对,并依据其有效零售销售数值。构造其元素的清单如下所示:
//sample constructor for values set CDomain<string> _d_values;_d_values.Cardinality(_values); //data population CElement<string> _e_values;_e_values.Cardinality(4);
话虽如此,有人也许会问这样的问题,为什么货币对视图(在上面的 MQL5 清单中是货币对价值)很重要?对的,它将事件日历中报告的两种货币的事件值统合为两者组成的货币对的单一价值。例如,这个单一价值可以形成一个时间序列,为进一步研究提供机会,譬如说该货币对的价格时间序列,或该货币对的任何其它指标序列。
回顾梳理到目前为止我们所涵盖的内容,设计和实现此分类所涉及的步骤首先是将原始日历事件整理到数据库制程。这样可识别重复的文本,并允许使用索引。将事件表链接到所有其它数据表的简单制程可用于此设计。
按这种设计,我们将迭代遍历日历事件,可以很容易地将其提取出来,如上面的第一个 listing-1 所示,并在我们的数据库中填充数值。不是要重新发明轮子,但该类是针对我们的数据库,其伴随表体现为结构,可如下所示:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CDatabase { public: STableEvents events; STableEventTypes event_types; STableCountries countries; STableCurrencies currncies; CDatabase(void); ~CDatabase(void); };
这与 SQL 的不同之处在于,数据存储在内存当中,这是临时的,而 SQL 通常将数据存储在计算机的外存空间(硬盘)上。不过,该类确实允许我们将其导出到现有数据库,并且由于 MQL5 IDE 拥有一些数据库处理能力,您最终可从物理数据库而非内存中读取这些值,从而节省计算资源。
一旦我们有了一个数据库,我们就能自上面列出的类来构造我们的矩形互倒。该过程仅涉及定义边角集合,如下图所示。对于每个集合,我们定义了它的元素结构,上面已经分享了它的清单。一旦元素定义完毕,它们将填充来自我们数据库的数据,然后添加到矩形互倒类的实例之中。
一旦我们有了集合,我们就可以做些有趣的事情,如开始定义这些集合之间的同态。从事件集到事件类型集的同态只是将事件映射到它们的类型,从数据库设计的角度来看,这意味着我们只能在事件表中有一列类型的索引,而实际类型及其索引将在事件类型表当中,两者之间的外键关系将等效于我们的同态。由于这不是一个数据库,因此在类型集中,我们只是将所有类型都列为事件集的一部分,且没有重复,这意味着我们的事件集是域,事件类型是协域。因此,可以使用下面的清单轻松定义同态:
//ab homomorphisms CHomomorphism<string,string> _ab; CElement<string> _e; for(int s=0;s<_sc.ab.domain.Cardinality();s++) { _e.Let(); if(_sc.ab.domain.Get(s,_e)) { string _s=""; if(_e.Get(0,_s)) { CMorphism<string,string> _m; _m.Morph(_sc.ab.domain,_sc.ab.codomain,s,EventType(_s)); _ab.Morphisms(_ab.Morphisms()+1); _ab.Set(_ab.Morphisms()-1,_m); } } }
像这样,从事件到货币的同态是一个简单的映射,可以通过下面的清单来实现:
//ac homomorphisms CHomomorphism<string,string> _ac; for(int s=0;s<_sc.ac.domain.Cardinality();s++) { _e.Let(); if(_sc.ac.domain.Get(s,_e)) { string _s=""; if(_e.Get(1,_s)) { CMorphism<string,string> _m; int _c=EventCurrency(_s); if(_c!=-1) { _m.Morph(_sc.ac.domain,_sc.ac.codomain,s,_c); _ac.Morphisms(_ac.Morphisms()+1); _ac.Set(_ac.Morphisms()-1,_m); } } } }
然而关键是剩下的三个同态,即从货币对价值集映射到事件类型,从货币对价值集映射到货币集,最后从事件集返回到货币对价值映射。如果我们从相对简单的前两个开始拆解它,我们得到一个从货币对价值到事件类型的映射,以及从货币对价值到货币的映射,这令我们合成一个产物。值得注意的是,根据同态的基本规则,域中的元素只能映射到协域中的一个元素。因此,这意味着在查看多种货币时,我们不能有相反方向的映射,因为这会导致每当货币对引用其值时,事件值映射到多个货币对,同样,对于货币,选择哪个映射的货币对价值集合,也面临类似的重复问题。因此,映射到事件值的实现如下所示:
//db homomorphisms CHomomorphism<string,string> _db; for(int s=0;s<_values;s++) { _e.Let(); if(_sc.db.domain.Get(s,_e)) { string _s=""; if(_e.Get(3,_s)) { int _t=TypeToInt(_s); CMorphism<string,string> _m; // _m.Morph(_sc.db.domain,_sc.db.codomain,s,_t); _db.Morphisms(_db.Morphisms()+1); _db.Set(_db.Morphisms()-1,_m); } } }
同样,与货币的映射如下所示:
//dc homomorphisms CHomomorphism<string,string> _dc; for(int s=0;s<_values;s++) { _e.Let(); if(_sc.dc.domain.Get(s,_e)) { string _s=""; if(_e.Get(0,_s))//morphisms for margin currency only { int _c=EventCurrency(_s); CMorphism<string,string> _m; // _m.Morph(_sc.dc.domain,_sc.dc.codomain,s,_c); _dc.Morphisms(_dc.Morphisms()+1); _dc.Set(_dc.Morphisms()-1,_m); } } }
这里值得注意的是出价货币和保证金货币的权重参数。这些可以通过优化或每个经济体的基准利率、或通货膨胀率的相对权重来得到(此清单并未穷尽)。交易者必须根据他的策略和市场前景做出选择。从事件返回的货币对价值的最终同态将映射到货币对价值中的单个元素,这样在事件集中就有两个条目。基于上述两个映射使用的权重,通用属性映射清单将如下所示:
//da homomorphisms CHomomorphism<string,string> _da; for(int s=0;s<_values;s++) { _e.Let(); if(_sc.da.domain.Get(s,_e)) { string _s_c="",_s_t=""; if(_e.Get(0,_s_c) && _e.Get(3,_s_t))// for margin currency { for(int ss=0;ss<_sc.ac.domain.Cardinality();ss++) { CElement<string> _ee; if(_sc.da.codomain.Get(ss,_ee)) { string _ss_c="",_ss_t=""; if(_ee.Get(1,_ss_c) && _ee.Get(6,_ss_t))// for margin currency { if(_ss_c==_s_c && _ss_t==_s_t) { CMorphism<string,string> _m; // _m.Morph(_sc.da.domain,_sc.da.codomain,s,ss); _da.Morphisms(_da.Morphisms()+1); _da.Set(_da.Morphisms()-1,_m); _sc.da=_da; _sc.SquareAssert(); break; } } } } } } } _da.domain=_sc.da.domain; _da.codomain=_sc.da.codomain; _sc.da=_da; _sc.SquareAssert();
故此,这将标志着基于单一货币的日历事件为货币对生成有效权重的最后一步。
日历事件分类
在制定事件类型表时,谨慎的做法是遵循一种规范的方法,在得出事件类型之前考虑临界数据量。正如问题陈述中所言,事件的分类很重要,因此,将这些事件分类到可比较类型的可能方法涉及:数据收集,上面分享了利用 MQL5 内置类提取基本数据;事件分组,我们可以使用标准分组,如指数、情绪读数、国债收益率、通胀读数等;随即是特征提取,因为每个事件都会被拆解为关键字,以判定它所属的最佳分组;随即是模型训练和我们的特征分类评估,我们创建训练和测试数据集,并从训练模型开始;随即是在测试数据集上测试模型,看看它对我们的事件进行分类的程度如何;最后,后期分析并迭代改进,通过研究如何在不过度拟合的情况下优调我们的模型得出结论。
一旦事件类型创建完毕后,我们会在上图所示的 “event_types” 数据表中填充这些事件类型。这意味着事件表中的 "event type id" 列将针对所有事件进行更新,如此来分配其分组。插入新行或更新行的存储过程可以指导我们上述模型的实现。
事件集的附加内容,由于元素数据类型是一个字符串数组,每个数组索引都贴合一个数据列,这意味着我们上面的组合仅有的重大变化就是从事件到事件值的同态。现在,我们不再只包含不同货币的描述文本与“零售”雷同的述值,而是可以容纳更宽泛的事件。
为交易决策提供信息
因此,从我们上面的组合中创建货币对价值域意味着我们拥有含时间戳的货币对价值。这个时间戳允许我们将这些值的大小(在某些情况下是方向)与最终的价格变化进行比较。仔细分析涉及的训练和测试数据集过程,可以看出每种事件类型与最终价格行为的相关性,及其程度。
通过使用这些事件价值与后续价格走势的相关性数据,我们不仅可以根据分析结果设置做多或做空交易的规则,还可以基于相关性的程度设置持仓规模。
如果将多个事件合并为一个加权平均值,且该“指标”与最终价格动作相关联,则使用加权货币对值标记最终价格走势的系统精度可大大提升。这就提出了一个问题,哪些权重适用于哪个事件。这个答案可以通过优化来回答,或者交易者自己对宏观经济学的理解可以指导这个过程。无论选择哪种方法,这种更全面的方法必然会产生更准确的预测。
案例研究:MQL5 中的实现和评估
为简洁起见,这不是一个完整的交易系统,而只是它的初始部分,它考虑了我们的组合,包括四个集合:事件、类型、货币和价值,如前所述。我们将直接提取日历事件数据,而不用我们的数据库类,填充矩形互倒类的实例,并读取我们能够生成的同态。同样,这只是一个纯粹的展示潜力的初始步骤,为此,我们的输入将主要有三个,即我们将关注的事件类型、买入/保证金货币的权重、和卖出/利润货币的权重。如前所述,这些权重是用于两种货币的日历值合二为一。为研究生成读数时,我们仅考虑 PMI 相关事件,并仅查看欧元、英镑、美元、瑞士法郎、和日元等货币;以及仅采用EURUSD、GBPUSD、USDCHF、和 USDJPY 的值。所有代码都附在文末。如果我们打印通用属性同态,我们应该在日志里得到下面这些:
2023.07.11 13:51:52.966 ct_13 (GBPUSD.i,H1) void OnStart() d to a homomorphisms are... 2023.07.11 13:51:52.966 ct_13 (GBPUSD.i,H1) 2023.07.11 13:51:52.966 ct_13 (GBPUSD.i,H1) {(EUR,USD,45.85000000,TYPE_PMI),(GBP,USD,47.00000000,TYPE_PMI),(USD,CHF,45.05000000,TYPE_PMI),(USD,JPY,48.75000000,TYPE_PMI)} 2023.07.11 13:51:52.966 ct_13 (GBPUSD.i,H1) | 2023.07.11 13:51:52.966 ct_13 (GBPUSD.i,H1) (EUR,USD,45.85000000,TYPE_PMI)|----->(markit-manufacturing-pmi,EUR,44.60000000,44.60000000,44.80000000,2023.06.01 11:00,TYPE_PMI) 2023.07.11 13:51:52.966 ct_13 (GBPUSD.i,H1) | 2023.07.11 13:51:52.966 ct_13 (GBPUSD.i,H1) {(markit-manufacturing-pmi,EUR,44.60000000,44.60000000,44.80000000,2023.06.01 11:00,TYPE_PMI),(markit-manufacturing-pmi,EUR,44.80000000,44.20000000,43.60000000,2023.06.23 11:00,TYPE_PMI),(markit-services-pmi,EUR,55.90000000,55.90000000,55.10000000,2023.06.05 11:00,TYPE_PMI),(markit-services-pmi,EUR,55.10000000,55.50000000,52.40000000,2023.06.23 11:00,TYPE_PMI),(markit-composite-pmi,EUR,53.30000000,53.30000000,52.80000000,2023.06.05 11:00,TYPE_PMI),(markit-composite-pmi,EUR,52.80000000,53.00000000,50.300000
我们仅输入 PMI 事件和上述预选货币和货币对,据此我们只得出一种态射,即保证金货币,在这种情况下恰好是欧元。我们的组合值高于欧元输入值,仅仅是因为美元的等效 PMI 值更高,而打印的 EURUSD 货币对价值只是加权平均值。对于这个特定的测试,欧元和美元两者都采用相等的权重。
结束语
我没有分享一个案例研究来展示如何在交易系统中应用这种分类,因为文章篇幅太长了,但我相信这篇文章中有足够的代码和材料令一些人自行实现。回顾一下,我们已经研究了范畴论和数据库制程如何协同工作,不仅有助于对日历事件进行分类,还有助于定义具有通用属性的产品组合,这些属性有助于量化日历事件对金融产品价格动作的影响。
对事件进行标准分类,其益处超出货币对价值轻松配对,它用到了范畴论的通用属性公理,这有助于定义一个同态,该同态可直接从事件集映射到货币对值集(无需使用事件值或货币集)。如前所述,这允许在只有一种货币的事件值是新的,而另一种货币的事件值在几天或几周内仍处于待定状态的情况下预测货币对价值。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/12950