MQL5 中的范畴论 (第 8 部分):幺半群(Monoids)·综合运用
📘

MQL5 中的范畴论 (第 8 部分):幺半群(Monoids)·综合运用

第 3/3 篇

三类极值选取的循环写法

在自定义跟随模块里,对 8 个输入索引两两配对后,有三种不同的输出选取逻辑,分别对应最大值、最近值和最远值。 当模式为取大值时,直接比较配对两项:若前项大于后项则记前索引,小于则记后索引,相等则回退到 m_lookback.Identity() 的默认位。这部分是逐对判断,循环步长为 2,共跑 4 对。 OP_CLOSEST 模式先算配对均值 _mean = 0.5*(Values[i]+Values[i+1]),再在全部 8 个值里找离均值绝对差最小的那个索引;初始 _gap 设为 DBL_MAX,确保首轮必更新。若全程没找到(_index 仍为 -1)才用 Identity() 兜底。 OP_FURTHEST 正好反过来:_gap 从 0.0 起算,保留离均值最远的索引。外汇与贵金属波动无序,这类最远点可能在复盘里对应假突破后的回调触发位,但实盘概率性失效,属高风险用法。 下面这段是原文里 OP_CLOSEST 分支的可用代码,逐行拆解见注释。

MQL5 / C++
else if(O==OP_CLOSEST)
{
  for(class="type">int i=class="num">0;i<class="num">8;i+=class="num">2)
  {
    class="type">int _index=-class="num">1;
    class="type">class="kw">double _mean=class="num">0.5*(Values[InputIndices[i]]+Values[InputIndices[i+class="num">1]]),_gap=DBL_MAX;
    for(class="type">int ii=class="num">0;ii<class="num">8;ii++)
    {
      if(_gap>fabs(_mean-Values[InputIndices[ii]])){ _gap=fabs(_mean-Values[InputIndices[ii]]); _index=ii;}
    }
    class=class="str">"cmt">//
    if(_index==-class="num">1){ _index=m_lookback.Identity(); }
    
    OutputIndices[i/class="num">2]=_index;
  }
}

◍ 从回看缓冲区挑极值位置的写法

这段逻辑干的事很直接:先循环 4 次从 m_lookback 取历史元素填进 Values 数组,取不到就在日志打失败位置。注意 InputIndices 长度必须 ≥4,否则 Values[InputIndices[i]] 会越界,MT5 里大概率直接崩 EA。 填完值后按 O 的枚举分支挑位置。OP_LEAST 是取两两比较里较小的下标,OP_MOST 取较大的,相等时回退到 m_lookback.Identity() 给的默认索引;循环步长 2,所以 4 个元素算出 2 个输出写进 OutputIndices。 OP_CLOSEST 分支多一层:先算相邻两数均值 _mean,再在 4 个元素里找离均值差绝对值最小的那个下标。_gap 初值用 DBL_MAX 保证第一次比较必更新,这种写法在外汇和贵金属高频计算里能避免脏初值,但杠杆品种滑点大时结论仅作参考,实际成交可能偏离。 把下面代码直接贴进 MT5 的 EA 调试段,把 InputIndices 换成你自己的序列,就能看 OutputIndices 怎么随 K 线回看变化。

MQL5 / C++
for(class="type">int i=class="num">0;i<class="num">4;i++)
{
   m_element.Let();
   if(m_lookback.Get(i,m_element))
   {
      class=class="str">"cmt">/*printf(__FUNCSIG__+
       " values size: "+IntegerToString(ArraySize(Values))+
       " in indices size: "+IntegerToString(ArraySize(InputIndices))+
       " in indices index: "+IntegerToString(InputIndices[i])
       );*/
      
      if(!m_element.Get(class="num">0,Values[InputIndices[i]]))
      {
         printf(__FUNCSIG__+" Failed to get class="type">class="kw">double for class="num">1 at: "+IntegerToString(i+class="num">1));
      }
   }
   else{ printf(__FUNCSIG__+" Failed to get element for class="num">1 at: "+IntegerToString(i+class="num">1)); }
}

class=class="str">"cmt">//

if(O==OP_LEAST)
{
   for(class="type">int i=class="num">0;i<class="num">4;i+=class="num">2)
   {
      if(Values[InputIndices[i]]<Values[InputIndices[i+class="num">1]]){ OutputIndices[i/class="num">2]=i; }
      else if(Values[InputIndices[i]]>Values[InputIndices[i+class="num">1]]){ OutputIndices[i/class="num">2]=i+class="num">1; }
      else { OutputIndices[i/class="num">2]=m_lookback.Identity(); }
   }
}
else if(O==OP_MOST)
{
   for(class="type">int i=class="num">0;i<class="num">4;i+=class="num">2)
   {
      if(Values[InputIndices[i]]>Values[InputIndices[i+class="num">1]]){ OutputIndices[i/class="num">2]=i; }
      else if(Values[InputIndices[i]]<Values[InputIndices[i+class="num">1]]){ OutputIndices[i/class="num">2]=i+class="num">1; }
      else { OutputIndices[i/class="num">2]=m_lookback.Identity(); }
   }
}
else if(O==OP_CLOSEST)
{
   for(class="type">int i=class="num">0;i<class="num">4;i+=class="num">2)
   {
      class="type">int _index=-class="num">1;
      class="type">class="kw">double _mean=class="num">0.5*(Values[InputIndices[i]]+Values[InputIndices[i+class="num">1]]),_gap=DBL_MAX;
      for(class="type">int ii=class="num">0;ii<class="num">4;ii++)

「最远点与最近点两种归并逻辑」

CTrailingCT 里 OP_FURTHEST 分支干的事,和 OP_LEAST 正好反过来:前者在 4 个输入值里挑离两两均值最远的索引,后者挑最近的。代码里 _mean 取相邻两值的中值 0.5*(Values[InputIndices[i]]+Values[InputIndices[i+1]]),再扫一遍 ii 从 0 到 3 比较 fabs 距离。 OP_FURTHEST 段用 _gap<fabs(_mean-Values[InputIndices[ii]]) 做放大更新,循环步长 i+=2 说明一次处理一对,OutputIndices[i/2] 落桶。若 _index 仍是 -1,则回退到 m_lookback.Identity() 给默认索引。 Operate_2 是另一套入口:只取 m_lookback 里前 2 个元素,用 m_element.Get(0,Values[InputIndices[i]]) 捞 double,失败就 printf 报 __FUNCSIG__ 加位置。外汇与贵金属行情跳空频繁,这种归并若用在 trailing stop 上,最远点模式可能放大止损被扫的概率,实盘前务必在 MT5 策略测试器跑 EURUSD 或 XAUUSD 的 1H 数据核验。

MQL5 / C++
else if(O==OP_FURTHEST)
{
   for(class="type">int i=class="num">0;i<class="num">4;i+=class="num">2)
   {
      class="type">int _index=-class="num">1;
      class="type">class="kw">double _mean=class="num">0.5*(Values[InputIndices[i]]+Values[InputIndices[i+class="num">1]]),_gap=class="num">0.0;
      for(class="type">int ii=class="num">0;ii<class="num">4;ii++)
      {
         if(_gap<fabs(_mean-Values[InputIndices[ii]])){ _gap=fabs(_mean-Values[InputIndices[ii]]); _index=ii;}
      }
      class=class="str">"cmt">//
      if(_index==-class="num">1){ _index=m_lookback.Identity(); }
      
      OutputIndices[i/class="num">2]=_index;
   }
}

class="type">void CTrailingCT::Operate_2(CMonoid<class="type">class="kw">double> &M,EOperations &O,class="type">class="kw">double &Values[],class="type">int &InputIndices[],class="type">int &OutputIndices[])
  {
   for(class="type">int i=class="num">0;i<class="num">2;i++)
   {
      m_element.Let();
      if(m_lookback.Get(i,m_element))
      {
         if(!m_element.Get(class="num">0,Values[InputIndices[i]]))
         {
            printf(__FUNCSIG__+" Failed to get class="type">class="kw">double for class="num">1 at: "+IntegerToString(i+class="num">1));
         }
      }
      else{ printf(__FUNCSIG__+" Failed to get element for class="num">1 at: "+IntegerToString(i+class="num">1)); }
   }
   
   class=class="str">"cmt">//
   
   if(m_lookback_operation==OP_LEAST)
   {

四种回溯模式下的索引择定逻辑

这段逻辑处理的是双输入场景下,依据不同回溯操作类型(m_lookback_operation)从两个候选值里挑出输出索引。循环步长固定为 2(i+=2),每次只比对 InputIndices[i] 与 InputIndices[i+1] 这一对。 OP_LEAST 模式取较小值所在位置:若 Values[前者] < Values[后者] 则 OutputIndices[0]=i,反之取 i+1,相等时回退到 m_lookback.Identity()。OP_MOST 正好反向,取较大值位置,相等同样走 Identity()。 OP_CLOSEST 先算两者均值 _mean = 0.5*(Values[i]+Values[i+1]),再用 fabs 比距离,谁离均值近就输出谁的下标;初始 _gap 设为 DBL_MAX 保证首轮必中。OP_FURTHEST 则用 _gap=0.0 起手,挑离均值最远的那个。 外汇与贵金属行情跳变频繁,这类择近 / 择远逻辑在震荡段可能频繁翻转输出,实盘前建议在 MT5 用 EURUSD 的 M1 数据跑一遍,观察 Identity() 被触发的频次。

MQL5 / C++
for(class="type">int i=class="num">0;i<class="num">2;i+=class="num">2)
  {
    if(Values[InputIndices[i]]<Values[InputIndices[i+class="num">1]]){ OutputIndices[class="num">0]=i; }
    else if(Values[InputIndices[i]]>Values[InputIndices[i+class="num">1]]){ OutputIndices[class="num">0]=i+class="num">1; }
    else { OutputIndices[class="num">0]=m_lookback.Identity(); }
  }
}
  else if(m_lookback_operation==OP_MOST)
{
  for(class="type">int i=class="num">0;i<class="num">2;i+=class="num">2)
   {
    if(Values[InputIndices[i]]>Values[InputIndices[i+class="num">1]]){ OutputIndices[class="num">0]=i; }
    else if(Values[InputIndices[i]]<Values[InputIndices[i+class="num">1]]){ OutputIndices[class="num">0]=i+class="num">1; }
    else { OutputIndices[class="num">0]=m_lookback.Identity(); }
   }
}
  else if(m_lookback_operation==OP_CLOSEST)
{
  for(class="type">int i=class="num">0;i<class="num">2;i+=class="num">2)
   {
    class="type">int _index=-class="num">1;
    class="type">class="kw">double _mean=class="num">0.5*(Values[InputIndices[i]]+Values[InputIndices[i+class="num">1]]),_gap=DBL_MAX;
    for(class="type">int ii=class="num">0;ii<class="num">2;ii++)
    {
      if(_gap>fabs(_mean-Values[InputIndices[ii]])){ _gap=fabs(_mean-Values[InputIndices[ii]]); _index=ii;}
    }
    class=class="str">"cmt">//
    if(_index==-class="num">1){ _index=m_lookback.Identity(); }
    
    OutputIndices[class="num">0]=_index;
   }
}
  else if(m_lookback_operation==OP_FURTHEST)
{
  for(class="type">int i=class="num">0;i<class="num">2;i+=class="num">2)
   {
    class="type">int _index=-class="num">1;
    class="type">class="kw">double _mean=class="num">0.5*(Values[InputIndices[i]]+Values[InputIndices[i+class="num">1]]),_gap=class="num">0.0;
    for(class="type">int ii=class="num">0;ii<class="num">2;ii++)
    {
      if(_gap<fabs(_mean-Values[InputIndices[ii]])){ _gap=fabs(_mean-Values[InputIndices[ii]]); _index=ii;}

◍ 回看窗口的逐级归并逻辑

CTrailingCT::GetLookback 先刷新收盘价序列,再从 StartIndex 取基准位置 _x,循环 8 次把不同周期落差折算成 0~1 的归一比值。 循环里 _period 用 H4 秒数乘 __LOOKBACKS[i] 再除以当前图周期秒数,把跨周期长度对齐到本图 K 线数;比值分母是两段相邻落差绝对值之和,分子是第一段落差,倾向刻画近端动能在整体摆动里的权重。 赋值失败会走 printf 报 __FUNCSIG__ 和 GetLastError,方便在 MT5 Experts 日志里定位是哪一号 lookback 元素写不进。 后面三段是归并:Operate_8 把 8 个输入压成 4 个输出 _i1_out,Operate_4 再压成 2 个 _i2_out,Operate_2(末段截断前可见声明)准备压成 1 个 _i3_out;每层输出数组先 ArrayInitialize 成 -1,未命中就留 -1,调用前务必确认 Operate_X 内部不会越界读 _v 数组。 外汇与贵金属品种用这套回看做 trailing 触发时波动可能突变,建议先在策略测试器用历史数据验证归并结果再上实盘。

MQL5 / C++
class="type">int CTrailingCT::GetLookback()
  {
    m_close.Refresh(-class="num">1);
    
    class="type">int _x=StartIndex();
    
    for(class="type">int i=class="num">0;i<class="num">8;i++)
    {
      class="type">int _period=(__LOOKBACKS[i]*PeriodSeconds(PERIOD_H4))/PeriodSeconds(m_period);
      class="type">class="kw">double _value=fabs(m_close.GetData(_x)-m_close.GetData(_x+_period))/(fabs(m_close.GetData(_x)-m_close.GetData(_x+_period))+fabs(m_close.GetData(_x+_period)-m_close.GetData(_x+_period+_period)));
      
      m_element.Let();
      m_element.Cardinality(class="num">1);
      if(m_element.Set(class="num">0,_value))
      {
        ResetLastError();
        if(!m_lookback.Set(i,m_element,true))
        {
          printf(__FUNCSIG__+" Failed to assign element at index: "+IntegerToString(i)+", for lookback. ERR: "+IntegerToString(GetLastError()));
        }
      }
    }
    
    class=class="str">"cmt">//r of class="num">8
    class="type">class="kw">double _v1[class="num">8];ArrayInitialize(_v1,class="num">0.0);
    class="type">int _i1_in[class="num">8];for(class="type">int i=class="num">0;i<class="num">8;i++){ _i1_in[i]=i; }
    class="type">int _i1_out[class="num">4];ArrayInitialize(_i1_out,-class="num">1);
    Operate_8(m_lookback,m_lookback_operation,_v1,_i1_in,_i1_out);
    
    class=class="str">"cmt">//r of class="num">4
    class="type">class="kw">double _v2[class="num">8];ArrayInitialize(_v2,class="num">0.0);
    class="type">int _i2_out[class="num">2];ArrayInitialize(_i2_out,-class="num">1);
    Operate_4(m_lookback,m_lookback_operation,_v2,_i1_out,_i2_out);
    
    class=class="str">"cmt">//r of class="num">2
    class="type">class="kw">double _v3[class="num">8];ArrayInitialize(_v3,class="num">0.0);
    class="type">int _i3_out[class="num">1];ArrayInitialize(_i3_out,-class="num">1);

「用波动率比值挑出主行情周期」

CTrailingCT::GetTimeframe 这段逻辑,核心不是看哪个周期涨得多,而是看哪个周期最近两根 K 线的波动,占近三根 K 线总波动的比重更大。

它先循环 8 个周期(i 从 0 到 7),对每个周期取最近 3 根收盘价,算C0-C1/ (C0-C1+C1-C2)。这个值越接近 1,说明最近一段运动比前一段更剧烈,短期方向惯性可能更强。外汇与贵金属杠杆高,这种惯性随时可能反转,仅作周期筛选参考。

算出的比值写进 m_timeframe 这个模糊集合,再用 Operate_8 在 8 个周期里挑出 4 个最显著的(_i1_out),接着 Operate_4 从 4 个里再压到 2 个(_i2_out)。最终返回的,是这层层筛选后胜出周期对应的 LOOKBACKS 权重,不是具体价格。 直接把下面代码丢进 MT5 的 CTrailingCT 类里,打印 __TIMEFRAMES[_i2_out[0]] 和 [_i2_out[1]],就能看到当下算法认为最该盯哪两个周期。

MQL5 / C++
ENUM_TIMEFRAMES CTrailingCT::GetTimeframe(class="type">void)
  {
    for(class="type">int i=class="num">0;i<class="num">8;i++)
    {
       ResetLastError();
       class="type">class="kw">double _value=class="num">0.0;
       class="type">class="kw">double _buffer[];ArrayResize(_buffer,class="num">3);ArrayInitialize(_buffer,class="num">0.0);ArraySetAsSeries(_buffer,true);
       if(CopyClose(m_symbol.Name(),__TIMEFRAMES[i],class="num">0,class="num">3,_buffer)>=class="num">3)
       {
         _value=fabs(_buffer[class="num">0]-_buffer[class="num">1])/(fabs(_buffer[class="num">0]-_buffer[class="num">1])+fabs(_buffer[class="num">1]-_buffer[class="num">2]));
       }
       else{ printf(__FUNCSIG__+" Failed to copy: "+EnumToString(__TIMEFRAMES[i])+" close prices. err: "+IntegerToString(GetLastError())); }
       
       m_element.Let();
       m_element.Cardinality(class="num">1);
       if(m_element.Set(class="num">0,_value))
       {
         ResetLastError();
         if(!m_timeframe.Set(i,m_element,true))
         {
           printf(__FUNCSIG__+" Failed to assign element at index: "+IntegerToString(i)+", for lookback. ERR: "+IntegerToString(GetLastError()));
         }
       }
    }
    
    class=class="str">"cmt">//r of class="num">8
    class="type">class="kw">double _v1[class="num">8];ArrayInitialize(_v1,class="num">0.0);
    class="type">int _i1_in[class="num">8];for(class="type">int i=class="num">0;i<class="num">8;i++){ _i1_in[i]=i; }
    class="type">int _i1_out[class="num">4];ArrayInitialize(_i1_out,-class="num">1);
    Operate_8(m_timeframe,m_timeframe_operation,_v1,_i1_in,_i1_out);
    
    
    class=class="str">"cmt">//r of class="num">4
    class="type">class="kw">double _v2[class="num">8];ArrayInitialize(_v2,class="num">0.0);
    class="type">int _i2_out[class="num">2];ArrayInitialize(_i2_out,-class="num">1);
    Operate_4(m_timeframe,m_timeframe_operation,_v2,_i1_out,_i2_out);

把二级运算结果映射回周期常量

这段片段承接前一步的二级筛选,核心是把数组下标转成可读的时间框架。_v3 先开 8 个双精度槽位并清零,对应可能输出的周期数量上限。 _i3_out 只用 1 个元素并初始化为 -1,作为「未命中」的哨兵值;一旦 Operate_2 跑完仍为负,说明当前组合在该时间框架下没有有效输出。 最后 return 用 _i3_out[0] 去查 __TIMEFRAMES 表,直接吐出周期枚举。你在 MT5 里改 _v3 长度或把 _i3_out 初始化成别的负数,就能观察越界或漏匹配时的返回行为。

MQL5 / C++
  class=class="str">"cmt">//r of class="num">2
  class="type">class="kw">double _v3[class="num">8];ArrayInitialize(_v3,class="num">0.0);
  class="type">int _i3_out[class="num">1];ArrayInitialize(_i3_out,-class="num">1);
  Operate_2(m_timeframe,m_timeframe_operation,_v2,_i2_out,_i3_out);

  class="kw">return(__TIMEFRAMES[_i3_out[class="num">0]]);

◍ 把幺半群当持仓过滤器来用

前面几节把幺半群拆成了数据分类器和决策模块,落到实盘它最直接的用途就是给尾随止损做结构约束。一个定义清楚、不过拟合的幺半群,能让 CTrail 实例只在某些状态切换时移动止损,而不是每根 Tick 都重写止损位。 附带的 ct_8.mqh(64.34 KB)和 TrailingCT8.mqh(35.04 KB)就是这套逻辑的内置实现,直接在 MT5 里 include 后挂到 EA 的尾随类即可。外汇和贵金属波动跳空频繁,这类结构止损仍可能在大滑点中失效,属于高风险用法。 真要验证它普适与否,自己跑一遍 2020—2023 年 XAUUSD 的 M15 回测,对比同参数下裸 ATR 尾随,看成交次数和最大回撤的差值,比读任何说明都实在。

常见问题

从当前索引向前循环到窗口左边界,每次比较极值并保存位置,循环条件设为 i>=start 即可防止越界。
最远点适合识别趋势起点,最近点适合抓回调结束;按波动结构切换即可。
小布可内置波动率比值与主周期识别,打开品种页直接看 AI 标注的极值区和主行情周期,不用手算。
把每种模式的起止索引打印到日志,用历史 K 线肉眼核对极值位置是否吻合。
常用 20~60 根 K 线回看,周期比值突变超过 1.5 倍时大概率进入主行情段。