形态搜索的暴力强推方式(第四部分):最小功能·进阶篇
(2/3)·上一版又臭又长还易错?这篇用极简功能把形态搜索设定提取变得人人可上手
◍ 用随机阈值切分候选因子
在蒙特卡洛式的因子筛选里,先用一个随机阈值 RX 决定某个候选系数 Ci[i] 是否保留。若 RandomX.NextDouble() 生成的均匀随机数大于等于 RX,就把该位置赋为另一个随机数;否则直接置 0.0,等于这一维被丢弃。 这种写法把特征维度做了稀疏化:每一次循环都可能让部分 Ci[i] 归零,从而降低过拟合概率。跑在 MT5 的策略测试器里,把 RX 从 0.3 调到 0.7,能看到有效因子数量明显收缩,回测曲线波动倾向变平缓,但样本外拟合可能偏弱。 验证方法很简单:把上面两段逻辑塞进你自己的 CoreWorker 循环,打印 Ci 数组非零占比,对比不同 RX 下的分布即可。外汇与贵金属品种跳空频繁,这类随机化方案须用小周期 tick 数据压力测试,高风险环境里别盲信单次结果。
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 不超阈值才计为可开/平点数。测试显示这步可选,但算力够时能提高真实度。柱线里存了点差,就能算出剔掉点差后的净表现。
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 线明显外扩。外汇与贵金属杠杆高,点差突变会直接啃掉止损余量,验证时请用模拟盘。
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 里加载就能测;也可以照同样思路在模拟账户上自己搜一套。外汇和贵金属波动大、杠杆高,任何回测通过的设置都只是历史概率,实盘仍可能失效。 从这一版开始,普通用户也能跑这套方法,不必等作者给结论——开个模拟账户,把参数丢进去回测一遍,比看任何说明都实在。