模式搜索的暴力方法·进阶篇
🔢

模式搜索的暴力方法·进阶篇

(2/3)· 当神经网络架构成为束缚,简单数字序列如何以更低算力挖掘可复用的市场模式

偏理论 第 2/3 篇
很多交易者把暴力搜索当成无脑穷举,结果在无效序列上烧光算力却得不到稳定形态。其实暴力方法的核心不是乱试,而是用可计算的数字序列逼近市场物理特性。忽略这一点,自适应系统的根基就会建在沙子上。

「EA模板里的系数与递归骨架」

这套模板最早在 MQL4 写就,后转 MQL5,两边都能跑。为了沿用 MQL4 那种预定义数组习惯,EA 里要补几行兼容代码,逻辑不复杂,但前提是看过前一篇里讲的那点适配细节。 核心变量先摊开:C1 是阶次系数数组,CNum 取最近多少根烛形进多项式,DeepBruteX 是多维展开的最大次数。我一般填 1,因为多维泰勒的系数总量随阶数陡增,计算负担比一维重得多。DatetimeStart 卡个起始时间,用来限制 EA 只在某时刻后干活。 bInvert 这个反转开关很关键。把阶次系数全变号,多项式形状不动,只是输出值符号反过来。若原设定负值为卖、正值为买,就保持 false;反过来则 true,等于告诉算法「用反号的值去交易」。建议把它设成 input,方便随时翻向。DaysToTrade 同理,控制未来几天允许交易。 系数数组尺寸靠两个递归函数算。DeepN 是中间分形,按相同总阶数累加项数;CalcDeepN 在循环里从 1 调到 deepC 去调它。比如深度限 4,就用自然数 1~4 各调一次 DeepN。 真正算多项式值的 Deep 函数结构类似,但数组自己生成不需预设大小。结果全累进全局变量 ValStart,另用 ValW 存「已有乘积 × 该柱点数位移」(Close-Open 带方向)。它内部调自己,调用树每次形态不同——这种多变结构正好适配多维级数。 若只用一维多项式,整个事就退化成「系数和 × 单阶柱运动」,柱数即项数,深度 1 时走简化版,否则走统一方法。新柱触发的主函数只管挂单平仓,柱检测几行带过。系数全部归一在 [-1,1],因为比值比绝对值更有用。下方代码即模板头部与递归骨架原文。

MQL5 / C++
class="type">class="kw">double C1[] = { %%%CVALUES%%% };class=class="str">"cmt">//array of coefficients
class="type">int CNum=%%%CNUMVALUE%%%;class=class="str">"cmt">//number of candlesticks in the formula
class="type">int DeepBruteX=%%%DEEPVALUE%%%;class=class="str">"cmt">//formula depth
class="type">int DatetimeStart=%%%DATETIMESTART%%%;class=class="str">"cmt">//start point in time
class="kw">input class="type">bool bInvert=%%%INVERT%%%;class=class="str">"cmt">//inverted trading
class="kw">input class="type">class="kw">double DaysToTrade=%%%DAYS%%%;class=class="str">"cmt">//number of days into the future to trade
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; 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)class=class="str">"cmt">//to launch calculations
  {
  NumCAll=class="num">0;
  for ( class="type">int i=class="num">0; i<deepC; i++ )
    {
    DeepN(Nums,i+class="num">1);
    }  
  }
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="num">1,class="type">class="kw">double Val0=class="num">1.0)class=class="str">"cmt">//calculate the sum of one degree
  {
  for ( class="type">int i=class="num">0; i<Nums; i++ )
    {
    if (deepC > class="num">1)
      {
      ValW=(Close[i+class="num">1]-Open[i+class="num">1])*Val0;      
      Deep(Ci0,Nums,deepC-class="num">1,ValW);
      }
    else
      {
      ValStart+=Ci0[NumC]*(Close[i+class="num">1]-Open[i+class="num">1])*Val0/Point;
      NumC++;
      }
    }  
  }

◍ 多项式估值如何驱动开平仓

这段逻辑把多项式输出值直接映射成交易动作,核心在 PolinomTrade() 与 Trade() 两个函数。当 DeepBruteX 小于等于 1 时,用一阶系数 C1 乘上每根 K 线的 (Close-Open)/Point 累加得到 Val;若大于 1 则走 CalcDeep 递归展开高阶项,返回 ValStart。外汇与贵金属杠杆高,信号仅代表概率倾向,实盘前务必在 MT5 策略测试器验证。 Trade() 里先用 Value 与 ValueCloseE 比较:大于阈值先平买(或反转模式平卖),小于负阈值平卖(或反转平买)。开仓则额外卡了 DaysToTrade 天数窗,要求 Value 落在 (ValueOpenE, ValueOpenEMax] 或 [-ValueOpenEMax, -ValueOpenE) 区间才触发,避免极值区追单。 CalcTimer() 以 Time[1] 跨根才调用 Trade(),保证每根新 K 线只交易一次。GenerateC() 则按 DeepBrute 决定展开阶数,并用 ArrayResize 给每个变异体分配 NumCAll 长度系数数组,NumCAll 在浅层时等于 CandlesE。把下面代码贴进 EA 的对应函数区,调 ValueOpenE 与 DaysToTrade 就能观察信号密度变化。

MQL5 / C++
class="type">void CalcDeep(class="type">class="kw">double &Ci0[],class="type">int Nums,class="type">int deepC=class="num">1)class=class="str">"cmt">//calculate the entire polynomial
  {
  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);
    }  
  }
class="type">class="kw">double Val;
class="type">class="kw">double PolinomTrade()class=class="str">"cmt">//optimized polynomial
  {
  Val=class="num">0;
  if ( DeepBruteX <= class="num">1 )
    {
    for ( class="type">int i=class="num">0; i<ArraySize(C1); i++ )
      {
      Val+=C1[i]*(Close[i+class="num">1]-Open[i+class="num">1])/Point;
      }
    class="kw">return Val;  
    }
  else
    {
    CalcDeep(C1,CNum,DeepBruteX);
    class="kw">return ValStart;
    }       
  }
class="type">void Trade()
  {
  class="type">class="kw">double Value;
  Value=PolinomTrade();

  if ( Value > ValueCloseE)
    {
    if ( !bInvert )
      {
      CloseBuyF();
      }       
    else
      {
      CloseSellF();
      }
    }
    
  if ( Value < -ValueCloseE)
    {
    if ( !bInvert )
      {
      CloseSellF();
      }       
    else
      {
      CloseBuyF();
      }       
    }  

  if ( class="type">class="kw">double(TimeCurrent()-DatetimeStart)/class="num">86400.0 <= DaysToTrade && Value > ValueOpenE && Value <= ValueOpenEMax )
    {
    if ( !bInvert ) SellF();
    else BuyF();
    }
    
  if ( class="type">class="kw">double(TimeCurrent()-DatetimeStart)/class="num">86400.0 <= DaysToTrade && Value < -ValueOpenE && Value >= -ValueOpenEMax )
    {
    if ( !bInvert ) BuyF();
    else SellF();
    }
  }
class="type">void CalcTimer()
  {
  if ( Time[class="num">1] > PrevTimeAlpha )
    {
    if ( PrevTimeAlpha > class="num">0 )
      {
      Trade();
      }
    PrevTimeAlpha=Time[class="num">1];
    }
  }
  class="type">void GenerateC()
    {
    class="type">class="kw">double RX;
    if ( DeepBrute > class="num">1 ) CalcDeepN(CandlesE,DeepBrute);
    else NumCAll=CandlesE;
    for ( class="type">int j=class="num">0; j<VariantsE; j++ )
      {
      ArrayResize(Variants[j].Ci,NumCAll,class="num">0);

变异体权重的三类随机注入

遗传算法里每个变异体 Variants[j] 需要先记下当前蜡烛数、总系数数和暴力搜索深度,再给系数数组 Ci[] 灌入随机初值。下面这段代码展示了 RANDOM_TYPE_1 到 RANDOM_TYPE_3 三种分布形态的差异,直接决定了后续进化的探索偏向。 RANDOM_TYPE_1 最简单,每个系数都是 [0,1] 均匀采样,MathRand()/32767.0 是 MT5 里生成 0~1 浮点的标准写法。RANDOM_TYPE_2 引入符号翻转:以 0.5 为界,一半概率取正、一半概率取负,系数域变成 [-1,1],适合需要双向权重的场景。 RANDOM_TYPE_3 最绕,先用 RX 存一个基准随机值,再以 RX 为阈值决定该系数是否非零;非零时再根据 RX+(1-RX)/2 的中点做正负切分。这种写法会让约 RX 比例的系数直接归零,稀疏度随 RX 浮动,外汇与贵金属回测中可能影响过拟合程度,相关品种波动大、杠杆高,实盘前务必小资金验证。 把这段逻辑抄进你的 EA 初始化函数,改 RE 宏定义就能切换分布;想看实际落点,把 RANDOM_TYPE_3 里那行 Print 注释打开,跑 100 代看 Ci[] 的零值占比是否和你预期的 RX 吻合。

MQL5 / C++
Variants[j].CNum=CandlesE;
Variants[j].ANum=NumCAll;
Variants[j].DeepBruteX=DeepBrute;
RX=MathRand()/class="num">32767.0;
for ( class="type">int i=class="num">0; i<Variants[j].ANum; i++ )
  {
  if ( RE == RANDOM_TYPE_1 ) Variants[j].Ci[i]=class="type">class="kw">double(MathRand())/class="num">32767.0;
  if ( RE == RANDOM_TYPE_2 )
    {
     if ( MathRand()/class="num">32767.0 >= class="num">0.5  )
      {
       Variants[j].Ci[i]=class="type">class="kw">double(MathRand())/class="num">32767.0;              
      }
     else
      {
       Variants[j].Ci[i]=class="type">class="kw">double(-MathRand())/class="num">32767.0;             
      }
    }
  if ( RE == RANDOM_TYPE_3 )
    {
     if ( MathRand()/class="num">32767.0 >= RX  )
      {
       if ( MathRand()/class="num">32767.0 >= RX+(class="num">1.0-RX)/class="num">2.0  )
         {
          Variants[j].Ci[i]=class="type">class="kw">double(MathRand())/class="num">32767.0;
          class=class="str">"cmt">///Print(Variants[j].Ci[i]);
         }
       else
         {
          Variants[j].Ci[i]=class="type">class="kw">double(-MathRand())/class="num">32767.0;             
         }        
      }
     else
      {
       Variants[j].Ci[i]=class="num">0.0;             
      }
    }
  if ( RE == RANDOM_TYPE_4 )

「用随机阈值砍掉冗余基因」

在遗传算法式的参数变异里,不是每个染色体位都要动。上面这段逻辑用 MathRand() 生成的 0~1 均匀随机数跟阈值 RX 比大小,决定第 j 个变异体的第 i 个系数 Ci 是随机赋值还是直接归零。 MathRand() 返回 0 到 32767 的整数,除以 32767.0 得到闭区间 [0,1] 的浮点值。当该值 >= RX 时,Ci[i] 被赋为一个新的随机浮点;否则强制 0.0,相当于把这一维特征从当前变异体中剔除。 RX 越大,被保留为 0 的维度越多,种群稀疏化倾向越明显,搜索空间收缩但可能漏掉有效组合。在 MT5 策略测试器里把 RX 从 0.3 调到 0.7,能直接观察到代际适应度方差的变化,外汇与贵金属品种下这种随机剔除会放大过拟合风险,需以多周期样本复核。

MQL5 / C++
      {
      if ( MathRand()/class="num">32767.0 >= RX  )
        {
        Variants[j].Ci[i]=class="type">class="kw">double(MathRand())/class="num">32767.0;
        }
      else
        {
        Variants[j].Ci[i]=class="num">0.0;               
        } 
      }             
      }
    }
  }

◍ EURUSD M5 上跑完的十二个暴力变体

作者挑了 EURUSD M5 上三段各约一个月、首尾都落在周五的连续区间做验证:2020.01.13–02.16、2020.02.13–03.15、2020.03.13–04.18。周五收尾留出了周末两天空档,实盘可用来搜模式,不过本文全程在测试器里跑所以只是顺带一提。 程序第一页管数字生成类型(仅正 / 正负 / 含零)和搜索条件,后者能在「期望值点数」和自拟的利润系数间切换。利润系数公式 P_Factor=(Profit-Loss)/(Profit+Loss) 被限制在 -1 到 +1,天然避开了除零。最大次数设 1 和 2 各六种变体,共 12 个。

不对称过滤器 D_Asymmetry=(BuyTrades-SellTrades)/(BuyTrades+SellTrades) 取值 0–1,用来逼程序别在单边市里只发同向信号。第二页把多项式值窗口切成等距区间做渐增暴力,并用 Deviation = Max(Profit[i]-LineProfit[i])/EndProfit 量曲线相对首尾连线的偏离,单位都是点。

为压住点差噪声,作者用 MT4 测试器把 spread 锁成 1 点。每个变体暴力加优化约两小时,他自己明说这不够,最好是一两天,但本文目的只是看模式长相。注意 DaysToTrade 默认 3 天,开 EA 前不调大,暴力段后可能根本没单。 一次多项式里,第三区间 2020.03.13 起的第五个机器人起手有波、尾段衰减,明显不对称,作者直接弃用;第二区间的第三个机器人图形接近直线,未来十天也像线,属于少数看着稳的。二次多项式在暴力段总冒出尖锐正半波,出了那段就乱,第十一个机器人虽延直线但噪点偏多,更像运气。 十二个里没一个被断言「能赚」,作者倾向认为次数高于 2 的多项式反转更平滑、意外少,值得拿三天以上暴力去试三次式。外汇与贵金属属高风险品种,这类历史区间拟合出的 EA 在未来失效概率不低,上 MT5 前先把自己那段样本和 spread 设一遍再信。

把重复劳动交给小布
这些模式诊断与序列回测的重复活儿,小布盯盘的 AIGC 已内置,打开对应品种页即可看到初步的形态命中提示,你只需判断哪些值得深挖。

常见问题

不是。暴力方法侧重用任意类型长度的数字序列直接对应收益目标,网格搜索多是参数空间均匀采样,前者更强调序列效率而非覆盖密度。
固定架构等于提前框定元集合与网络图,就像空模板不写逻辑就跑不起来,而暴力序列更轻量,虽效率上限低但不会被图纸绑死。
一般先抽象出可交互的代码接口,让数字序列能驱动订单逻辑与信号判断,具体实现见后续小节的技术拆解。
目前小布内置的是形态命中与初步诊断,完整暴力序列寻优仍需在 MT5 端写 EA,但小布可帮你省掉日常盯形态的眼睛。
市场物理特性会漂移,序列有效时长需用样本外数据持续校验,这类品种高杠杆高风险,形态失效可能很快。