模式搜索的暴力算法(第三部分):新视野·进阶篇
⚙️

模式搜索的暴力算法(第三部分):新视野·进阶篇

(2/3)· 单核每小时仅 700–2000 方案,30 核也要等数天,本篇给出拆样本提速与深研时间区间的进阶路径

含代码示例实战向 第 2/3 篇
很多人以为暴力搜模式就是无脑跑全样本,结果一开 M5 十年数据服务器直接躺平好几天。其实可靠全局模式根本不需要一次算完,把周期拆成可跳过的小段既能提速又能看清每天每时的局部规律。

「把报价导出塞进EA模板而不是临时算时间」

改 EA 模板时,我顺手把星期几、小时、分钟直接写进每根 K 线的输出里,不再在 C# 端用临时函数把 datetime 拆开。除了 ENUM_DAY_OF_WEEK 是枚举,其余全是整数,只要 C# 侧照着同一套编号解析就不会错位。避开时间函数,一是省了外部计算,二是躲开两端时区或结构不一致导致的危险偏差。 下面这段是生成报价的骨架代码,无输入参数,跑在历史数据上会吐出一份纯文本报价文件,任意编辑器都能开。测试起止各写一次头尾,新柱出现才写一根 bar 的 OHLC 与时间分量。 [CODE] string FileNameString; uint Handle0x; datetime Time0=0; double Open[]; double Close[]; double High[]; double Low[]; datetime Time[]; void WriteEnd() { FileWriteString(Handle0x,"EndBars"+"\r\n"); MqlDateTime T; TimeToStruct(Time[1],T); FileWriteString(Handle0x,IntegerToString(int(T.year))+"\r\n"); FileWriteString(Handle0x,IntegerToString(int(T.mon))+"\r\n"); FileWriteString(Handle0x,IntegerToString(int(T.day))); } void OpenAndWriteStart() { FileDelete(FileNameString);

Handle0x=FileOpen(FileNameString,FILE_WRITEFILE_TXTFILE_COMMONFILE_ANSI,'\t',CP_UTF8);

FileSeek(Handle0x,0,SEEK_SET); FileWriteString(Handle0x,"DataXXX"+" "+Symbol()+" "+IntegerToString(Period())+"\r\n"); FileWriteString(Handle0x,DoubleToString(_Point,8)+"\r\n"); MqlDateTime T; TimeToStruct(Time[1],T); FileWriteString(Handle0x,IntegerToString(int(T.year))+"\r\n"); FileWriteString(Handle0x,IntegerToString(int(T.mon))+"\r\n"); FileWriteString(Handle0x,IntegerToString(int(T.day))+"\r\n"); } void WriteBar() { FileWriteString(Handle0x,"\r\n"); FileWriteString(Handle0x,DoubleToString(Close[1],8)+"\r\n"); FileWriteString(Handle0x,DoubleToString(Open[1],8)+"\r\n"); FileWriteString(Handle0x,DoubleToString(High[1],8)+"\r\n"); FileWriteString(Handle0x,DoubleToString(Low[1],8)+"\r\n"); FileWriteString(Handle0x,IntegerToString(int(Time[1]))+"\r\n"); MqlDateTime T; TimeToStruct(Time[1],T); FileWriteString(Handle0x,IntegerToString(int(T.hour))+"\r\n"); [/CODE] 逐行看:FileNameString 是公共目录下的文件名;Handle0x 存文件句柄;Open/Close/High/Low/Time 是系列数组,下标 1 取已收盘柱。WriteEnd 在收尾写 EndBars 加年月日。OpenAndWriteStart 先删旧文件再开新文件,头部写品种、周期、_Point(精度 8 位),再补起始年月日。WriteBar 每根写收盘价、开、高、低、UNIX 时间、小时——这些就是喂给后续程序的原始字段。 输入参数现在由生成程序在创建 EA 时直接写死进代码,编译完就能跑,不必开 MetaEditor 调。我手头 MT4 暴力优化原型曾因设置文件太多搞混过,内嵌数组虽比外挂 txt 少点灵活,但更不易出错。外汇与贵金属波动剧烈、杠杆风险高,任何自动化模板都先在历史数据核验再上实盘。

MQL5 / C++
class="type">class="kw">string FileNameString;
class="type">uint Handle0x;
class="type">class="kw">datetime Time0=class="num">0;
class="type">class="kw">double Open[];
class="type">class="kw">double Close[];
class="type">class="kw">double High[];
class="type">class="kw">double Low[];
class="type">class="kw">datetime Time[];
class="type">void WriteEnd()
  {
  FileWriteString(Handle0x,"EndBars"+"\r\n");
  class="type">MqlDateTime T;
  TimeToStruct(Time[class="num">1],T);
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.year))+"\r\n");
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.mon))+"\r\n");
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.day)));
  }
class="type">void OpenAndWriteStart()
  {
  FileDelete(FileNameString);
  Handle0x=FileOpen(FileNameString,FILE_WRITE|FILE_TXT|FILE_COMMON|FILE_ANSI,&class="macro">#x27;\t&class="macro">#x27;,CP_UTF8);
  FileSeek(Handle0x,class="num">0,SEEK_SET);
  FileWriteString(Handle0x,"DataXXX"+" "+Symbol()+" "+IntegerToString(Period())+"\r\n");
  FileWriteString(Handle0x,DoubleToString(_Point,class="num">8)+"\r\n");
  class="type">MqlDateTime T;
  TimeToStruct(Time[class="num">1],T);
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.year))+"\r\n");
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.mon))+"\r\n");
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.day))+"\r\n");
  }
class="type">void WriteBar()
  {
  FileWriteString(Handle0x,"\r\n");
  FileWriteString(Handle0x,DoubleToString(Close[class="num">1],class="num">8)+"\r\n");
  FileWriteString(Handle0x,DoubleToString(Open[class="num">1],class="num">8)+"\r\n");
  FileWriteString(Handle0x,DoubleToString(High[class="num">1],class="num">8)+"\r\n");
  FileWriteString(Handle0x,DoubleToString(Low[class="num">1],class="num">8)+"\r\n");
  FileWriteString(Handle0x,IntegerToString(class="type">int(Time[class="num">1]))+"\r\n");
  class="type">MqlDateTime T;
  TimeToStruct(Time[class="num">1],T);
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.hour))+"\r\n");

◍ 新K线判定与历史数组的时序翻转

这段逻辑核心是解决 MT5 里「新柱确认」和「数组方向」的冲突。bNewBar() 先把 OHLC 四个数组用 ArraySetAsSeries(...,false) 翻成下标递增,Copy 最近 2 根数据后再翻回 true(最新在 [0]),避免直接读序列数组时拿到错位值。 判定新柱靠一个外部变量 Time0:若 Time0 小于当前 Time[1],说明上一根已经走完,更新 Time0=Time[1] 并返回 true;首次运行 Time0==0 时只记录不报新柱,防止 EA 加载瞬间误触发。 OnInit 里给 Close/Open/Time/High/Low 都 ArrayResize 成 2,正好对应 Copy 函数的 2 根长度。外汇与贵金属杠杆高,这种时序处理若写错,可能在跳空缺口时重复下单或漏单,建议开 MT5 用 EURUSD 的 M1 跑一遍看 Time0 的打印节奏。

MQL5 / C++
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.min))+"\r\n");
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.day_of_week))+"\r\n");
  class=class="str">"cmt">//FileClose(Handle0x);
  }
class="type">void CloseFile()
  {
  FileClose(Handle0x);
  }
class="type">bool bNewBar()
  {
  ArraySetAsSeries(Close,class="kw">false);
  ArraySetAsSeries(Open,class="kw">false);
  ArraySetAsSeries(High,class="kw">false);
  ArraySetAsSeries(Low,class="kw">false);
  CopyOpen(_Symbol,_Period,class="num">0,class="num">2,Open);
  CopyClose(_Symbol,_Period,class="num">0,class="num">2,Close);
  CopyHigh(_Symbol,_Period,class="num">0,class="num">2,High);
  CopyLow(_Symbol,_Period,class="num">0,class="num">2,Low);
  ArraySetAsSeries(Close,true);
  ArraySetAsSeries(Open,true);
  ArraySetAsSeries(High,true);
  ArraySetAsSeries(Low,true);
  if ( Time0 < Time[class="num">1] )
    {
    if (Time0 != class="num">0)
      {
      Time0=Time[class="num">1];
      class="kw">return true;
      }
    else
      {
      Time0=Time[class="num">1];
      class="kw">return class="kw">false;
      }
    }
  else class="kw">return class="kw">false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
  ArrayResize(Close,class="num">2,class="num">0);
  ArrayResize(Open,class="num">2,class="num">0);
  ArrayResize(Time,class="num">2,class="num">0);
  ArrayResize(High,class="num">2,class="num">0);
  ArrayResize(Low,class="num">2,class="num">0);

EA 里时间走廊与多项式信号的进出场判定

这段 MT5 代码把多项式拟合结果 Value 和交易时间窗绑在一起管仓位。核心思路是:不打开时间走廊开关时,只认 (TimeCurrent()-DatetimeStart)/86400.0 落在 DaysToFuture 天数内、且 Value 在 ValueOpenE 到 ValueOpenEMax 之间这一段窗口,才允许按 bInvert 反向发 BuyF / SellF。 时间走廊启用后,用 HourCorrect(TradeHour)*60+MinuteCorrect(TradeMinute) 算出起始分钟数,和 tick 的 hour*60+min 比大小,把日内交易压缩进 TradeHour:TradeMinute 到 TradeHourEnd:TradeMinuteEnd 这截。外汇与贵金属杠杆高,这类自动开关仓逻辑若参数错配,可能在几根 K 线内放大回撤,实盘前务必在策略测试器跑历史。 平仓分支不依赖时间,只看 Value 越界:大于 ValueCloseE 时按 bInvert 平买或平卖,小于 -ValueCloseE 时反向平。DaysToTrade 数组配合 bDay() 还能按 weekday 过滤,把周一至周五里不想要的日子直接跳过。 把下面这段直接贴进 MQ5 看结构,重点核对 %% 占位符在实盘里被替换成了什么数——尤其是 DeepBruteX 决定多项式最高次,CNum 决定参与方程的历史 Bars 数,这两个值直接改写拟合形状。

MQL5 / C++
class="type">class="kw">double C1[] = { %%%CVALUES%%% };class=class="str">"cmt">//Brutted Values
class="type">int CNum=%%%CNUMVALUE%%%;class=class="str">"cmt">//Bars To Equation
class="type">int DeepBruteX=%%%DEEPVALUE%%%;class=class="str">"cmt">//Max Pow Of Polynomial
class="type">int DatetimeStart=%%%DATETIMESTART%%%;class=class="str">"cmt">//Help Datetime
class="kw">input class="type">bool bInvert=%%%INVERT%%%;class=class="str">"cmt">//Invert Trade(or sign of values as the same)
class="kw">input class="type">int DaysToFuture=%%%DAYSFUTURE%%%;class=class="str">"cmt">//Days To Future
class="type">int DaysToTrade[]={ %%%DAYS%%% };class=class="str">"cmt">//Days To Trade
class="kw">input class="type">class="kw">double ValueOpenE=%%%OPTVALUE%%%;class=class="str">"cmt">//Open Signal
class="kw">input class="type">bool bUseTimeCorridorE=%%%TIMECORRIDORVALUE%%%;class=class="str">"cmt">//Use Time Corridor
class="kw">input class="type">int TradeHour=%%%HOURSTARTVALUE%%%;class=class="str">"cmt">//Start Trading Hour
class="kw">input class="type">int TradeMinute=%%%MINUTESTARTVALUE%%%;class=class="str">"cmt">//Start Trading Minute
class="kw">input class="type">int TradeHourEnd=%%%HOURENDVALUE%%%;class=class="str">"cmt">//End Trading Hour
class="kw">input class="type">int TradeMinuteEnd=%%%MINUTEENDVALUE%%%;class=class="str">"cmt">//End Trading Minute
class="type">bool bDay()class=class="str">"cmt">//Day check
  {
  class="type">MqlDateTime T;
  TimeToStruct(Time[class="num">0],T);
  for ( class="type">int i=class="num">0; i<ArraySize(DaysToTrade); i++ )
    {
    if ( T.day_of_week == DaysToTrade[i] ) class="kw">return true;
    }
  class="kw">return class="kw">false;
  }
class="type">void Trade()class=class="str">"cmt">//Trade Function
  {
  class="type">class="kw">double Value;
  Value=PolinomTrade();
  class="type">MqlTick LastTick;
  SymbolInfoTick(Symbol(),LastTick);
  class="type">MqlDateTime tm;
  TimeToStruct(LastTick.time,tm);
  class="type">int MinuteEquivalent=tm.hour*class="num">60+tm.min;
  class="type">int BorderMinuteStartTrade=HourCorrect(TradeHour)*class="num">60+MinuteCorrect(TradeMinute);
  class="type">int BorderMinuteEndTrade=HourCorrect(TradeHourEnd)*class="num">60+MinuteCorrect(TradeMinuteEnd);
  
  if ( Value > ValueCloseE)
    {
    if ( !bInvert ) CloseBuyF();
    else CloseSellF();
    }
    
  if ( Value < -ValueCloseE)
    {
    if ( !bInvert ) CloseSellF();
    else CloseBuyF();
    }  
  
  if ( !bUseTimeCorridorE )
    {
    if ( class="type">class="kw">double(TimeCurrent()-DatetimeStart)/class="num">86400.0 <= DaysToFuture && Value > ValueOpenE && Value <= ValueOpenEMax )
      {
      if ( !bInvert ) SellF();
      else BuyF();
      }
    }

「跨日时段里的开仓边界判定」

这段逻辑处理的是交易时段跨过午夜的情况,以及正常交易窗口内的双向触发。核心变量 BorderMinuteStartTrade 与 BorderMinuteEndTrade 的大小关系,决定了我们是按「跨天禁交易区」还是「当日允许区」来过滤 MinuteEquivalent。 当起始分钟大于结束分钟且 bDay() 为真时,系统认为中间那段(EndTrade 到 StartTrade)是休息区,只有落在该区间之外才继续判断。此时若 (TimeCurrent()-DatetimeStart)/86400.0 不超过 DaysToFuture,且 Value 落在 (ValueOpenE, ValueOpenEMax] 或 [-ValueOpenEMax, -ValueOpenE) 区间内,就按 bInvert 反向或正向调用 SellF / BuyF。 若起始分钟小于等于结束分钟,则只在 MinuteEquivalent 处于 [StartTrade, EndTrade] 内才放行同样的 Value 区间判定。最后 bPrintValue 为真时会用 Print 输出 Value 的实时数值,方便在 MT5 专家日志里抓偏离幅度。 外汇与贵金属杠杆品种跳空频繁,这种基于固定分钟窗口的触发在重大数据发布前后可能连续误触,实盘前务必用策略测试器跑至少 3 个月 Tick 数据验证。

MQL5 / C++
   if( class="type">class="kw">double(TimeCurrent()-DatetimeStart)/class="num">86400.0 <= DaysToFuture && Value < -ValueOpenE && Value >= -ValueOpenEMax )
      {
      if( !bInvert ) BuyF();
      else SellF();
      }   
   }
 else
   {
   if( BorderMinuteStartTrade > BorderMinuteEndTrade && bDay() )
      {
      if( !(MinuteEquivalent>=BorderMinuteEndTrade && MinuteEquivalent<= BorderMinuteStartTrade) )
         {
         if( class="type">class="kw">double(TimeCurrent()-DatetimeStart)/class="num">86400.0 <= DaysToFuture && Value > ValueOpenE && Value <= ValueOpenEMax )
            {
            if( !bInvert ) SellF();
            else BuyF();
            }

         if( class="type">class="kw">double(TimeCurrent()-DatetimeStart)/class="num">86400.0 <= DaysToFuture && Value < -ValueOpenE && Value >= -ValueOpenEMax )
            {
            if( !bInvert ) BuyF();
            else SellF();
            }
         }      
      }
   if( BorderMinuteStartTrade <= BorderMinuteEndTrade && bDay() )
      {
      if( MinuteEquivalent>=BorderMinuteStartTrade && MinuteEquivalent<= BorderMinuteEndTrade )
         {
         if( class="type">class="kw">double(TimeCurrent()-DatetimeStart)/class="num">86400.0 <= DaysToFuture && Value > ValueOpenE && Value <= ValueOpenEMax )
            {
            if( !bInvert ) SellF();
            else BuyF();
            }

         if( class="type">class="kw">double(TimeCurrent()-DatetimeStart)/class="num">86400.0 <= DaysToFuture && Value < -ValueOpenE && Value >= -ValueOpenEMax )
            {
            if( !bInvert ) BuyF();
            else SellF();
            }
         }      
      }   
   }
 if( bPrintValue ) Print("Value="+DoubleToString(Value));   
 }

◍ 跨货币对前向验证的实情

所有训练样本均取自 2010.01.01-2020.01.01,前测窗口锁在 2020.01-2020.12.01,用来当提前期看模式寿命。EURCHF H1 上跑了三个 EA:MT4 里 0.1 手预期收益从旧版 8 美元抬到 54 美元,利润因子至少多 0.5,旧版约 1.14,这是目前摸到的最高值,说明按星期几和成交时段切样本确实改了结果。 但高频不等于好用。两轮核心跑完所有测试只花 2-3 天,单方案 5-6 小时,期间还揪出几个会吐错误结果的算法 bug,修完之后可用模式数量变少。MT5 里我限了点差求稳,EURCHF 一年仅 2 笔交易却都为正;同间隔不限点差反而亏损,可见底层样本越大,小样本里强模式越少但越稳。 EURUSD H4 两个机器人前向全正,最大那笔不算其余也盈利;EURJPY M5 出现首个负向前向——根子在 2015 年训练段有明显反转延续,不是算法崩了。外汇与贵金属高杠杆高风险,这种跨期反转随时吞掉样本外收益。 为挡掉这类外部断层,模式搜索逻辑要改,下个版本再落地。

把样本切片交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到按日按小时拆好的模式热力,你只需决定切多细、跳哪些段,重复劳动交给小布。

常见问题

单次 pass 容量很小且计算本身涉及较多条件判断,即便普通设置在好内核上也只有这个吞吐,多核并行仍可能要等数天。
有可能,但分割的目的本是加速加细看局部;最终仍需在全局段回测验证,倾向把切片结果当候选而非定论。
可以,小布盯盘的品种页已内置按天/小时拆段的模式概览,你导入 EA 逻辑后它能替你跑重复切片,你专注看哪些区间值得深挖。
它负责把优化后的模板自动加载、挂不同样本段跑校验,减少手动切图表的繁琐,属于研究型辅助而非自动交易。
历史样本再匹配也可能因流动性突变失效,外汇贵金属杠杆高、回撤可能超预期,任何模式都只是概率优势而非保证。