MQL5 中的范畴论 (第 8 部分):幺半群(Monoids)·综合运用
三类极值选取的循环写法
在自定义跟随模块里,对 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 分支的可用代码,逐行拆解见注释。
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 线回看变化。
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 数据核验。
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() 被触发的频次。
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 触发时波动可能突变,建议先在策略测试器用历史数据验证归并结果再上实盘。
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]],就能看到当下算法认为最该盯哪两个周期。
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 初始化成别的负数,就能观察越界或漏匹配时的返回行为。
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 尾随,看成交次数和最大回撤的差值,比读任何说明都实在。