交易货币对篮子时出现的测试形态。第 I 部·进阶篇
(2/3)· 当标准WPR的±20/±80失灵于篮子,怎样用历史回测重新标定入场阈值
把 ZigZag 拐点按价位分层落盘
这段逻辑干的事很直接:遍历全部 K 线,把 ZigZag 指标buffer里有效的拐点值,按预设的 100 个价位档(levels[0]~levels[99])做分层计数,最后写文件。外汇和贵金属波动大,ZigZag 拐点分布本身受滑点和高杠杆影响,统计结论只代表历史样本,实盘有高风险。 外层 for 从 i=0 跑到 rates_total,遇到 ZigZagBuffer[i] 为 0 或 EMPTY_VALUE 就跳过——也就是说只处理真正画出的拐点。内层再跑 j 从 0 到 99,一旦拐点值小于当前档位 levels[j],对应 count[j] 加一并 break,保证每个拐点只计入最低的满足条件的那档。 统计完之后,第二个 for 把序号、档位值、命中次数通过 FileWrite 写进句柄 h 指向的文件,然后 FileClose 收尾,Print 打出“工作完成”和 name。整个函数最终 return rates_total,表示处理了这么多根 K 线。 下面的 TimeFrameToShortString 是个辅助:把 ENUM_TIMEFRAMES 枚举映射成 M1、H1 这类短字符串,方便文件名或日志标记周期。MT5 里 PERIOD_M2、M3、M4 这类非标准周期也覆盖了,写文件命名时不会漏。
for(class="type">int i=class="num">0;i<rates_total;i++) { if(ZigZagBuffer[i]==class="num">0 || ZigZagBuffer[i]==EMPTY_VALUE) class="kw">continue; else { for(class="type">int j=class="num">0; j<class="num">100; j++) { if(ZigZagBuffer[i]<levels[j]) { count[j]++; class="kw">break; } } } } for(class="type">int i=class="num">0; i<class="num">100; i++) { FileWrite(h,i,levels[i],count[i]); } FileClose(h); Print("工作完成: ",name); } class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string TimeFrameToShortString(ENUM_TIMEFRAMES period) { class="kw">switch (period ) { case PERIOD_M1: class="kw">return ("M1"); case PERIOD_M2: class="kw">return ("M2"); case PERIOD_M3: class="kw">return ("M3"); case PERIOD_M4: class="kw">return ("M4"); case PERIOD_M5: class="kw">return ("M5"); case PERIOD_M6: class="kw">return ("M6"); case PERIOD_M10: class="kw">return ("M10"); case PERIOD_M12: class="kw">return ("M12"); case PERIOD_M15: class="kw">return ("M15"); case PERIOD_M20: class="kw">return ("M20"); case PERIOD_M30: class="kw">return ("M30"); case PERIOD_H1: class="kw">return ("H1"); case PERIOD_H2: class="kw">return ("H2"); case PERIOD_H3: class="kw">return ("H3"); case PERIOD_H4: class="kw">return ("H4"); case PERIOD_H6: class="kw">return ("H6"); case PERIOD_H8: class="kw">return ("H8");
「周期枚举到字符串的收口写法」
上面这段 switch 的末尾几个分支,把 MT5 里非常规周期也兜住了:H12(12 小时)、D1(日线)、W1(周线)、MN1(月线),各自直接返回对应缩写字符串。 没匹配到任何已知 ENUM_TIMEFRAMES 值时,函数落到最后的 return(""),返回一个空串而不是报错,调用层可以用空串判断“未知周期”。 在写多周期扫描 EA 时,这种映射函数建议单独抽出来:你传 PERIOD_W1 进去,拿到 "W1" 就能直接拼进文件名或日志;外汇与贵金属波动受周期切换影响明显,高周期信号滞后但噪声低,实盘前务必在 MT5 用不同品种回测验证映射是否漏了 broker 自定义周期。
case PERIOD_H12: class="kw">return ("H12"); case PERIOD_D1: class="kw">return ("D1"); case PERIOD_W1: class="kw">return ("W1"); case PERIOD_MN1: class="kw">return ("MN1"); } class="kw">return (""); }
◍ 篮子指标到底给出多少潜在信号
把组合 WPR 约束在 -100% 到 +100% 区间后,先按常规把超买/超卖边界设在 ±60%。但要评估真实可交易机会的密度,得统计指标冲破更极端阈值(±70%、±80%)的频次。做法是在组合 WPR 图表上加载 testIndexZig-Zag1.mq5,让它以 1% 为间隔切片,数出每个间隔里的极值个数并落盘,再逐时间帧、逐货币篮重复。 以欧元篮为例,历史回看深度分别是 D1 自 2000.11.09、H4 自 2005.04.12、H1 自 2006.01.17。在 ±80% 阈值上,H4 时间帧累计出现 83 次极值越界,H1 是 165 次,D1 仅 25 次;放宽到 ±70% 后,三档变为 45 / 207 / 449。跨所有篮子与时间帧汇总,±80% 潜在入场共 3793 次,±70% 达 7885 次。 即便后续会对信号做清洗剔除,原始样本量依旧很厚。这也说明当前先把超买/超卖边界钉在 ±60% 是合理的——极端区外还有充足概率性机会可供筛选。外汇与贵金属市场杠杆高、滑点跳空频繁,这些计数只是历史分布,实盘信号触发率会随波动状态漂移,参数务必留调整余地。
把入场形态翻译成算法条件
组合 WPR 下穿超买线 +60% 时,是卖出一篮子货币对的触发点,但要求蜡烛收盘时指标值仍不低于 +50%,且下降中的指标线不能跌破 +70%。这条 +70% 的底线还有个替代方案:超买等级设为 +70% 时,指标线需在 +80% 以上才算合格。 买入逻辑与卖出完全对称,只是方向反过来。原文图中高亮的那三种形态,数值全部卡在上述区间内,说明它们不是肉眼感觉“漂亮”,而是能被精确量化的形态。 把 +60%、+50%、+70%(或 +80%/+70%)这几道阈值写死,形态识别就从主观看图变成了可回测的规则。这一步不走完,后续根本没法交给 EA 自动跑。
「用篮子EA把形态跑出来」
把货币篮子当成一个组合来测形态,比单品种更贴近美元整体强弱。之前那篇讲篮子交易的表格可以直接搬进EA逻辑:点1触发阈值、点2确认区间、组合WPR回到0%附近平仓。这一步目的不是证明EA能赚钱,而是把形态形状在MT5里可视化出来。 附件里的 testBasket.mq5 挂在 EURUSD 上就能跑美元篮子(EURUSD、GBPUSD、AUDUSD、NZDUSD为空头方向,USDCAD、USDCHF、USDJPY为多头方向)。输入参数里 SELL1LIMIT 默认70、SELL2FROM 默认60、SELL2TO 默认50,对应超买区识别;BUY 系列对称取负。WPR 周期默认20。 从2016年1月的历史数据起测,第一种用默认阈值,第二种把超买/超卖线整体上移:SELL1LIMIT 改80、SELL2FROM 改70、BUY1LIMIT 改-80、BUY2FROM 改-70。两种形态的交易次数、持仓时间和回撤差异,能在导出的 html 报告里直接比对,但标准报告不含我们要的形态分布数据,得自己从成交序列里捞。 外汇与贵金属属高风险品种,篮子组合放大了杠杆暴露,回测形态不等于实盘概率,参数平移后胜率可能明显漂移。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| testBasket.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2017, MetaQuotes 软件公司| class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2017, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- 输入参数 class="macro">#include <Trade\Trade.mqh> class="macro">#define LG class="num">7 input class="type">int SELLPROFIT = class="num">0; input class="type">int SELL1LIMIT = class="num">70; input class="type">int SELL2FROM = class="num">60; input class="type">int SELL2TO = class="num">50; input class="type">int BUYPROFIT = class="num">0; input class="type">int BUY1LIMIT = -class="num">70; input class="type">int BUY2FROM = -class="num">60; input class="type">int BUY2TO = -class="num">50; input class="type">int WPR=class="num">20; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ enum BSTATE { BCLOSE = class="num">0, BBUY = class="num">1, BSELL = class="num">2 }; class="type">class="kw">string pair[]={"EURUSD","GBPUSD","AUDUSD","NZDUSD","USDCAD","USDCHF","USDJPY"}; class="type">bool bDirect[]={class="kw">false,class="kw">false,class="kw">false,class="kw">false,true,true,true}; class="type">class="kw">datetime TimeParam[class="num">3]; class="type">class="kw">double dWpr[class="num">3]; class="type">ulong Ticket[LG]; class="type">class="kw">double TradeResult[LG]; class="type">class="kw">double TradeCurrency; class="type">class="kw">double Drw;
◍ 多品种WPR合成与日志落盘的实现细节
这段逻辑把 LG 个品种的威廉指标(iWPR)句柄在 OnInit 里一次性建好,之后每秒由定时器驱动轮询。GetValue1 函数对 shift 位置的各品种缓冲做平均,方向标记 bDirect 决定是加原值还是取负偏移,最终用 (res+50)*2 把 -100~0 的区间映射到 0~100,再按 _Digits 规范化。 初始化时 mDrw 被设为 1000000 作为回撤基准,TradeTotalResult 与 dWpr 分别清零和置空,bstate 切到 BCLOSE 等待信号。文件部分用 FileFindFirst 探测当前品种+周期命名的 html 是否存在,存在就追加序号重新拼名,直到拿到 INVALID_HANDLE 才 FileOpen 新建,避免覆盖历史记录。 直接把下面代码贴进 MT5 专家顾问的 OnInit 和辅助函数区,改 LG、pair[]、WPR 三个宏就能跑通多品种 WPR 的合成与日志初始化。外汇与贵金属波动剧烈,这类合成指标只反映概率倾向,实盘前务必在策略测试器用至少 3 个月 tick 数据验证。
class="type">class="kw">string sLog; class="type">class="kw">double TradeTotalResult[LG]; class="type">class="kw">double TradeTotalCurrency; class="type">int iTradeCount; class="type">class="kw">double mDrw; class="type">int h1[LG]; BSTATE bstate; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetValue1(class="type">int shift) { class="type">class="kw">double dBuf[class="num">1]; class="type">class="kw">double res=class="num">0.0; for(class="type">int i=class="num">0; i<LG; i++) { CopyBuffer(h1[i],class="num">0,shift,class="num">1,dBuf); if(bDirect[i]==true) res+=dBuf[class="num">0]; else res+=-(dBuf[class="num">0]+class="num">100); }class=class="str">"cmt">//end for (class="type">int i = class="num">0; i < iCount; i++) res=res/LG; class="kw">return (NormalizeDouble((res + class="num">50) * class="num">2, _Digits) ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int lh; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { EventSetTimer(class="num">1); for(class="type">int i=class="num">0; i<LG; i++) { h1[i]=iWPR(pair[i],class="num">0,WPR); } bstate=BCLOSE; ArrayInitialize(TradeTotalResult,class="num">0); ArrayInitialize(dWpr,EMPTY_VALUE); TradeTotalCurrency=class="num">0; iTradeCount=class="num">0; mDrw=class="num">1000000; lh=INVALID_HANDLE; class="type">class="kw">string lname = _Symbol + "_" + TimeFrameToShortString(Period() ); class="type">class="kw">string t1, t = lname; class="type">int i=class="num">0; for(;;) { t+=".html"; class="type">long lg=FileFindFirst(t,t1); if(lg==INVALID_HANDLE) { lh= FileOpen(t,FILE_WRITE | FILE_TXT | FILE_ANSI); Print("CREATE ",t); class="kw">break; } FileFindClose(lg); t=lname+"_"+IntegerToString(i++); }