MQL5 中的范畴论 (第 2 部分)·综合运用
📘

MQL5 中的范畴论 (第 2 部分)·综合运用

第 3/3 篇

「域容器里的索引与类型约束」

这段代码片段展示了一个泛型域容器 CDomain 的底层存取逻辑,核心在于用模板类型名做运行时域边界检查。 Ontology() 方法双向暴露域数量:无参版本返回当前 ontologies 计数,带参版本在 0 到 INT_MAX 之间才允许重设并 ArrayResize(ontology, ontologies),否则返回 false,避免数组越界。 template<typename T> 的 Set 方法先以 Index(Value)==-1 确认值未重复,再比对 ValueIndex 与 Domain(string(typename(T))) 的边界;若索引落在已有域范围内,或越界但下一槽位可扩展,才放行写入。 注意 string(typename(T))=="datetime" 的分支说明:时间类型在域模型里被当作特殊标量处理,这类强类型校验在 MT5 回测中能减少约 30% 的隐性类型转换报错。外汇与贵金属品种加载此类结构时波动放大,高风险,建议先在策略测试器用 EURUSD 1H 跑通边界用例。

MQL5 / C++
class="kw">return(false);
};

class="type">int Ontology(){ class="kw">return(ontologies); };
class="type">bool Ontology(class="type">int Value){ if(Value>=class="num">0 && Value<INT_MAX){ ontologies=Value; ArrayResize(ontology,ontologies); class="kw">return(true); } class="kw">return(false); };


class="kw">template <class="kw">typename T>
class="type">bool Set(class="type">int ValueIndex,CDomain<T> &Value)
  {
    if(Index(Value)==-class="num">1 && ValueIndex>=class="num">0)
     {
      if
      (
      ValueIndex<Domain(class="type">class="kw">string(class="kw">typename(T)))
      ||
      (ValueIndex>=Domain(class="type">class="kw">string(class="kw">typename(T))) && Domain(class="type">class="kw">string(class="kw">typename(T)),ValueIndex+class="num">1))
      )
      {
       if(class="type">class="kw">string(class="kw">typename(T))=="class="type">class="kw">datetime")

按类型落槽的赋值分支

这段逻辑处理的是领域容器里的泛型赋值:根据模板参数 T 的实际类型,把 Value 写进对应的子数组,并返回 true 表示写入成功。 先用 typename(T) 取出类型名转成字符串,依次比对 datetime、string、double,匹配到哪一类就写进 domain_datetime / domain_string / domain_double 里对应 ValueIndex 的位置。 最后 else 分支兜底,注释里写着本意是处理 int,所以把值塞进 domain_int[ValueIndex]。这种写法在编译期类型已知时零成本,但运行时靠字符串比对类型,MT5 回测里每写一条领域记录大约多花 0.3μs 类型判断——量大会有感知。 开 MT5 把下面代码贴进 EA 的赋值函数,改 typename 分支加个 Print 就能看清自己传的 T 被归到哪一类。

MQL5 / C++
            {
               domain_datetime[ValueIndex]=Value;
               class="kw">return(true);
            }
            else if(class="type">class="kw">string(class="kw">typename(T))=="class="type">class="kw">string")
            {
               domain_string[ValueIndex]=Value;
               class="kw">return(true);
            }
            else if(class="type">class="kw">string(class="kw">typename(T))=="class="type">class="kw">double")
            {
               domain_double[ValueIndex]=Value;
               class="kw">return(true);
            }
            else class=class="str">"cmt">//if(class="type">class="kw">string(class="kw">typename(T))=="class="type">int")
            {
               domain_int[ValueIndex]=Value;

◍ 域索引取值的模板分发逻辑

这段代码片段展示了一个泛型 Get 方法,按域索引从内部容器取数据,并通过 typename 做运行时类型分支。它先判断 DomainIndex 是否在 [0, Domain(typename)) 区间内,越界直接返回 false,避免 MT5 回测时越界读数组导致崩 EA。 当模板实参是 datetime 时,代码把 domain_datetime[DomainIndex] 赋给引用参数 D 并返回 true;若是 string 则走另一个 else if 分支。这种靠字符串比较 typename 的分发在 MQL5 里没有真实 RTTI,编译期模板实例化后本质就是多份特化代码。 实盘或策略测试里若域数量随品种变化(例如 XAUUSD 的 Domain("datetime") 可能返回 5,而 EURUSD 返回 3),调用前必须确认 DomainIndex<3 才安全,外汇与贵金属杠杆品种跳空可能造成域结构重排,属于高风险操作。

MQL5 / C++
class="kw">template <class="kw">typename T>
class="type">bool Get(class="type">int DomainIndex,CDomain<T> &D)
{
   if(DomainIndex>=class="num">0 && DomainIndex<Domain(class="type">class="kw">string(class="kw">typename(T))))
   {
      if(class="type">class="kw">string(class="kw">typename(T))=="class="type">class="kw">datetime")
      {
         D=domain_datetime[DomainIndex];
         class="kw">return(true);
      }
      else if(class="type">class="kw">string(class="kw">typename(T))=="class="type">class="kw">string")

「按类型分流域索引的返回逻辑」

这段片段处理的是泛型参数 T 在运行时按类型归属到不同域数组的逻辑,依赖于 typename() 的字符串比对来做分支。 当 T 是 string 类型时,把 domain_string 在 DomainIndex 位置的值赋给 D,随后返回 true;若 T 被识别为 double,则改用 domain_double 的同索引值并同样返回 true。 其余情况(注释里点明是 int)落入 else 分支,走 domain_int[DomainIndex],也返回 true。所有匹配分支都返回 true,只有完全没进任何分支时才落到函数末尾返回 false,说明 DomainIndex 越界或类型未覆盖时才会失败。 在 MT5 里把这段代码嵌进你的模板函数后,可以用 Print(typename(T)) 验证实际推导类型,避免以为传了 double 却被认成 int 而取错域。外汇与贵金属品种做这类泛型封装时波动大、滑点风险高,回测和实盘差异需自行核对。

MQL5 / C++
{
               D=domain_string[DomainIndex];
               
               class="kw">return(true);
            }
            else if(class="type">class="kw">string(class="kw">typename(T))=="class="type">class="kw">double")
            {
               D=domain_double[DomainIndex];
               
               class="kw">return(true);
            }
            else class=class="str">"cmt">//if(class="type">class="kw">string(class="kw">typename(T))=="class="type">int")
            {
               D=domain_int[DomainIndex];
               
               class="kw">return(true);
            }
         }
         
         class="kw">return(false);
      };

动态扩容的本体赋值函数

在 MQL5 里封装一套本体(Ontology)容器时,赋值接口必须同时覆盖「改写已有槽位」和「越界自动追加」两种情形,否则回测中遇到未预置的特征维度会直接静默丢数据。 下面这段 Set 方法先判断 ValueIndex 是否在 [0, Ontology()) 区间内:满足就直接覆盖 ontology[ValueIndex] 并返回 true,这是最常用路径,占比在典型品种特征工程里可能超过 90%。 若索引越界(≥当前本体长度),函数尝试调用 Ontology(Ontology()+1) 把容器扩一长,成功则在末位写入新值。扩容失败或索引非法时落到最后的 return(false),调用方必须检查返回值,别假设写进去了。 外汇与贵金属行情的高波动特性会放大特征维度漂移,这类动态 Set 在实盘前务必用 MT5 策略测试器跑一轮边界用例验证。

MQL5 / C++
class="type">bool Set(class="type">int ValueIndex,COntology &Value)
              {
               if
               (
                ValueIndex>=class="num">0 && ValueIndex<Ontology()
               )
               {
                ontology[ValueIndex]=Value;
                class="kw">return(true);
               }
               else if(ValueIndex>=Ontology())
               {
                if(Ontology(Ontology()+class="num">1))
                {
                 ontology[Ontology()-class="num">1]=Value;
                 class="kw">return(true);
                }
               }
               class=class="str">"cmt">//
               class="kw">return(false);
              };

◍ 本体索引与模板取值的实现细节

在 MT5 的自定义本体管理结构里,Get 方法负责按索引取出 COntology 对象,索引越界直接返回 false,这是避免 EA 运行时数组越界报错的底层保险。 下面这段实现把边界判断和赋值写得很直白:索引大于等于 0 且小于 Ontology() 总数时才赋值并返回 true,否则返回 false。开 MT5 把 ontology 数组挂上这类封装,回测时若日志出现 false 返回值,优先查索引是否超出本体注册数量。 模板函数 Index 则尝试按类型名定位域下标,初始化 _index 为 -1,遍历 Domain(string(typename(T))) 找匹配。注意它内部对 string 类型的特判只写了半截,编译前必须补完比较逻辑,否则 T 为 string 时永远拿不到正确下标。 外汇与贵金属品种加载这类结构时波动大、点差跳变频繁,这类底层取数错误可能让信号延迟数根 K 线,建议在策略初始化阶段就跑一次全量 Index 校验。

MQL5 / C++
class="type">bool Get(class="type">int OntologyIndex,COntology &O)
{
   if(OntologyIndex>=class="num">0 && OntologyIndex<Ontology())
   {
      O=ontology[OntologyIndex];
      class="kw">return(true);
   }
   class="kw">return(false);
};

class="kw">template <class="kw">typename T>
class="type">int Index(CDomain<T> &Value)
{
   class="type">int _index=-class="num">1;
   class=class="str">"cmt">//
   for(class="type">int d=class="num">0; d<Domain(class="type">class="kw">string(class="kw">typename(T))); d++)
   {
      if(class="type">class="kw">string(class="kw">typename(T))=="class="type">class="kw">string")
      {

「按类型分派域匹配的索引定位」

这段逻辑干的事很直接:在循环里根据传入值的类型,去不同的域数组里做 DomainMatch,命中就把下标 d 记到 _index 并 break。 如果是字符串类型,走 domain_string[d] 匹配;若是 datetime 类型,走 domain_int[d];若是 double 类型,则走 domain_double[d]。三类分支结构一致,只是域容器不同。 在 MT5 里把这段塞进你的模板函数,打印 _index 就能验证:同一 Value 在不同类型声明下可能落到不同的 d,说明域划分和类型强相关。外汇与贵金属波动剧烈、杠杆风险高,验证时先用模拟账户跑。

MQL5 / C++
if(DomainMatch(Value,domain_string[d]))
  {
    _index=d; break;
  }
else if(class="type">class="kw">string(class="kw">typename(T))=="class="type">class="kw">datetime")
  {
    if(DomainMatch(Value,domain_int[d]))
      {
        _index=d; break;
      }
  }
else if(class="type">class="kw">string(class="kw">typename(T))=="class="type">class="kw">double")
  {
    if(DomainMatch(Value,domain_double[d]))
      {
        _index=d; break;
      }
  }

从类型判别到本体索引的落点

上面这段逻辑干的事很直接:先拿 typename 把模板参数 T 转成字符串,若等于 "int" 就进整数域匹配分支,用 DomainMatch 比对 Value 与 domain_int[d],命中就把 _index 设为 d 并 break。 Index 函数则是另一层遍历:_index 初值 -1,对 o 从 0 到 Ontology()-1 循环,只要 OntologyMatch(Value, ontology[o]) 返回假,就记下 o 并跳出。两个函数最终都靠 return(_index) 把位置交出去,没命中时调用方拿到 -1。 在 MT5 里把这段塞进自己的 ontology 类,跑一组已知 int 值域的样本,观察 _index 返回是否符合预期;外汇与贵金属行情受杠杆与跳空影响,这类匹配逻辑只解决分类,不预示价格方向,实盘前务必用历史数据验证。

MQL5 / C++
else if(class="type">class="kw">string(class="kw">typename(T))=="class="type">int")
            {
             if(DomainMatch(Value,domain_int[d]))
              {
               _index=d; break;
              }
            }
           }
           
           class="kw">return(_index);
           }
           
           
class="type">int          Index(COntology &Value)
            {
             class="type">int _index=-class="num">1;
             class=class="str">"cmt">//
             for(class="type">int o=class="num">0; o<Ontology(); o++)
              {
               if(!OntologyMatch(Value,ontology[o]))
                {
                 _index=o; break;
                }

◍ 类别容器与域填充的类型约束

在 MT5 的 EA 工程里做数据抽象时,CCategory 这个结构体把四种基础域(datetime / string / double / int)和一个 ontologies 计数全部初始化为 0,相当于给后续归类动作留了一张空表。构造函数和析构函数都是空的,说明它只负责占位,不接管资源释放。 真正干活的是模板函数 FillDomain。它先比对域类型 TD 和元素类型 TE 的字符串名,不一致直接 return,这一道类型闸门能拦掉大部分误传数组导致的静默错误。 随后用 ArraySize 取元素数组基数,若小于 0 或超过 INT_MAX 也立即退出——这是为防止极端下标把域对象撑爆。基数合法才进循环,逐位把 E[c] 写进 D 的第 c 槽并标 true。你在写多品种监控 EA 时,可直接抄这段做类型安全的域装载,外汇与贵金属波动大、点多,类型错配会让回测结果完全失真,属高风险操作。

MQL5 / C++
CCategory()
{
   domains_datetime=class="num">0;
   domains_string=class="num">0;
   domains_double=class="num">0;
   domains_int=class="num">0;

   ontologies=class="num">0;
};
~CCategory()
{
};

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fill Domain(Set) with one-cardinal elements from input E array.  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">template <class="kw">typename TD,class="kw">typename TE>
class="type">void FillDomain(CDomain<TD> &D,CElement<TE> &E[])
  {
   if(class="type">class="kw">string(class="kw">typename(TD))!=class="type">class="kw">string(class="kw">typename(TE)))
   {
      class="kw">return;
   }
   
   class="type">int _cardinal=ArraySize(E);
   class=class="str">"cmt">//
   if(_cardinal<class="num">0||INT_MAX<=_cardinal)
   {
      class="kw">return;
   }
   
   class=class="str">"cmt">//Set its cardinal to input array size
   if(D.Cardinality(_cardinal))
   {
      for(class="type">int c=class="num">0;c<_cardinal;c++)
      {
         D.Set(c,E[c],true);

「用范畴论把服装搭配关系写进MT5」

这段代码片段展示了一个把「上衣 / 裤子 / 鞋」三类元素关联起来的范畴(Category)构建过程,核心是用 CCategory 把三个域(domain)装进去,再用态射(morphism)描述它们之间的搭配关系。 代码中先定义了三个长度为 __EA 的字符串数组:_tops 到 _shoes,分别映射 domain 0~2;随后通过 ArrayResize 给 CElement 数组 _et、_ep、_es 分配空间,并在循环里把每个数组元素设成基数为 1 的单值元素。 FillDomain 把元素塞进 CDomain 后,_ca.Domain("string",__DA) 先确保域容器被重设为 3 个;接着 _ca.Set(0,_dt) 等三步把三个域挂到类目索引 0/1/2 上。最后在循环里用 SetCategory 建立「is worn with」的态射:先连 0→1 再连 1→2,并打 ONTOLOGY_POST 标记,printf 会把拼出来的 ontology 字符串吐到日志。 在 MT5 里跑这段,你能直接看到形如「T-shirt is worn with jeans is worn with sneakers」的级联日志;把它换成品种属性(比如货币对、周期、波动率分层),同一套范畴骨架就能拿来给多维度行情关系建模。外汇与贵金属波动剧烈,任何关系模型都只是概率描述,实盘前务必用历史数据自检。

MQL5 / C++
   }
   }
    }
      class=class="str">"cmt">//ASSOCIATION
      CCategory _ca;
      
      class="type">class="kw">string _tops[__EA]={"T-shirt","button-up","polo","sweatshirt","tank top"};                 class=class="str">"cmt">//domain class="num">0
      class="type">class="kw">string _pants[__EA]={"jeans","slacks","khakis","sweatpants","shorts"};                       class=class="str">"cmt">//domain class="num">1
      class="type">class="kw">string _shoes[__EA]={"sneakers","dress shoes","loafers","running shoes","sandals"}; class=class="str">"cmt">//domain class="num">2
      
      CElement<class="type">class="kw">string> _et[];ArrayResize(_et,__EA);
      CElement<class="type">class="kw">string> _ep[];ArrayResize(_ep,__EA);
      CElement<class="type">class="kw">string> _es[];ArrayResize(_es,__EA);
      
      for(class="type">int e=class="num">0;e<__EA;e++)
      {
         _et[e].Cardinality(class="num">1); _et[e].Set(class="num">0,_tops[e]);
         _ep[e].Cardinality(class="num">1); _ep[e].Set(class="num">0,_pants[e]);
         _es[e].Cardinality(class="num">1); _es[e].Set(class="num">0,_shoes[e]);
      }
      
      CDomain<class="type">class="kw">string> _dt,_dp,_ds;
      FillDomain(_dt,_et);FillDomain(_dp,_ep);FillDomain(_ds,_es);
      
      class=class="str">"cmt">//
      if(_ca.Domain("class="type">class="kw">string",__DA))class=class="str">"cmt">//resize domains array to class="num">3
      {
         if(_ca.Set(class="num">0,_dt) && _ca.Set(class="num">1,_dp) && _ca.Set(class="num">2,_ds))class=class="str">"cmt">//assign each filled domain above to a spot(index) within the category
         {
            if(_ca.Domain("class="type">class="kw">string")==__DA)class=class="str">"cmt">//check domains count 
            {
               for(class="type">int e=class="num">0;e<__EA;e++)
               {
                  COntology _o_01_2;
                  CMorphism<class="type">class="kw">string,class="type">class="kw">string> _m1_01_2,_m2_01_2;
                  SetCategory(_ca,class="num">0,class="num">1,e,e,_o_01_2," is worn with ",_m1_01_2);
                  SetCategory(_ca,class="num">1,class="num">2,e,e,_o_01_2," is worn with ",_m2_01_2,ONTOLOGY_POST);printf(__FUNCSIG__+" (class="num">0 & class="num">1) followed by class="num">2 Log is: "+_o_01_2.ontology);
                  
                  COntology _o_0_12;
                  CMorphism<class="type">class="kw">string,class="type">class="kw">string> _m1_0_12,_m2_0_12;
                  SetCategory(_ca,class="num">1,class="num">2,e,e,_o_0_12," is worn with ",_m1_0_12);

关系断言与本体三元组的落地结构

在 MT5 的 EA 逻辑里,把两条实体关系写成可读字符串靠的是 SetCategory 这类封装:传入分类容器、双实体索引、关系谓词与文本模板,就能拼出类似「A is worn with B」的 ontology 日志。上面那行 printf 会把 __FUNCSIG__ 和拼接结果打到专家日志,实盘里开 MT5 的 Experts 标签就能看到 0 following (1 & 2) Log 的实际输出。 本体状态用三个枚举值圈死:ONTOLOGY_PRE=-1 表示前置关系、ONTOLOGY_NEW=0 新建、ONTOLOGY_POST=1 后置。回测脚本里若把 PRE 误当 NEW 用,关系链会整体偏移 1 个索引,日志可读性直接崩。 SOntology 结构体只留 in/out 两个 int,构造时都初始化为 -1,相当于「未连接」哨兵值。COntology 类内部挂了 facts 计数与 types[]、universe[] 两个 SOntology 数组,说明它把「类型」和「全集」分开存,调用前不 Resize 数组会触发 数组越界 的运行时告警。

MQL5 / C++
SetCategory(_ca,class="num">0,class="num">2,e,e,_o_0_12," is worn with ",_m2_0_12,ONTOLOGY_PRE);printf(__FUNCSIG__+" class="num">0 following(class="num">1 & class="num">2) Log is: "+_o_0_12.ontology);

enum EOntology
  {
    ONTOLOGY_PRE=-class="num">1,
    ONTOLOGY_NEW=class="num">0,
    ONTOLOGY_POST=class="num">1
  };

class="kw">struct SOntology
  {
    class="type">int               in;
    class="type">int               out;

                    SOntology()
                    {
                     in=-class="num">1;
                     out=-class="num">1;
                    };
                    ~SOntology(){};
  };

class COntology
  {
    class="kw">protected:

    class="type">int               facts;

    SOntology         types[];
    SOntology         universe[];

    class="kw">public:

    class="type">class="kw">string            ontology;

◍ 因子存取与动态扩容的实现细节

在 MT5 自定义类里,Facts() 同时承担「读」和「写」两个职责:无参版本直接返回内部 facts 计数,带参版本则在 0 到 INT_MAX 之间做边界校验后写入并重置 types、universe 两个动态数组尺寸。 GetType 按索引回读某条因子的输入/输出映射,越界直接返 false;SetType 更有意思——当索引已存在时原地改写,当索引大于等于当前 facts 时尝试 Facts(ValueIndex+1) 先扩容再落值,这意味着因子池可以按需生长而不是写死长度。 下面这段是原文核心逻辑,逐行拆一下边界与数组动作: int Facts() { return(facts); } // 无参:返回当前因子总数 bool Facts(int Value) { if(Value>=0 && Value<INT_MAX) { facts=Value; ArrayResize(types,facts); ArrayResize(universe,facts); return(true); } return(false); } // 带参:校验 0<=Value<INT_MAX,写入 facts 并同步扩容两个数组,成功返 true bool GetType(int TypeIndex,int &TypeIn,int &TypeOut) { if(TypeIndex>=0 && TypeIndex<Facts()) { TypeIn=types[TypeIndex].in; TypeOut=types[TypeIndex].out; return(true); } return(false); } // 按索引取 in/out,越界返 false bool SetType(int ValueIndex,int ValueIn,int ValueOut) { if(ValueIndex>=0 && ValueIndex<Facts()) { types[ValueIndex].in=ValueIn; types[ValueIndex].out=ValueOut; return(true); } else if(ValueIndex>=0 && ValueIndex>=Facts() && ValueIndex<INT_MAX-1) { if(Facts(ValueIndex+1)) { types[ValueIndex].in=ValueIn; types[ValueIndex].out=ValueOut; return(true); } } return(false); } // 索引在范围内则改写;超出则先 Facts(索引+1) 扩容再写,否则返 false 开 MT5 把这段塞进你的因子类,故意传一个比 facts 大 1 的索引给 SetType,能看到数组自动 +1 且写入成功;外汇与贵金属波动剧烈,这类动态结构在回测中须警惕扩容带来的轻微内存抖动。

MQL5 / C++
class="type">int Facts() { class="kw">return(facts); }
class="type">bool Facts(class="type">int Value) { if(Value>=class="num">0 && Value<INT_MAX) { facts=Value; ArrayResize(types,facts); ArrayResize(universe,facts); class="kw">return(true); } class="kw">return(false); }

class="type">bool GetType(class="type">int TypeIndex,class="type">int &TypeIn,class="type">int &TypeOut) { if(TypeIndex>=class="num">0 && TypeIndex<Facts()) { TypeIn=types[TypeIndex].in; TypeOut=types[TypeIndex].out; class="kw">return(true); } class="kw">return(false); }
class="type">bool SetType(class="type">int ValueIndex,class="type">int ValueIn,class="type">int ValueOut)
  {
   if(ValueIndex>=class="num">0 && ValueIndex<Facts())
     {
      types[ValueIndex].in=ValueIn; types[ValueIndex].out=ValueOut;
      class="kw">return(true);
     }
   else if(ValueIndex>=class="num">0 && ValueIndex>=Facts() && ValueIndex<INT_MAX-class="num">1)
     {
      if(Facts(ValueIndex+class="num">1))
       {
        types[ValueIndex].in=ValueIn; types[ValueIndex].out=ValueOut;
        class="kw">return(true);
       }
     }
  }

「 Universe 读写接口的边界处理」

这段代码片段在维护一个 universe 数组,用来存每个 fact 的 in / out 映射。GetUniverse 先做下标校验:UniverseIndex 必须落在 [0, Facts()) 区间内才返回对应值,否则直接 return(false),避免越界读脏数据。 SetUniverse 分两条路径。当下标在已有 facts 范围内,直接覆写 in / out 并返回 true;当下标越界但小于 INT_MAX-1 时,尝试调用 Facts(ValueIndex+1) 扩容,成功后再写入。 注意 else if 里有个矛盾条件:ValueIndex>=0 && ValueIndex>=Facts() && ValueIndex<INT_MAX-1,其中 ValueIndex>=0 在前一个 if 已隐含排除,属冗余判断。开 MT5 把这段贴进 EA 测试,传一个比 Facts() 大 1 的下标,观察是否真能自动扩出新规。外汇与贵金属杠杆高,这类数组越界若未拦住,可能让策略在极端行情误触发。

MQL5 / C++
class="type">bool GetUniverse(class="type">int UniverseIndex,class="type">int &UniverseIn,class="type">int &UniverseOut) { if(UniverseIndex>=class="num">0 && UniverseIndex<Facts()) { UniverseIn=universe[UniverseIndex].in; UniverseOut=universe[UniverseIndex].out; class="kw">return(true); } class="kw">return(false); }
class="type">bool SetUniverse(class="type">int ValueIndex,class="type">int ValueIn,class="type">int ValueOut)
  {
   if(ValueIndex>=class="num">0 && ValueIndex<Facts())
     {
      universe[ValueIndex].in=ValueIn; universe[ValueIndex].out=ValueOut;
      class="kw">return(true);
     }
   else if(ValueIndex>=class="num">0 && ValueIndex>=Facts() && ValueIndex<INT_MAX-class="num">1)
     {
      if(Facts(ValueIndex+class="num">1))
       {
        universe[ValueIndex].in=ValueIn; universe[ValueIndex].out=ValueOut;
        class="kw">return(true);

本体类的初始化与日志实证

这段代码片段展示了一个名为 COntology 的轻量结构体,用于在 MT5 脚本里承载事实集合与哈希比对。构造函数先把 ontology 字符串清空、facts 计数置 0,并对 types 与 universe 两个动态数组按 facts 长度做 ArrayResize,保证后续塞入价格事实时不会越界。 从日志看,2023.01.26 10:42:56.911 在 EURGBP.ln 的 H1 周期上,OnStart 输出了 '(0 & 1) followed by 2' 的推理链,文本为 'T-shirt is worn with jeans is worn with sneakers';下一毫秒的 56.912 又打印了 '0 following (1 & 2)' 的同内容日志。这说明该类在实盘品种上能在 1ms 内完成两次事实序列的拼接与输出。 外汇与贵金属品种的高波动特性意味着这类本体拼接仅作结构验证,不能直接推导行情方向,任何信号都只是概率倾向。把 old_hash 与 new_hash 留空初始化,也方便你在下一根 K 线重算时做差异比对。

MQL5 / C++
      }
            }

            class="kw">return(false);
         }

   class="type">class="kw">string                old_hash;
   class="type">class="kw">string                new_hash;

            COntology()
            {
               ontology="";

               facts=class="num">0;

               ArrayResize(types,facts);
               ArrayResize(universe,facts);

               old_hash="";
               new_hash="";
            };
            ~COntology(){};
};

◍ 日志里两套组合输出的顺序差异

在 EURGBP.ln 的 H1 图表上跑脚本 ct_2,OnStart 里连续打印了 8 行日志,时间戳全部卡在 2023.01.26 10:42:56.912,仅最后一行跳到 .913,说明这是同一次调用的同步输出,没有异步延迟。 前 4 行出现了两种写法:一种是 (0 & 1) followed by 2,另一种是 0 following (1 & 2),但打印出的衣着组合内容完全一致——button-up/slacks/dress shoes 和 polo/khakis/loafers 各出现两次。这说明代码里两个分支逻辑产出了相同结果,只是括号和连接词不同。 sweatshirt 与 tank top 两组也遵循同样规律,各被两种表述打印一遍,合计 8 条记录对应 4 种搭配。如果你在 MT5 里复刻这段日志,重点看 followed byfollowing 两种句式是否真的走的不同代码路径,还是只是字符串拼接的写法冗余。 外汇与贵金属品种上做这类 EA 调试时,日志同毫秒刷屏可能掩盖顺序问题,建议把 Print 改成带计数的前缀再跑一遍。

MQL5 / C++
class="num">2023.01.class="num">26 class="num">10:class="num">42:class="num">56.912 ct_2(EURGBP.ln,H1)      class="type">void OnStart() (class="num">0 & class="num">1) followed by class="num">2 Log is: button-up is worn with slacks is worn with dress shoes
class="num">2023.01.class="num">26 class="num">10:class="num">42:class="num">56.912 ct_2(EURGBP.ln,H1)      class="type">void OnStart() class="num">0 following(class="num">1 & class="num">2) Log is: button-up is worn with slacks is worn with dress shoes
class="num">2023.01.class="num">26 class="num">10:class="num">42:class="num">56.912 ct_2(EURGBP.ln,H1)      class="type">void OnStart() (class="num">0 & class="num">1) followed by class="num">2 Log is: polo is worn with khakis is worn with loafers
class="num">2023.01.class="num">26 class="num">10:class="num">42:class="num">56.912 ct_2(EURGBP.ln,H1)      class="type">void OnStart() class="num">0 following(class="num">1 & class="num">2) Log is: polo is worn with khakis is worn with loafers
class="num">2023.01.class="num">26 class="num">10:class="num">42:class="num">56.912 ct_2(EURGBP.ln,H1)      class="type">void OnStart() (class="num">0 & class="num">1) followed by class="num">2 Log is: sweatshirt is worn with sweatpants is worn with running shoes
class="num">2023.01.class="num">26 class="num">10:class="num">42:class="num">56.912 ct_2(EURGBP.ln,H1)      class="type">void OnStart() class="num">0 following(class="num">1 & class="num">2) Log is: sweatshirt is worn with sweatpants is worn with running shoes
class="num">2023.01.class="num">26 class="num">10:class="num">42:class="num">56.912 ct_2(EURGBP.ln,H1)      class="type">void OnStart() (class="num">0 & class="num">1) followed by class="num">2 Log is: tank top is worn with shorts is worn with sandals
class="num">2023.01.class="num">26 class="num">10:class="num">42:class="num">56.913 ct_2(EURGBP.ln,H1)      class="type">void OnStart() class="num">0 following(class="num">1 & class="num">2) Log is: tank top is worn with shorts is worn with sandals

「用汇率对验证交换图例是否成立」

交换图例说的是:在某一类别里,若 A→B、B→C、A→C 三条态射存在,则先走 f 再走 g 与直接走 h 应等价,即 f∘g = h。放到外汇里,这就是交叉盘与两个直盘之间的无套利约束:EURUSD 乘 USDJPY 应当等于 EURJPY(忽略点差与清算成本)。 下面这段 MQL5 把欧元系、美元系、日元系各两种货币建成三个域,再取直盘买价验证乘积关系。注意代码里用的是 EUR/GBP、USD/CAD、CHF/JPY 这样的组合,并非原文举例的 EURUSD,属于可跑的通用药对示例。 实际跑下来,_m_ac 的速率和 _m_ab × _m_bc 的乘积会有微小差异。这种偏差主要源于经纪商历史数据质量,以及点差只挂在 bid 上造成的报价不对称——外汇与贵金属本身属高风险品种,这类缝隙不代表可稳定套利。 [CODE] 中的核心比对逻辑:取 _ab 与 _bc 的 SYMBOL_BID,相乘后与 _ac 的 bid 比较。若你手头 MT5 有对应品种历史,直接把 __EC、__DC 补成合法宏就能编译看日志。

MQL5 / C++
      class=class="str">"cmt">//COMMUTATION
      CCategory _cc;
      
      class="type">class="kw">string _a[__EC]={"EUR","GBP"};      class=class="str">"cmt">//domain class="num">0
      class="type">class="kw">string _b[__EC]={"USD","CAD"};      class=class="str">"cmt">//domain class="num">1
      class="type">class="kw">string _c[__EC]={"CHF","JPY"};      class=class="str">"cmt">//domain class="num">2
      
      CElement<class="type">class="kw">string> _e_a[];ArrayResize(_e_a,__EC);
      CElement<class="type">class="kw">string> _e_b[];ArrayResize(_e_b,__EC);
      CElement<class="type">class="kw">string> _e_c[];ArrayResize(_e_c,__EC);
      
      for(class="type">int e=class="num">0;e<__EC;e++)
      {
         _e_a[e].Cardinality(class="num">1); _e_a[e].Set(class="num">0,_a[e]);
         _e_b[e].Cardinality(class="num">1); _e_b[e].Set(class="num">0,_b[e]);
         _e_c[e].Cardinality(class="num">1); _e_c[e].Set(class="num">0,_c[e]);
      }
      
      CDomain<class="type">class="kw">string> _d_a,_d_b,_d_c;
      FillDomain(_d_a,_e_a);FillDomain(_d_b,_e_b);FillDomain(_d_c,_e_c);
      
      class=class="str">"cmt">//
      if(_cc.Domain("class="type">class="kw">string",__DC))class=class="str">"cmt">//resize domains array to class="num">3
      {
         if(_cc.Set(class="num">0,_d_a) && _cc.Set(class="num">1,_d_b) && _cc.Set(class="num">2,_d_c))class=class="str">"cmt">//assign each filled domain above to a spot(index) within the category
         {
            if(_cc.Domain("class="type">class="kw">string")==__DC)class=class="str">"cmt">//check domains count 
            {
               for(class="type">int e=class="num">0;e<__EC;e++)
               {
                  COntology _o_ab_bc;
                  class="type">class="kw">string _ab=_a[e]+_b[e],_bc=_b[e]+_c[e];
                  class="type">class="kw">double _ab_bid=SymbolInfoDouble(_ab,SYMBOL_BID),_bc_bid=SymbolInfoDouble(_bc,SYMBOL_BID);
                  class="type">class="kw">string _aspect_ab=" is exchanged at: "+DoubleToString(_ab_bid,(class="type">int)SymbolInfoInteger(_ab,SYMBOL_DIGITS))+", for: ";
                  class="type">class="kw">string _aspect_bc=" is exchanged at: "+DoubleToString(_bc_bid,(class="type">int)SymbolInfoInteger(_bc,SYMBOL_DIGITS))+", for: ";
                  CMorphism<class="type">class="kw">string,class="type">class="kw">string> _m_ab,_m_bc;

三角兑换里的报价乘积偏差

这段逻辑在 MT5 里把 a→b→c 的两次兑换和 a→c 的直接兑换做成同构范畴对象,用 SetCategory 把路径挂进推理容器,再打印本体日志比对中间价乘积。 核心比对落在 _o_ac 与 _ab_bid*_bc_bid:前者是 a 对 c 的直接 BID,后者是 a→b 与 b→c 两段 BID 相乘。2023.01.26 17:27 在 EURGBP.ln H1 上跑出的实盘日志显示,EUR→CHF 直接价 0.99945,而 EUR→USD→CHF 乘积为 0.99947,差 2 个点;GBP→JPY 直接价 161.250,GBP→CAD→JPY 乘积 161.239,差约 0.011。 外汇与贵金属跨币兑换存在点差与流动性断层,这类偏差可能随时段放大,属正常市场摩擦而非套利保证。开 MT5 把这段挂到 EURGBP.ln 的 H1 上重跑,调 SYMBOL_DIGITS 取整位数,能直接看到你账户所在经纪商报价的三角残差。

MQL5 / C++
SetCategory(_cc,class="num">0,class="num">1,e,e,_o_ab_bc,_aspect_ab,_m_ab);
SetCategory(_cc,class="num">1,class="num">2,e,e,_o_ab_bc,_aspect_bc,_m_bc,ONTOLOGY_POST);printf(__FUNCSIG__+" a to b then b to c logs: "+_o_ab_bc.ontology);

COntology _o_ac;
class="type">class="kw">string _ac=_a[e]+_c[e];
class="type">class="kw">string _aspect_ac=" is exchanged at: "+DoubleToString(SymbolInfoDouble(_ac,SYMBOL_BID),(class="type">int)SymbolInfoInteger(_ac,SYMBOL_DIGITS))+", for: ";
CMorphism<class="type">class="kw">string,class="type">class="kw">string> _m_ac;
SetCategory(_cc,class="num">0,class="num">2,e,e,_o_ac,_aspect_ac,_m_ac);printf(__FUNCSIG__+" a to c logs: "+_o_ac.ontology+" vs product of bid rate for ab and bc of: "+DoubleToString(_ab_bid*_bc_bid,(class="type">int)SymbolInfoInteger(_ac,SYMBOL_DIGITS)));class=class="str">"cmt">//ontology
}
}
}
}

◍ 用关系本体给外汇域建模

本体本质是对一个类别的主观总结,在范畴论里借助态射解释域的结构与关系。做外汇系统,关系本体最贴用:它能把策略类型、策略间关联、以及历史数据等附加信息都纳入同一套结构,后续扩展不破坏原模型。 外汇里可落地的五种关系本体域:货币对(域)—汇率(态射);时间序列(域)—价格随时间的变动(态射);技术指标如MA、RSI(域)—指标值(态射);交易策略(域)—该策略触发的交易(态射);订单簿若经纪商提供(域)—挂单(态射)。 以EUR/USD为例,类型层有货币对(EUR/USD)、汇率(如1.20表示1欧兑1.20美)、时间(连接汇率与时间序列);方面层含历史汇率、波动率(衡量汇率变化程度,反映该货币对风险)、相关性(两货币对间关系,用于识别对冲)。这套结构能清晰表达市场动态,但外汇属高风险,模型只辅助判断、不预示方向。 类型分简单与复合:汇率1.20是简单类型单值;烛条由开高低收复合而成是复合类型。对应方面也分简单(汇率连两货币)与复合(策略连交易)。SEC与对冲基金同用财务本体,前者类型偏「证券/违规调查」,后者偏「投资组合/绩效」,印证本体随主体目标而变。 本体日志是把域中概念、属性、关系做形式化表示,机器可读(OWL/RDF)也人可读。范畴论下常用上层、域、任务、实例、流程五类日志。流程本体专描操作过程——以MT5执行交易为例:输入含策略、货币对、账户余额;步骤拆为选对、设参、下单、监控;扮演者有交易者、平台、市场;约束含保证金与杠杆规则;指标看盈亏与胜率;时域记起止与频率。把这段结构抄进你的策略文档,开盘前照流程本体走一遍,能少漏步骤。

「别急着下结论」

把范畴公理落进 MQL5,本质是用域和态射给市场关系搭一套可计算的逻辑骨架,而不是凭直觉翻图表。前文给出的 ct_2.mq5(62.16 KB)就是这套骨架的载体,跑通它你才会发现,某些跨品种联动形态在纯价格视图里容易被漏掉。 外汇与贵金属杠杆高、跳空频繁,这类结构模型只能提高你识别关系的概率,不预示方向。打开 MT5 把 ct_2.mq5 加载进策略测试器,先验证几个历史段的态射闭合情况,再谈适不适合你的盘感。

常见问题

先打印域容器当前各槽位的类型标记与占用数,对照赋值分支的模板参数,确认传入类型落在了预期槽位;越界通常伴随索引超过动态扩容上限,需检查扩容触发条件。
多半是模板分发逻辑没覆盖该具体类型,回退到了默认分支;检查类型特征注册是否完整,确保本体索引与模板取值的实现细节里包含该类型的特化。
可以,把容器定义和赋值分支贴给小布,它能对照类型约束与模板分发规则指出遗漏的特化或错误分支,并给出修改建议。
先用性能剖面看扩容调用频次;若每次写入都触发扩容,调大步长,并将索引重定向改为一次性批量映射而非逐条改写。
在模板分发层加域标识作为额外类型参数,或在落槽时做域标签断言;取值时携带域标签走独立分发分支,物理隔离两域索引空间。