模式搜索的暴力方法(第二部分):深入·进阶篇
🔍

模式搜索的暴力方法(第二部分):深入·进阶篇

(2/3)· 仅靠利润因子排序会漏掉大量优质形态,引入形状相似度后结果结构彻底改变

含代码示例偏理论 第 2/3 篇
很多交易者以为暴力搜出的高预期收益图形就是靠谱信号,却从没评估过曲线形状本身。把弯曲杂乱的走势当成模板去匹配,回测好看实盘却容易失效。机器不挑形状,人得替它加这道筛子。

「多维因子重构下的模板函数改造」

逻辑升级后,模板里的计算函数必须能吃下新参数:原先只处理 1 个数值,现在同时跑 4 个独立乘数,公式搜索能力随数据维度上升而提高,但代价是组合可能重复。 写代码时暴露出一个缺陷——多维多项式的调用树不控制因子排列,导致相同因子组合被重复计入。两种修法:枚举所有排列,或用总系数除以重复因子个数做补偿。我选了后者,副作用是平均系数会向 0.5 收敛,但因子至少不会失效。 固定总次数下乘法器组合数可直接算:Comb = n! / (n-k)!,n 是独立乘数数目,k 是总次数。下面这段就是组合数的 MQL5 实现,注意它用两次循环规避了完整阶乘溢出。 [CODE] double Combinations(int k,int n) { int i1=1; int i2=1; for ( int i=n; i<n-k; i-- ) i1*=i; for ( int i=n-k; i>1; i-- ) i2*=i; return double(i1/i2); } [/CODE] 逐行拆解:第 3–4 行初始化分子 i1、分母 i2 为 1;第 5 行从 n 累乘到 n-k+1 填进 i1;第 6 行从 n-k 累乘到 2 填进 i2;第 7 行返回 i1/i2 即组合数。 主多项式函数 PolinomTrade 现在依赖 5 个柱面参数而非 1 个:4 个初始数组外加价格差分。代码里把相邻开盘/收盘、最高/开盘、开盘/最低、最高/收盘、收盘/最低的差都除以 Point 后乘系数累加,循环长度已是旧版的 5 倍。 这类递归树用 OOP 反而笨重,用类 C 的平铺函数建调用树更直白,虽然逻辑绕但能跑。配套 C# 原型还在内部用旧版逻辑,目前不适合普通 PC 做全局模式扫描,属于半成品。外汇与贵金属品种用这套做因子扩展时波动剧烈,高风险,参数没调稳前别直接上实盘。

MQL5 / C++
class="type">class="kw">double Combinations(class="type">int k,class="type">int n)
  {
  class="type">int i1=class="num">1;
  class="type">int i2=class="num">1;
  for ( class="type">int i=n; i<n-k; i-- ) i1*=i;
  for ( class="type">int i=n-k; i>class="num">1; i-- ) i2*=i;
  class="kw">return class="type">class="kw">double(i1/i2);
  }
class="type">class="kw">double Val;
class="type">int iterator;
class="type">class="kw">double PolinomTrade()class=class="str">"cmt">//Polynomial for trading
  {
  Val=class="num">0;
  iterator=class="num">0;
  if ( DeepBruteX <= class="num">1 )
    {
    for ( class="type">int i=class="num">0; i<CNum; i++ )
      {
      Val+=C1[iterator]*(Close[i+class="num">1]-Open[i+class="num">1])/Point;
      iterator++;
      }
    for ( class="type">int i=class="num">0; i<CNum; i++ )
      {
      Val+=C1[iterator]*(High[i+class="num">1]-Open[i+class="num">1])/Point;
      iterator++;
      }
    for ( class="type">int i=class="num">0; i<CNum; i++ )
      {
      Val+=C1[iterator]*(Open[i+class="num">1]-Low[i+class="num">1])/Point;
      iterator++;
      }
    for ( class="type">int i=class="num">0; i<CNum; i++ )
      {
      Val+=C1[iterator]*(High[i+class="num">1]-Close[i+class="num">1])/Point;
      iterator++;
      }
    for ( class="type">int i=class="num">0; i<CNum; i++ )
      {
      Val+=C1[iterator]*(Close[i+class="num">1]-Low[i+class="num">1])/Point;
      iterator++;
      }
    class="kw">return Val;  
    }
  else
    {
    CalcDeep(C1,CNum,DeepBruteX);
    class="kw">return ValStart;
    }
  }
class=class="str">"cmt">///Fractal calculation of numbers
class="type">class="kw">double ValW;class=class="str">"cmt">//the number where everything is multiplied(and then added to ValStart)
class="type">uint NumC;class=class="str">"cmt">//the current number for the coefficient
class="type">class="kw">double ValStart;class=class="str">"cmt">//the number where to add everything
class="type">void Deep(class="type">class="kw">double &Ci0[],class="type">int Nums,class="type">int deepC,class="type">int deepStart,class="type">class="kw">double Val0=class="num">1.0)class=class="str">"cmt">//intermediary fractal
  {
  for ( class="type">int i=class="num">0; i<Nums; i++ )
    {
    if (deepC > class="num">1)
      {

◍ 五类价格波幅的递归加权累加

这段逻辑把单根 K 线的价格运动拆成五种典型波幅:收盘减开盘、最高减开盘、开盘减最低、最高减收盘、收盘减最低,分别作为递归入口的权重基值 ValW。 当递归深度 deepC 大于 1 时,代码把当前波幅乘上 Val0 后继续往 Deep() 里塞,让组合在更深层展开;若 deepC 已到 1,则直接把 Ci0[NumC] 除以组合数 Combinations(deepStart, Nums*5) 再做归一化累加,并自增 NumC 指针。 注意分母里写死 Nums*5:因为总共恰好枚举了 5 种波幅结构,所以组合空间按 5 倍 Nums 来配。外汇与贵金属杠杆高,这类递归权重若直接拿来出信号,需先在小周期回测验证稳定性,价格行为存在跳空时可能偏离。 CalcDeep() 是总入口,先把 NumC 和 ValStart 归零,再按 i 从 0 到 deepC-1 循环调用 Deep(),每层 deepStart 与 deepC 同步设为 i+1,相当于由浅入深把各阶组合都扫一遍。

MQL5 / C++
      ValW=(Close[i+class="num">1]-Open[i+class="num">1])*Val0;
      Deep(Ci0,Nums,deepC-class="num">1,deepStart,ValW);
      }
      else
      {
      ValStart+=(Ci0[NumC]/Combinations(deepStart,Nums*class="num">5))*(Close[i+class="num">1]-Open[i+class="num">1])*Val0/Point;
      NumC++;
      }
      }
      
   for ( class="type">int i=class="num">0; i<Nums; i++ )
      {
      if (deepC > class="num">1)
      {
      ValW=(High[i+class="num">1]-Open[i+class="num">1])*Val0;
      Deep(Ci0,Nums,deepC-class="num">1,deepStart,ValW);
      }
      else
      {
      ValStart+=(Ci0[NumC]/Combinations(deepStart,Nums*class="num">5))*(High[i+class="num">1]-Open[i+class="num">1])*Val0/Point;
      NumC++;
      }
      }
   for ( class="type">int i=class="num">0; i<Nums; i++ )
      {
      if (deepC > class="num">1)
      {
      ValW=(Open[i+class="num">1]-Low[i+class="num">1])*Val0;
      Deep(Ci0,Nums,deepC-class="num">1,deepStart,ValW);
      }
      else
      {
      ValStart+=(Ci0[NumC]/Combinations(deepStart,Nums*class="num">5))*(Open[i+class="num">1]-Low[i+class="num">1])*Val0/Point;
      NumC++;
      }
      }
   for ( class="type">int i=class="num">0; i<Nums; i++ )
      {
      if (deepC > class="num">1)
      {
      ValW=(High[i+class="num">1]-Close[i+class="num">1])*Val0;
      Deep(Ci0,Nums,deepC-class="num">1,deepStart,ValW);
      }
      else
      {
      ValStart+=(Ci0[NumC]/Combinations(deepStart,Nums*class="num">5))*(High[i+class="num">1]-Close[i+class="num">1])*Val0/Point;
      NumC++;
      }
      }
   for ( class="type">int i=class="num">0; i<Nums; i++ )
      {
      if (deepC > class="num">1)
      {
      ValW=(Close[i+class="num">1]-Low[i+class="num">1])*Val0;
      Deep(Ci0,Nums,deepC-class="num">1,deepStart,ValW);
      }
      else
      {
      ValStart+=(Ci0[NumC]/Combinations(deepStart,Nums*class="num">5))*(Close[i+class="num">1]-Low[i+class="num">1])*Val0/Point;
      NumC++;
      }
      }
      }

class="type">void CalcDeep(class="type">class="kw">double &Ci0[],class="type">int Nums,class="type">int deepC=class="num">1)
   {
   NumC=class="num">0;
   ValStart=class="num">0.0;
   for ( class="type">int i=class="num">0; i<deepC; i++ ) Deep(Ci0,Nums,i+class="num">1,i+class="num">1);
   }

递归展开分形系数的层数代价

下面这段 MQL5 函数用来统计一个中间分形在给定深度下会展开出多少系数项。核心思路是:DeepN 按 Nums*5 的循环次数向下递归,deepC 每减一层就再展开一次,直到 deepC 为 1 才把 NumCAll 累加。 CalcDeepN 先清零计数器,再从深度 1 到 deepC 逐层调用 DeepN。以 Nums=2、deepC=3 为例,外层循环 3 次分别走 deepC=1/2/3:deepC=1 时 i 跑 10 次全加计数;deepC=2 时每次再嵌套一层 10 次;deepC=3 时再嵌套两层。实际跑完 NumCAll 会远大于 30,递归层数越深,系数数组膨胀越快,MT5 里直接拖大 deepC 就能看到终端打印的 NumCAll 跳变。 外汇与贵金属行情里这类分形展开常用于多周期嵌套过滤,但递归深度过高会吃掉回测速度,属于典型的高风险策略计算负担,调参时建议先在策略测试器里盯内存与耗时。

MQL5 / C++
class="type">int NumCAll=class="num">0;class=class="str">"cmt">//size of the array of coefficients
class="type">void DeepN(class="type">int Nums,class="type">int deepC=class="num">1)class=class="str">"cmt">//intermediate fractal
  {
  for ( class="type">int i=class="num">0; i<Nums*class="num">5; i++ )
    {
    if (deepC > class="num">1) DeepN(Nums,deepC-class="num">1);
    else NumCAll++;
    }
  }
class="type">void CalcDeepN(class="type">int Nums,class="type">int deepC=class="num">1)
  {
  NumCAll=class="num">0;
  for ( class="type">int i=class="num">0; i<deepC; i++ ) DeepN(Nums,i+class="num">1);
  }

「H1暴力扫描下的全局模式成色」

作者受算力所限只跑 H1 全局模式,且刻意退回 MetaTrader 4 做测试——MT5 测试器在自定义品种下也会按经纪商实时点差下限兜底,容易把回测润色得过于乐观,而 MT4 能避开这层修饰。EURUSD H1 取 2010–2020 十年数据、用 50 根 K 线暴力枚举约 20 万组变体,单核跑完得到的全局模式数学期望仅约 8 个点,P_Factor(类利润系数,区间[0,1])全样本均值约 0.02、最优约 0.029。 把同一组参数推到 2020.01–2020.11 前向验证,EURUSD 前半年还勉强有效,六个月后直接反转,说明这类全局模式若训练期质量不够,实战里大概率只能撑几个月。EURCHF 则明显不同:训练期 P_Factor 峰值约 0.057,比 EURUSD 高近 2 倍,前向全年虽不均匀但未崩,利润系数实跑高出 0.09。 USDJPY 是最差的,前向后半段复制了 EURUSD 的“先上后直线反转”;USDCHF likewise 年中反转。初步结论很直白:训练期指标成色直接决定前向表现,EURCHF 在训练和验证两段都压过其余货币对,非 EURCHF 图表普遍只挺到年中。外汇与贵金属属高风险品种,这类统计优势随时被点差和波动吞噬,实盘前务必自测。 换小样本试一刀:EURJPY H1 取 2017 全年(样本缩到十年变量的 1/12),训练出的利润系数和数学期望反而高于十年组。推到 2018.01–02 一个月前向,模式几乎立刻反转——样本越小越容易过拟合,几个月的可交易窗口往往伴生更高的随机性,验证成本不低但值得手动复一遍。

把形状初筛交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到线性因子与平滑度评分,你只需圈定区间让小布跑一遍暴力匹配。

常见问题

激活线性因子形状过滤器后,程序需要对每个候选图形做第二次相似度过程,因此整体耗时约为不开启时的两倍,但换来的是结果数量更少、质量更可控。
预期收益衡量盈利倾向,线性因子衡量与直线的平滑相似度。两者作用不同,通常先按质量排再用地形因子剔掉需肉眼检查的弯曲图形,更可能在第二阶段留下可用方案。
可以,小布盯盘的品种页已内置形状相似度与利润因子双维度诊断,你导入模板后它能自动标出高平滑度区间,省去本地跑 MQL 的等待。
改后的多项式改变了拟合基线,会使长短区间的分布对比更明显,具体差异需结合下一篇的全局货币对数据来看,本篇侧重单货币对深入。