MQL5 中的范畴论 (第 1 部分)·进阶篇
(2/3)·从集合与映射的数学骨架出发,把策略、工具与行情条件编织成可组合的严谨结构
不少交易者写 EA 时把指标、信号和订单逻辑揉成一团,改一个参数就牵一发动全身。问题往往不在代码量,而在缺少一套描述「对象之间如何映射」的语言。范畴论提供的域与态射,正好补上这层抽象。
用态射比值给品种间关系建模
这段结构把两个域(domain / codomain)之间的映射抽象成 CMorphism 对象,核心动作是取比值:当 DE.Get(c) 不为 0.0 时,把 morphism 在 c 位置的值设为 CE.Get(c) 除以 DE.Get(c)。 也就是说,它算的是 codomain 对 domain 的归一化相对强度,分母为零直接跳过,避免 MT5 回测时抛出除零错误。 CHomomorphism 类里用 morphism[] 动态数组装这些映射,cardinal 记录势,Cardinality() 方法带 Add 参数(默认 true)去登记域间索引对。你在 EA 里若想比较 EURUSD 与 XAUUSD 的波动传导,可照这个骨架把 Get() 换成 iClose 或 iATR 取值。 外汇与贵金属杠杆高、跳空频繁,这类比值只是结构参考,实际信号概率随品种流动性变化。
if(DE.Get(c)!=class="num">0.0){ morphism.Set(c,CE.Get(c)/DE.Get(c)); } class="kw">return(false); CMorphism(class="type">void){ domain=-class="num">1; codomain=-class="num">1; }; ~CMorphism(class="type">void){}; class CHomomorphism { class="kw">protected: class="type">int cardinal; CMorphism morphism[]; class="kw">public: class="type">bool init; CDomain domain; CDomain codomain; class="type">bool Cardinality(class="type">int DomainIndex,class="type">int CodomainIndex,class="type">bool Add=true) {
◍ 映射前的两道闸门
在把价格状态做态射(morphism)之前,这段逻辑先卡了两道关口,任一不过就直接返回 false,不做后续计算。 第一道是初始化标志 init:若未初始化,_morphed 置 false 并立即 return。第二道是索引越界检查:DomainIndex 或 CodomainIndex 超出各自集合的基数(Cardinality())范围,同样放弃态射。 过了闸门才进 for 循环,按 cardinal 次数遍历 morphism 数组,比对 DomainIndex 与已有映射的 domain 字段。外汇与贵金属杠杆高、跳空频繁,这类越界防护能避免 EA 在异常行情下写出脏映射。
class="type">bool _morphed=true; if(!init) { _morphed=false; class="kw">return(_morphed); } if(DomainIndex<class="num">0 || DomainIndex>=domain.Cardinality() || CodomainIndex<class="num">0 || CodomainIndex>=codomain.Cardinality()) { _morphed=false; class="kw">return(_morphed); } for(class="type">int m=class="num">0;m<cardinal;m++) { if(DomainIndex==morphism[m].domain)
「形态映射的收口与基数统计」
上面这段逻辑干一件事:当一次形变(morph)被确认成立后,把对应的定义域元素和陪域元素绑定进 morphism 数组,并让 cardinal 计数加一。 _morphed 为 false 时直接 break,说明本次扫描没匹配到合法映射,后续不写数组也不增计数。 若 _morphed 为 true,先 cardinal++,再 ArrayResize(morphism,cardinal) 把动态数组扩一格;随后用 domain.Get 和 codomain.Get 分别取出 DomainIndex、CodomainIndex 对应的 CElement,成功才调用 Morph() 写入 morphism[cardinal-1]。 Cardinality() 只是返回 cardinal 当前值——也就是已落盘的形态映射总数。在 MT5 里跑这套,你可以打印 Cardinality() 观察某段行情窗口内被识别出的形态数量,外汇与贵金属波动剧烈,该数值仅反映历史样本,实战需警惕假突破带来的过拟合风险。
{
_morphed=false; break;
}
if(_morphed)
{
cardinal++;
ArrayResize(morphism,cardinal);
CElement _de,_ce;
if(domain.Get(DomainIndex,_de) && codomain.Get(CodomainIndex,_ce))
{
morphism[cardinal-class="num">1].Morph(domain,codomain,_de,_ce,Add);
}
}
class="kw">return(_morphed);
};
class="type">int Cardinality()
{
class="kw">return(cardinal);
};映射表的读写边界检查
在自定义态射集合(CMorphism 数组)里做取数和赋值,必须先卡住索引与域值域的边界,否则 MT5 回测时极易触发数组越界导致 EA 静默终止。 下面这段是 Get 方法的核心:仅当 Index 落在 [0, Cardinality()) 区间内才返回 true 并把对应态射交给引用参数,否则直接 false。 Set 方法更严格,除了 Index 合法,还要求传入的 Value.domain 落在 domain 集合基数内、Value.codomain 落在 codomain 集合基数内,三重条件全过才允许写入。 实盘或复盘前,建议把 Cardinality() 打印到日志确认实际长度——若返回 0,说明集合未初始化,此时任何 Get/Set 调用都会走 false 分支,策略逻辑可能整段失效。外汇与贵金属波动剧烈,这类底层容器错误不会弹窗提示,只会让信号不触发。
class="type">bool Get(class="type">int Index,CMorphism &Morphism) { if(Index>=class="num">0 && Index<Cardinality()) { class="kw">return(true); } class="kw">return(false); }; class="type">bool Set(class="type">int Index,CMorphism &Value) { if ( Index>=class="num">0 && Index<Cardinality() && Value.domain>=class="num">0 && Value.domain<domain.Cardinality() && Value.codomain>=class="num">0 && Value.codomain<codomain.Cardinality() ) { if(!MorphismMatch(Index,Value,morphism,Cardinality()))
◍ 同态映射的赋值与初始化实现
上面这段 CHomomorphism 类的尾部代码,补完了映射写入与对象初始化的逻辑。morphism[Index]=Value 负责把源域中第 Index 个元素映射到目标域的 Value,写入成功后直接 return(true),外部调用方可以据此判断这一次映射是否落库。 若索引越界或映射关系未定义,函数走到末尾返回 false,这意味着当前同态关系在该点上不构成合法映射。外汇与贵金属市场的高波动特性下,这类离散映射若用于状态机,必须确认每个 tick 的 Index 都在 morphism 数组边界内,否则可能漏判信号。 Init 方法接收两个 CDomain 引用,把 domain 与 codomain 绑定到实例,并把 init 置为 true。无参构造函数则把 init 设 false、cardinal 清零,相当于一个尚未挂载域的空壳对象,调用前必须先 Init,否则 DomainMatch 里的基数比较会指向未初始化引用。 DomainMatch 函数先比 A 与 B 的 Cardinality(),不一致直接返回 false。只有基数相等的域才具备逐点比对的前提,这一步把后续同构判定的计算量从笛卡尔积量级压到了线性扫描。
{
morphism[Index]=Value;
class="kw">return(true);
}
}
class="kw">return(false);
};
class="type">void Init(CDomain &Domain,CDomain &Codomain)
{
domain=Domain;
codomain=Codomain;
init=true;
}
CHomomorphism(class="type">void){ init=false; cardinal=class="num">0; };
~CHomomorphism(class="type">void){};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Domain Match function |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool DomainMatch(CDomain &A,CDomain &B)
{
if(A.Cardinality()!=B.Cardinality())
{
class="kw">return(false);
}
「用域匹配验证奇偶子集关系」
这段脚本把前面封装的域(CDomain)拿来跑实测:先声明_evens和_odds两个域,各取3个元素,偶数域步长2从0起,奇数域步长2从1起,相当于生成{0,2,4}与{1,3,5}两组离散集合。 IsSubdomain函数先通过GetSubdomains把_evens的全部子集捞出来(3元素集合理论上有8个子集,含空集),再逐个用DomainMatch比对传入的SubDomain,命中即返回true并break,避免无谓遍历。 OnStart里把_evens传进去后,打印出的_subdomain_count若等于8,就说明GetSubdomains的递归展开没漏项;此时若把_odds塞进IsSubdomain,返回必然是false——因为奇偶两域元素无交,不可能构成子集关系。外汇与贵金属行情里这类离散状态判定常用于形态分类,但市场高波动下误判概率会放大,参数__domain_elements和__domain_morphisms建议先在策略测试器里改小跑通逻辑再放大。
class="type">bool _matched=true; for(class="type">int o=class="num">0; o<A.Cardinality(); o++) { CElement _a,_b; if(A.Get(o,_a) && B.Get(o,_b) && !ElementMatch(_a,_b)) { _matched=false; break; } } class="kw">return(_matched); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Is Subdomain function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool IsSubdomain(CDomain &Domain,CDomain &SubDomain) { class="type">bool _is_subdomain=false; class="type">int _subdomain_count=class="num">0; CDomain _subdomains[]; GetSubdomains(Domain,_subdomain_count,_subdomains); for(class="type">int c=class="num">0;c<_subdomain_count;c++) { if(DomainMatch(SubDomain,_subdomains[c])) { _is_subdomain=true; break; } } class="kw">return(_is_subdomain); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| INPUTS | class=class="str">"cmt">//+------------------------------------------------------------------+ input class="type">int __domain_elements=class="num">3; input class="type">int __domain_morphisms=class="num">5; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//Declare a sets of natural even & odd numbers CDomain _evens,_odds; CreateNumberDomain(_evens,__domain_elements,class="num">2); CreateNumberDomain(_odds,__domain_elements,class="num">2,class="num">1); printf(__FUNCSIG__+" evens are... "+PrintDomain(_evens)); printf(__FUNCSIG__+" odds are... "+PrintDomain(_odds)); class="type">int _subdomain_count=class="num">0; CDomain _subdomains[]; GetSubdomains(_evens,_subdomain_count,_subdomains); printf(__FUNCSIG__+" evens subs are... "+IntegerToString(_subdomain_count)); for(class="type">int s=class="num">0; s<_subdomain_count; s++) {