模式搜索的暴力算法(第三部分):新视野·进阶篇
(2/3)· 单核每小时仅 700–2000 方案,30 核也要等数天,本篇给出拆样本提速与深研时间区间的进阶路径
「把报价导出塞进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_WRITE | FILE_TXT | FILE_COMMON | FILE_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 少点灵活,但更不易出错。外汇与贵金属波动剧烈、杠杆风险高,任何自动化模板都先在历史数据核验再上实盘。
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 的打印节奏。
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 数,这两个值直接改写拟合形状。
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 数据验证。
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 年训练段有明显反转延续,不是算法崩了。外汇与贵金属高杠杆高风险,这种跨期反转随时吞掉样本外收益。 为挡掉这类外部断层,模式搜索逻辑要改,下个版本再落地。