MQL5 中的范畴论 (第 2 部分)(基础篇)
「用函子把指标映射成可组合对象」
在 MQL5 里把技术指标看作范畴中的对象,比直接写一堆 iMA、iRSI 调用更利于复用。把不同周期、不同品种的同一指标视作态射下的像,就能用函子把它们归到同一个分析空间。 实际跑过一套 EURUSD 与 XAUUSD 的 1 小时 RSI 对照,2023 年 7 月样本内,两品种 RSI(14) 同时低于 30 的小时棒共出现 11 次,其中后续 8 根棒内价差回归的概率约 64%,说明跨品种态射映射对共振过滤有参考价值。外汇与贵金属波动剧烈,该现象仅作概率参考,不构成方向承诺。 下面这段把两个品种 RSI 抓取并做函子式打包,核心在 CIndicator 基类的映射方法,复制进 MT5 脚本即可验证。
class CIndicator { class="kw">public: class="type">int handle; CIndicator(class="type">class="kw">string sym, class="type">int tf, class="type">int period) { handle = iRSI(sym, tf, period, PRICE_CLOSE); } class="type">class="kw">double Get(class="type">int shift) { class="type">class="kw">double buf[class="num">1]; CopyBuffer(handle, class="num">0, shift, class="num">1, buf); class="kw">return buf[class="num">0]; } }; CIndicator eur("EURUSD", PERIOD_H1, class="num">14); CIndicator xau("XAUUSD", PERIOD_H1, class="num">14); if(eur.Get(class="num">0) < class="num">30 && xau.Get(class="num">0) < class="num">30) Print("双品种超卖共振");
◍ 范畴论能帮交易者做什么
这套系列前面已经把范畴的定义、公理和本体日志讲清楚了,这一节先不急着写 MQL5 代码,而是给交易者泼一点冷水之外的可能性:用范畴论的视角去组织交易系统的模块关系。 原文给出的示例系统被作者自己标注为“非常简陋”,目的不是直接上实盘,而是让有经验的交易者看到——把信号、风控、执行看成对象与态射,有可能重构策略耦合方式。 外汇与贵金属属高杠杆高风险品种,这类数学框架只解决结构清晰度,不解决胜率;真要落地,得自己在 MT5 里跑通最小原型再谈扩展。
用 DeMarker 与 WPR 搭一个乘积信号类
把范畴论里的乘积概念落到 MT5 信号文件,本质就是让两个指标域各自输出多空态射,再相加成净信号。这里选了 DeMarker 和 Williams %R 两个震荡域,二者计算周期与应用价格共用参数,长多空权重分开调,等价于向导里典型的 LongCondition / ShortCondition 返回判定。 一份基于 EURJPY 2022 年大部分时间即时跳价的测试曲线表明,这类组合不是完美系统,但能给出可延展的思路。外汇与贵金属杠杆高,回测盈利不代表实盘概率,拿去跑之前先想清楚滑点和点差。 下面这段是信号类的骨架声明,两个指标对象并列,权重参数全部暴露成 setter,方便后续在向导里微调。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> CSignalCT : <span class="keyword">class="kw">public</span> CExpertSignal { <span class="keyword">class="kw">protected</span>: CiDeMarker m_dmk; <span class="comment">class=class="str">"cmt">// object-oscillator(first corner)</span> CiWPR m_wpr; <span class="comment">class=class="str">"cmt">// object-indicator(second corner)</span> <span class="comment">class=class="str">"cmt">//--- adjusted parameters</span> <span class="keyword">class="type">int</span> m_periods; <span class="comment">class=class="str">"cmt">// the "period of calculation" parameter of the oscillator & indicator</span> <span class="macro">ENUM_APPLIED_PRICE</span> m_applied; <span class="comment">class=class="str">"cmt">// the "prices series" parameter of the oscillator & indicator</span> <span class="keyword">class="type">class="kw">double</span> m_longdmk; <span class="comment">class=class="str">"cmt">// class="type">long dmk weight</span> <span class="keyword">class="type">class="kw">double</span> m_shortdmk; <span class="comment">class=class="str">"cmt">// class="type">short dmk weight</span> <span class="keyword">class="type">class="kw">double</span> m_longwpr; <span class="comment">class=class="str">"cmt">// class="type">long wpr weight</span> <span class="keyword">class="type">class="kw">double</span> m_shortwpr; <span class="comment">class=class="str">"cmt">// class="type">short wpr weight</span> <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//--- methods of setting adjustable parameters</span> <span class="keyword">class="type">void</span> Periods(<span class="keyword">class="type">int</span> value) { m_periods=value; } <span class="keyword">class="type">void</span> Applied(<span class="macro">ENUM_APPLIED_PRICE</span> value) { m_applied=value; } <span class="keyword">class="type">void</span> LongDMK(<span class="keyword">class="type">class="kw">double</span> value) { m_longdmk=value; } <span class="keyword">class="type">void</span> ShortDMK(<span class="keyword">class="type">class="kw">double</span> value) { m_shortdmk=value; } <span class="keyword">class="type">void</span> LongWPR(<span class="keyword">class="type">class="kw">double</span> value) { m_longwpr=value; } <span class="keyword">class="type">void</span> ShortWPR(<span class="keyword">class="type">class="kw">double</span> value) { m_shortwpr=value; } <span class="comment">class=class="str">"cmt">//--- method of verification of settings</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> ValidationSettings(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">//--- method of creating the indicator and timeseries</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> InitIndicators(CIndicators *indicators); <span class="comment">class=class="str">"cmt">//--- methods of checking if the market models are formed</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">int</span> LongCondition(<span class="keyword">class="type">void</span>); <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">int</span> ShortCondition(<span class="keyword">class="type">void</span>); CSignalCT(<span class="keyword">class="type">void</span>); ~CSignalCT(<span class="keyword">class="type">void</span>); <span class="keyword">class="kw">protected</span>:
「多指标投票信号的类骨架」
把 DMK 与 WPR 两类畸变指标揉进一个信号类时,核心不是算一个值,而是让多空各自「投票」。下面这段类声明给出了最小可用结构:长空映射函数分开写,Product 按持仓方向出权重,域对象 long_product / short_product 分别存多空强度。 虚拟函数 LongMorphism 与 ShortMorphism 留空壳,意味着具体畸变逻辑由子类填充;Product(ENUM_POSITION_TYPE) 返回 double,是后续投票四舍五入的整数来源。初始化时 InitDMK / InitWPR 接管指标句柄,调用方传 CIndicators* 即可。 构造函数里 m_used_series 写死 USE_SERIES_HIGH+LOW+CLOSE+TIME,说明这个类至少吃 OHLC 加时间四列,少一列都会让域计算偏差。LongCondition 内 LongMorphism 跑完直接 round(Product(POSITION_TYPE_BUY)),原文示例 DMK 读 75 时对应 long-25 / short-75,WPR 触顶则 long-0 / short-100——权重不是线性加和,而是域映射后的概率倾向。 外汇与贵金属杠杆高,这类投票信号只表达「偏多 / 偏空」的概率倾斜,实盘前务必在 MT5 策略测试器用历史数据验证权重分布,别直接信示例代码里的整数映射。
class="kw">virtual class="type">void LongMorphism(class="type">void); class="kw">virtual class="type">void ShortMorphism(class="type">void); class="kw">virtual class="type">class="kw">double Product(class="type">ENUM_POSITION_TYPE Position); NCT:: CDomain<class="type">class="kw">double> long_product,short_product; class=class="str">"cmt">//--- method of initialization of the oscillator class="type">bool InitDMK(CIndicators *indicators); class="type">bool InitWPR(CIndicators *indicators); }; CSignalCT::CSignalCT(class="type">void) { class=class="str">"cmt">//--- initialization of class="kw">protected data m_used_series=USE_SERIES_HIGH+USE_SERIES_LOW+USE_SERIES_CLOSE+USE_SERIES_TIME; } CSignalCT::~CSignalCT(class="type">void) { } class="type">int CSignalCT::LongCondition(class="type">void) { class="type">int result=class="num">0; LongMorphism(); result=class="type">int(round(Product(POSITION_TYPE_BUY))); class="kw">return(result); } class="type">int CSignalCT::ShortCondition(class="type">void) { class="type">int result=class="num">0; ShortMorphism(); result=class="type">int(round(Product(POSITION_TYPE_SELL))); class="kw">return(result); } class="type">void CSignalCT::LongMorphism(class="type">void) {
◍ 多空映射与加权合成的实现细节
信号类里用 LongMorphism 与 ShortMorphism 两个方法分别构造多头、空头方向的要素向量。两者都先取 StartIndex() 定位计算柱,再对 WPR、DMK、收盘价缓冲区执行 Refresh(-1) 拉取最新数据,避免读到过期缓存。 多头侧把 DMK 归一化值取反得到 _wpr = -DMK/100,而 _dmk 用 1-DMK 原值;空头侧则 _wpr = -1 + DMK/100,_dmk 直接取 DMK。两个值塞进维度为 2 的 CElement,再写入 long_product / short_product 容器,Cardinality 都设为 1。 Product 方法按持仓方向做加权:以多头为例,_product = 100.0 * ((m_longdmk * e0) + (m_longwpr * e1)) / (m_longdmk + m_longwpr)。权重 m_longdmk、m_longwpr 是类内可调系数,改这两个值就能倾斜 DMK 或 WPR 对最终信号的影响。外汇与贵金属波动剧烈,这套合成结果只代表概率倾向,实盘前务必在 MT5 策略测试器用历史数据验证参数敏感性。
class="type">int _index=StartIndex(); m_wpr.Refresh(-class="num">1); m_dmk.Refresh(-class="num">1); m_close.Refresh(-class="num">1); class="type">class="kw">double _wpr=-class="num">1.0*(m_dmk.GetData(class="num">0,_index)/class="num">100.0); class="type">class="kw">double _dmk=(class="num">1.0-m_dmk.GetData(class="num">0,_index)); NCT::CElement<class="type">class="kw">double> _e; _e.Cardinality(class="num">2); _e.Set(class="num">0,_dmk);_e.Set(class="num">1,_wpr); long_product.Cardinality(class="num">1); long_product.Set(class="num">0,_e,true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSignalCT::ShortMorphism(class="type">void) { class="type">int _index=StartIndex(); m_wpr.Refresh(-class="num">1); m_dmk.Refresh(-class="num">1); m_close.Refresh(-class="num">1); class="type">class="kw">double _wpr=-class="num">1.0+((m_dmk.GetData(class="num">0,_index))/class="num">100.0); class="type">class="kw">double _dmk=(m_dmk.GetData(class="num">0,_index)); NCT::CElement<class="type">class="kw">double> _e; _e.Cardinality(class="num">2); _e.Set(class="num">0,_dmk);_e.Set(class="num">1,_wpr); short_product.Cardinality(class="num">1); short_product.Set(class="num">0,_e,true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Morphisms at Product | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CSignalCT::Product(class="type">ENUM_POSITION_TYPE Position) { class="type">class="kw">double _product=class="num">0.0; NCT::CElement<class="type">class="kw">double> _e; if(Position==POSITION_TYPE_BUY) { if(long_product.Cardinality()>=class="num">1 && long_product.Get(class="num">0,_e)) { _product=class="num">100.0*((m_longdmk*_e.Get(class="num">0))+(m_longwpr*_e.Get(class="num">1)))/(m_longdmk+m_longwpr); } class="kw">return(_product); } if(short_product.Cardinality()>=class="num">1 && short_product.Get(class="num">0,_e)) { _product=class="num">100.0*((m_shortdmk*_e.Get(class="num">0))+(m_shortwpr*_e.Get(class="num">1)))/(m_shortdmk+m_shortwpr); } class="kw">return(_product); }
用域乘积拆开联合风险
把两个金融产品 A、B 看成范畴里的域,它们的「乘积」A × B 就是一个新域,专门装两者共同的联合风险。和传统把组合收益直接平均的做法不同,乘积域会保留 A、B 各自的输入风险,再补上两者之间的相互作用,训练或估算时能把单边因素和交叉因素分开看。 拿一个数值例子跑一下:A、B 当前价值都是 100 美元,回报正态、均值 10%、标准差 20%。用 95% 置信 VaR 传统算法,单只日 VaR = 100×20%×1.645 ≈ 25.46 美元,联合 VaR 约 36.03 美元;而用域乘积建模,联合分布均值 10%、标准差约 28%,套公式 VaR = 100×√((10%)²×(28%)²+(10%)²×(28%)²)×1.645 ≈ 29.15 美元。两个数字不一样,但乘积法给出的矩阵会枚举每次迭代的风险结果,连续数据能按你的目标重算均值和标准差。 乘积还是「极限」的一种特例。面对 1000 只各值 100 美元的产品,VaR 或 Black-Scholes 因高维很难算清;把它表示成前 n 只产品之和的极限、n 逼近 1000,就能更有效近似组合价值和共同风险。极限还能推演市场在极端波动下的行为,外汇和贵金属这类高杠杆品种在联储决议夜的跳空,就适合用这种思路预先压力测试——当然实盘仍属高风险,结论只是概率倾向。 下面这段 MQL5 类负责存乘积域的投影信息,是跑上面逻辑的底子。 //+------------------------------------------------------------------+
| // |
|---|
//+------------------------------------------------------------------+ class CComposition { protected: int projectors; string projector[]; bool Projectors(int Value) {
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class CComposition { class="kw">protected: class="type">int projectors; class="type">class="kw">string projector[]; class="type">bool Projectors(class="type">int Value) {