形态搜索的暴力强推方式(第四部分):最小功能·进阶篇
⚙️

形态搜索的暴力强推方式(第四部分):最小功能·进阶篇

(2/3)·上一版又臭又长还易错?这篇用极简功能把形态搜索设定提取变得人人可上手

新手友好 第 2/3 篇
很多交易者卡在暴力强推的第一道门槛:EA 命名冲突、设置文件塞不进数组、界面元素多到眼晕。其实换一种 txt 配置读取思路,MT4 和 MT5 能用同一套极简流程跑起来。

◍ 用随机阈值切分候选因子

在蒙特卡洛式的因子筛选里,先用一个随机阈值 RX 决定某个候选系数 Ci[i] 是否保留。若 RandomX.NextDouble() 生成的均匀随机数大于等于 RX,就把该位置赋为另一个随机数;否则直接置 0.0,等于这一维被丢弃。 这种写法把特征维度做了稀疏化:每一次循环都可能让部分 Ci[i] 归零,从而降低过拟合概率。跑在 MT5 的策略测试器里,把 RX 从 0.3 调到 0.7,能看到有效因子数量明显收缩,回测曲线波动倾向变平缓,但样本外拟合可能偏弱。 验证方法很简单:把上面两段逻辑塞进你自己的 CoreWorker 循环,打印 Ci 数组非零占比,对比不同 RX 下的分布即可。外汇与贵金属品种跳空频繁,这类随机化方案须用小周期 tick 数据压力测试,高风险环境里别盲信单次结果。

MQL5 / C++
if (RandomX.NextDouble() >= RX) CoreWorker.Variant.Ci[i] = RandomX.NextDouble();
else CoreWorker.Variant.Ci[i] = class="num">0.0;
}

用中间价导数绕开点差干扰

点差是策略数学期望的头号吞噬者。很多所谓正回报的回测,只是因为样本太小——一个月甚至一年的实盘统计都不足以推断未来,外汇和贵金属的高风险正在于此:点差一扩,胜率模型可能瞬间失效。 经纪商午夜后常主动拉大点差来绞杀夜间剥头皮机器人。但无论点差怎么变,中间价 MidPrice = (要价 + 出价) / 2 在各家数据流里相对均匀。实测两个随机经纪商的价格序列,黑线(中间价)几乎重合,差别仅在常数 D:MidPrice2 = MidPrice1 - D,两者对时间的导数相等。 因为我们的多项式吃的是价格差值而非绝对价格,泛函只依赖这些导数。既然导数跨经纪商一致,同一套参数在别家平台也可能直接跑通,不必重新针对某家即时报价做脆弱回测。 落地时要改写入逻辑:柱线基于出价,但必须额外记录 Open/Close/High/Low 四点位的 spread。下面这段代码把四个点差写进报价文件,PrevSpread、CurrentSpread、PrevHighSpread、PrevLowSpread 用绿底标出,是旧版完全没有的。 RecalcHighLowSpreads 只在当前价突破 LastHigh / LastLow 时抓一次 SYMBOL_SPREAD,新柱成型即冻结。bNewBar 配合把序列翻转,保证写盘的是已闭合的 1 号柱。 另开一个开关:优化时把点差纳入成交判定,仅当 spread 不超阈值才计为可开/平点数。测试显示这步可选,但算力够时能提高真实度。柱线里存了点差,就能算出剔掉点差后的净表现。

MQL5 / C++
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");
  FileWriteString(Handle0x,IntegerToString(PrevSpread)+"\r\n");
  FileWriteString(Handle0x,IntegerToString(CurrentSpread)+"\r\n");
  FileWriteString(Handle0x,IntegerToString(PrevHighSpread)+"\r\n");
  FileWriteString(Handle0x,IntegerToString(PrevLowSpread)+"\r\n");  
  class="type">MqlDateTime T;
  TimeToStruct(Time[class="num">1],T);
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.hour))+"\r\n");
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.min))+"\r\n");
  FileWriteString(Handle0x,IntegerToString(class="type">int(T.day_of_week))+"\r\n");        
  }    
class="type">void RecalcHighLowSpreads()
  {
  if ( Close[class="num">0] > LastHigh )
    {
    LastHigh=Close[class="num">0];
    HighSpread=class="type">int(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
    }
  if ( Close[class="num">0] < LastLow )
    {
    LastLow=Close[class="num">0];
    LowSpread=class="type">int(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
    }      
  }
class="type">bool bNewBar()
  {
  ArraySetAsSeries(Close,false);

「用点差偏移重画前一根K线」

这段逻辑先把 Open/High/Low 三组数组设为普通序列(false),再用 CopyOpen/CopyHigh/CopyLow 抓当前品种、当前周期最近 2 根 K 线数据;随后把四个价格数组改回时间序列(true),保证索引 [0] 是最新、[1] 是上一根。 bNewBar() 里靠 Time0 与 Time[1] 比较判新柱:若 Time0 更小且非 0,则把上一根的点差快照存进 PrevHighSpread / PrevLowSpread / PrevSpread,再读 SYMBOL_SPREAD 刷新 CurrentSpread,返回 true 触发 WriteBar()。 新柱确认后,代码把点差的一半折算成点数加到原价上:OpenX[1] 用 PrevSpread、CloseX[1] 用当前 Spread、HighX[1]/LowX[1] 用上一根高低点差,均乘 _Point 再除 2.0。这样重画的上一根 K 线会整体右移半个点差,肉眼能看出经纪商成本吞噬的空间。 开 MT5 把这段塞进 EA 的 OnTick,盯黄金 XAUUSD 这类点差跳到 30~50 点的品种,切换 M1 周期,可能倾向看到重画后的影线比原生 K 线明显外扩。外汇与贵金属杠杆高,点差突变会直接啃掉止损余量,验证时请用模拟盘。

MQL5 / C++
   ArraySetAsSeries(Open,false);
   ArraySetAsSeries(High,false);
   ArraySetAsSeries(Low,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];
         PrevHighSpread=HighSpread;
         PrevLowSpread=LowSpread;
         PrevSpread=CurrentSpread;
         CurrentSpread=class="type">int(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
         HighSpread=CurrentSpread;
         LowSpread=CurrentSpread;
         class="kw">return true;
         }
      else
         {
         Time0=Time[class="num">1];
         class="kw">return false;
         }
      }
   else class="kw">return false;
   }
RecalcHighLowSpreads();
if ( bNewBar()) WriteBar();
OpenX[class="num">1]=Open[class="num">1]+(class="type">class="kw">double(PrevSpread)/class="num">2.0)*_Point;
CloseX[class="num">1]=Close[class="num">1]+(class="type">class="kw">double(Spread)/class="num">2.0)*_Point;
HighX[class="num">1]=High[class="num">1]+(class="type">class="kw">double(PrevHighSpread)/class="num">2.0)*_Point;
LowX[class="num">1]=Low[class="num">1]+(class="type">class="kw">double(PrevLowSpread)/class="num">2.0)*_Point;

◍ 用线性手数调整吃下暴力强推周期

手数递增与递减是两套风险/仓位机制,不是装饰品。逆势预期信号很快翻面时用递增;信号已知稳定且能拖很久才用递减。我选了最直白的线性函数:手数随持仓时间匀速变化,不绕弯子。 实测用 USDJPY M15,因即时报价回测在 MQL4 版机器人上会由于点差跳增崩掉,故演示用 MQL4 版。搜索区间取一年,数学期望略高于 12 个点(未计点差)。一年前瞻验证里 EA 连续工作不少于一年,说明有算力就能扫一两周低点差主流货币对,挑出最佳形态用至少一年。 前段回撤很大,红框标出的那段 EA 被限制交易 50 个交易日、且信号逆转扭亏为盈。固定手数时盈利因子一般;改用手数从最小到最大线性递增后,盈利因子抬升。绿框那段是长上升后逆转,换用手数递减、跑到阶段结束,盈利因子同样递增,曲线更平、尾段无回撤。 外汇与贵金属属高风险品种,这类机制在正确位置用可提盈利或减亏,但实战更复杂。MT5 实时报价回测务必过一遍,别只信演示。

从即时报价历史里挖出的可用参数组

很多做 EA 调参的人会想直接看现成能跑的设置,而不是从头暴力搜。我因为单机算力有限、只能自己硬推,全局回测花的时间不短,但也确实筛出几组基于即时报价历史能通过全局回测的变体。 这些变体附在程序版本里,你拉到 MT5 里加载就能测;也可以照同样思路在模拟账户上自己搜一套。外汇和贵金属波动大、杠杆高,任何回测通过的设置都只是历史概率,实盘仍可能失效。 从这一版开始,普通用户也能跑这套方法,不必等作者给结论——开个模拟账户,把参数丢进去回测一遍,比看任何说明都实在。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到点差噪声与短线手数变化的实时标注,你只需专注是否采纳暴力强推给出的设定。

常见问题

设置文件不支持浮动长度数组,而该方法输入依赖数组,所以用普通 txt 配置由 EA 读取更稳,也避开了命名重叠。
小布内置了相关诊断视图,可展示点差噪声与手数阶段变化,但设定搜索本身仍需在终端加载本文的极简模板。
它提供更平滑的函数分解基底,降低过拟合概率,让暴力强推在全局历史里提取的操作设置更偏向可用而非巧合。
会,杠杆品类本就高风险,手数随阶段浮动可能放大回撤,实盘前需用历史黏合测试确认过度拟合程度。