MQL5 中的范畴论 (第 1 部分)(基础篇)
用范畴视角看 MQL5 的信号组合
在 MQL5 里写策略,多数人把指标、信号、订单当成孤立模块拼起来。换个角度,把它们看作范畴里的对象,信号之间的组合关系就是态射,组合顺序不同会导出不同的交易逻辑链。 一个具体现象:在 MT5 内置的 28 个默认技术指标中,约 19 个可输出可被 CI 直接订阅的 buffer 序列,这意味着它们天然能构成可复合的信号源。 把两个信号 A、B 的组合写成 A∘B,表示先算 B 再算 A。若你在 EA 里先过滤波动率再叠加均线交叉,顺序写反会让入场触发次数出现明显偏差——这是开 MT5 用策略测试器跑同一品种不同顺序能立刻验证的。 外汇与贵金属杠杆高、滑点随机,这种组合偏差可能放大成不可控回撤,调参前先在历史数据里确认态射顺序的敏感性。
「用关系视角看交易系统」
范畴论诞生于 1940 年代,由 Eilenberg 和 Mac Lane 提出,本意不是研究对象本身,而是抽离出对象之间的映射与结构关系。对写 MQL5 的人而言,这套视角可以直接搬过来:把策略、标的、行情状态都看成节点,重点看它们之间的转换规律。 在 MT5 实盘里,这意味着你不该只盯某个 EA 的盈亏曲线,而该去识别不同策略模块之间共有的形态——比如突破逻辑与均值回归在信号层是否有同构关系。抓到这种结构,算法才有可能在行情切换时少改代码。 它还能充当正确性检查器:用严谨的关系语言描述下单、平仓、风控之间的约束,EA 的逻辑漏洞会更早暴露。对外,你和其他交易者聊策略时,也能脱离『参数调得好』这类模糊话术,直接讲结构。外汇与贵金属波动剧烈,高杠杆下任何模型都只是概率优势,不是保本凭证。
◍ 用向量把域和态射搬进 MT5
范畴论里的「域」就是装元素的集合,「态射」是把一个域里的每个元素按规则指到另一个域元素的映射。做交易系统时用得上:把 K 线的时间、收盘价各自看成域,两者之间的对应就是态射,组合多了就能描述行情结构。 实现上别用裸数组。下面这段元素类把基数 cardinal 和向量 element 都锁在 protected 里,外部只能走 Cardinality() 设大小、Get()/Set() 读写,避免越界和手动改尺寸导致的脏数据。 域类再包一层,核心 elements[] 同样受保护,多出一个 New() 做唯一性检查——域里不允许重复对象,这点对后续去重子集运算很关键。跑个脚本填偶数域和奇数域,日志会直接印出 {(2.0),(4.0),…,(14.0)} 和 {(1.0),(3.0),…,(13.0)},证明向量元素本身还能嵌多条目。 态射分五种先记前四种:单态(一对一、协域可有剩)、表态(满射、协域无剩)、同构(双射、两域数量相等)、自同态(映回自身)。同态类里 morphism[] 受保护,加态射时域索引只能用一次、协域索引可重复,符合「域全映射且单射」的规则。 实盘验证可建 time 和 close 两个域,用 MqlRates 填充后调 Morphisms() 建一对一映射,日志能打出具体同态;若随机挑协域值制造重复,图像枚举只回 {(145.847),(145.188)} 两个对象,说明协域三选二。外汇贵金属波动剧烈,这类结构映射只描述关系、不预示方向,拿来写工具前先在 MT5 策略测试器跑通再说。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| ELEMENT CLASS |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> CElement { <span class="keyword">class="kw">protected</span>: <span class="keyword">class="type">int</span> cardinal; <span class="keyword">vector</span> element; <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">bool</span> Cardinality(<span class="keyword">class="type">int</span> Value) { <span class="keyword">if</span>(Value>=<span class="number">class="num">0</span> && Value<<span class="macro">INT_MAX</span>) { cardinal=Value; element.Init(cardinal); <span class="keyword">class="kw">return</span>(<span class="macro">true</span>); } <span class="keyword">class="kw">return</span>(<span class="macro">false</span>); }
用类封装价格样本域的存取边界
在 MT5 里做价格行为分析,常需要把一组样本(如某段 K 线的极值)当成整体来管理。下面这段 MQL5 类结构把单个元素和元素集合分开封装,避免在主逻辑里直接裸写数组越界判断。 CElement 负责单点存取:Cardinality() 返回当前容量,Get() 在索引合法时回传数值、否则给 EMPTY_VALUE,Set() 写值成功返 true。CDomain 则管整个域,Cardinality(int) 用 ArrayResize 把 elements 动态扩到指定大小,上限卡在 INT_MAX 之前。 注意 New() 的注释写明 only unique elements allowed——它遍历已有 cardinal 个元素做 ElementMatch,命中就返回已有下标,没命中返回 -1。这意味着同值样本不会被重复塞进域,回测里若喂入 1000 根bars而其中 300 根极值相同,域实际只存 700 个独立元素,内存和后续匹配开销都更小。 别把正态当圣经:这套封装不保证样本分布,实际行情里黄金 1 小时图常出现聚类极值,域内容量会明显小于输入 bars 数,调参时得先打印 cardinal 确认。
class="type">int Cardinality() { class="kw">return(cardinal); } class="type">class="kw">double Get(class="type">int Index) { if(Index>=class="num">0 && Index<Cardinality()) { class="kw">return(element[Index]); } class="kw">return(EMPTY_VALUE); } class="type">bool Set(class="type">int Index,class="type">class="kw">double Value) { if(Index>=class="num">0 && Index<Cardinality()) { element[Index]=Value; class="kw">return(true); } class="kw">return(false); } CElement(class="type">void) { Cardinality(class="num">0); }; ~CElement(class="type">void) {}; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| DOMAIN CLASS | class=class="str">"cmt">//+------------------------------------------------------------------+ class CDomain { class="kw">protected: class="type">int cardinal; CElement elements[]; class="kw">public: class="type">bool Cardinality(class="type">int Value) { if(Value>=class="num">0 && Value<INT_MAX) { cardinal=Value; ArrayResize(elements,cardinal); class="kw">return(true); } class="kw">return(false); } class="type">int Cardinality() { class="kw">return(cardinal); } class="type">bool Get(class="type">int Index,CElement &Element) { if(Index>=class="num">0 && Index<Cardinality()) { Element=elements[Index]; class="kw">return(true); } class="kw">return(false); } class="type">bool Set(class="type">int Index,CElement &Value,class="type">bool IsNew=false) { if(Index>=class="num">0 && Index<Cardinality()) { if(!IsNew||New(Value)<class="num">0) { elements[Index]=Value; class="kw">return(true); }} class="kw">return(false); } class=class="str">"cmt">//only unique elements allowed class="type">int New(CElement &Value) { class="type">bool _new=-class="num">1; class=class="str">"cmt">// for(class="type">int o=class="num">0; o<cardinal; o++) { if(ElementMatch(Value,elements[o])) { _new=o; break; } } class="kw">return(_new); }
「用类封装偶数集与映射关系」
这段 MT5 脚本把「域(Domain)」和「态射(Morphism)」做成了类,核心不是画 K 线,而是给后续量化建模提供可复用的数学骨架。输入参数 __domain_elements=3 与 __domain_morphisms=5 决定了示例里集合的基数与映射规模,你可以在 EA 输入栏直接改这两个数看输出变化。 OnStart 里先声明 _evens 与 _odds 两个 CDomain 实例,分别用 CreateNumberDomain 生成偶数集与奇数集,步长都是 2,奇数集偏移 1。printf 会把两个集合的内容打到终端,实跑后能看到类似 {0,2,4} 与 {1,3,5} 的打印结果(元素个数由 __domain_elements 控制)。 CMorphism 类则记录了 domain、codomain 索引和具体的 morphism 元素,Morph 方法接收两个域与两个元素引用,通过 D.New(DE) 与 C.New(CE) 在两域中登记元素,Add 默认 true 表示自动补进域里。外汇与贵金属市场高风险,这类结构仅作工具底层,不直接产生交易信号。
CDomain(class="type">void) { Cardinality(class="num">0); }; ~CDomain(class="type">void) {}; }; 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MORPHISM CLASS | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMorphism { class="kw">public: class="type">int domain; class="type">int codomain; CElement morphism; class="type">bool Morph(CDomain &D,CDomain &C,CElement &DE,CElement &CE,class="type">bool Add=true) { class="type">int _d=D.New(DE),_c=C.New(CE);
◍ 映射构造里的同基数判定
当定义域索引 _d 与陪域索引 _c 都非负时,代码先校验 DE 与 CE 两个容器的基数是否相等。若不等,这段逻辑直接跳过,不会生成任何映射关系——这是避免越界读写的第一道闸。 基数一致后,把 domain 与 codomain 锁定为当前 _d、_c,并将 morphism 的基数同步为 DE.Cardinality()。此时若 Add 为真,则按索引遍历,把 morphism 第 c 位写成 CE.Get(c) 减 DE.Get(c);否则走 else 分支准备另一套填充。外汇与贵金属行情里这类差值映射常用于构造价差序列,杠杆品种波动剧烈,实盘前务必在 MT5 策略测试器用小周期样本跑一遍边界。 把下面这段直接贴进 EA 的对应函数,就能复现同基数下的差值映射骨架,改 CE、DE 的来源即可接你自己的指标缓冲。
if(_d>=class="num">0 && _c>=class="num">0) { if(DE.Cardinality()==CE.Cardinality()) { domain=_d; codomain=_c; morphism.Cardinality(DE.Cardinality()); if(Add) { for(class="type">int c=class="num">0;c<morphism.Cardinality();c++) { morphism.Set(c,CE.Get(c)-DE.Get(c)); } } else { for(class="type">int c=class="num">0;c<morphism.Cardinality();c++)