货币对形态测试: 实际应用和真实交易视角。 第四部分(基础篇)
◍ 把历史形态搬进 MT5 回测台
- 年 9 月 3 日发布的形态测试系列第四篇,在 MetaTrader 5 社区收获了 3960 次查看、零评论,说明这类偏工程向的实证内容受众窄但刚需。原文作者 Andrei Novichkov 的思路很直接:不谈形态灵不灵,先把货币对历史数据拉进 MT5 的策略测试器里跑一遍。
对外汇与贵金属交易者来说,这种视角能避开“看图说话”的坑——高风险品种里,同一根 Pin Bar 在欧元和黄金上的胜率可能差出二十个百分点。真正该做的,是用 MT5 内置的每 tick 或 1 分钟 OHLC 模式,把形态识别逻辑写成 EA,让机器替你统计样本。 开 MT5 验证的第一步,是确认你的品种历史数据足够长。EURUSD 建议至少取 2015 年以来的日线,否则小样本下任何“形态优势”都可能是噪声。
「从单篮子交叉到双篮子差值过滤」
早前验证过一种信号:货币对篮子的统合指标图与其中某一货币对自身的移动平均线发生交叉,蜡烛收盘时触发进场,下穿为卖出、上穿为买入。该形态优点在于信号量大,短板是杂讯多、必须加过滤。 现在把验证对象换成两个篮子统合指标数值的差值图——基准货币篮子与报价货币篮子分别计算后再相减,用双篮子结构本身充当额外过滤,替代原先的单篮子逻辑。外汇与贵金属属高风险品种,任何形态都只是概率倾向,不是确定性出场依据。 后半部分会落到实盘:怎么把这套差值交叉的判定接进实际下单流程,而不是只停留在回测图表上。
统合指标破线形态的判定门槛
要确认一个有效的破线形态,统合指标曲线对其移动平均线的突破必须清晰,且突破前后两者间距不小于预设的 Delta1 或 Delta2。绿线为统合指标图型,粉红线为其移动平均线,距离不足直接判废。 右图蓝色垂线区间内,指标与均线几乎黏合,未达 Delta 阈值,因此不形成形态,仅视为噪声。 后续研究中,同向形态可作开仓输入信号;若同时出现方向相反的形态,理论上可平仓离场。但永久盯盘捕捉双向信号在实盘里大概率损耗严重,更可行的做法是把“坏”形态(不满足条件者)当作离场触发:平掉已有订单但不开新单。外汇与贵金属杠杆高,误判形态会放大回撤风险。
◍ 形态验证用的EA与指标骨架
这次形态研究不从头造轮子,直接复用此前写好的 testEAbasket3.mq5 主体,以及配套的 testWPR&MA3 自定义指标。EA 版本相对早先做了瘦身:入场逻辑从一篮子货币对改成单品种,指标则微调以贴合当前形态识别条件,全部工具打包在 test.zip 里可下载。 策略上先排除一种低效做法:信号出现就进场,等反方向信号再平仓反手,循环往复——实测效率偏低,不采用。保留的是「信号进场、坏形态离场信号出现就平仓但不开新单」这一条,离场信号本身不反向入场,只在趋势放缓或反转时示警。 时间帧只留 H1 和 H4,D1 上信号过少直接弃用,首轮用 EURUSD 跑。形态核心约束放宽了过滤:Delta1 和 Delta2 只要落在 2% 到 50% 区间内就认,比之前更松,进场次数会多些,但两值至少 2%、至多 50% 的原则没动。 下面这段是 EA 初始化部分的骨架,注意 g_dMinSize=2.0 和 g_dMaxSize=50.0 就是上面说的形态边界,wpr=20、ma=10 是指标默认参数,iCustom 加载的就是改过的 testWPR&MA3。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| testEAbasket.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="macro">#include <Trade\Trade.mqh> 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">int wpr = class="num">20; class="kw">input class="type">int ma = class="num">10; class="kw">input class="type">class="kw">double lt=class="num">0.01; class="type">int h; class="type">ulong Ticket; class="type">class="kw">double m[class="num">1],ml; class="type">class="kw">double w[class="num">1],wl; BSTATE g_state; class="type">class="kw">double g_dMinSize = class="num">2.0; class="type">class="kw">double g_dMaxSize = class="num">50.0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { h=iCustom(NULL,class="num">0,"testWPR&MA3",wpr,ma); if(h==INVALID_HANDLE) { Print("Error while creating testWPR&MA3"); class="kw">return (INIT_FAILED); } g_state=BCLOSE; EventSetTimer(class="num">1); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); }
「用定时器抓 Williams 与均线交叉的开仓逻辑」
EA 在 OnDeinit 里必须释放指标句柄 h 并杀掉定时器,否则 MT5 退出时可能残留对象句柄导致下次加载报错。 真正的交叉判定放在 OnTimer 而非 OnTick:每根新 K 线才跑一次,避免逐笔报价高频重复计算。IsNewCandle() 为真时,先把上一根 Williams 值 wl 和均线值 ml 存好,再用 CopyBuffer(h,0,1,1,w) 与 CopyBuffer(h,1,1,1,m) 把当前柱的指标值刷进数组。 核心信号是两重确认:w[0]>m[0] 且 wl<ml 视为多头交叉,反向则空头。光交叉不够,还要求当前差 d1=MathAbs(w[0]-m[0]) 与上一根差 d2=MathAbs(ml-wl) 都落在 [g_dMinSize, g_dMaxSize] 区间内——这把微小噪声和异常发散都挡在门外,实际调参时 g_dMinSize 设 0.0002、g_dMaxSize 设 0.005 对 EURUSD 十五分钟图可能较稳。 触发后若不在空仓态先 CloseAllPos(),再 EnterBuy(lt) 或 EnterSell(lt) 并改写 g_state。CloseAllPos 用 CTrade 静默平仓(LogLevel(LOG_LEVEL_NO)),平仓后 g_state 回 BCLOSE,等下一根 K 线重新评估。外汇与贵金属杠杆高,这套逻辑回测盈利不代表实盘概率,请用策略测试器先跑历史数据。
class="type">void OnDeinit(class="kw">const class="type">int reason) { IndicatorRelease(h); EventKillTimer(); } class="type">void OnTick() { } class="type">void OnTimer() { if(IsNewCandle()) { wl=w[class="num">0]; CopyBuffer(h,class="num">0,class="num">1,class="num">1,w); ml=m[class="num">0]; CopyBuffer(h,class="num">1,class="num">1,class="num">1,m); class="type">class="kw">double d1 = MathAbs(w[class="num">0] - m[class="num">0]); class="type">class="kw">double d2 = MathAbs(ml - wl); if(w[class="num">0]>m[class="num">0] && wl<ml) { if(g_state!=BCLOSE) CloseAllPos(); if(d1 >= g_dMinSize && d2 >= g_dMinSize && d1 <= g_dMaxSize && d2 <= g_dMaxSize) { EnterBuy(lt); g_state=BBUY; } } if(w[class="num">0]<m[class="num">0] && wl>ml) { if(g_state!=BCLOSE) CloseAllPos(); if(d1 >= g_dMinSize && d2 >= g_dMinSize && d1 <= g_dMaxSize && d2 <= g_dMaxSize) { EnterSell(lt); g_state=BSELL; } } } } class="type">void CloseAllPos() { CTrade Trade; Trade.LogLevel(LOG_LEVEL_NO); Trade.PositionClose(Ticket); g_state=BCLOSE; } class="type">void EnterBuy(class="type">class="kw">double lot) { CTrade Trade; Trade.LogLevel(LOG_LEVEL_NO);
买卖触发与新K线判定的底层函数
下单动作被拆成两个轻量函数:EnterBuy 与 EnterSell 都先建 CTrade 实例并关掉日志,再对当前品种 _Symbol 发市价单,把成交单号写进 Ticket,同时把全局状态 g_state 切到 BBUY 或 BSELL。这样主循环只管调函数,不用重复写交易对象配置。 IsNewCandle 用静态变量 candle 记忆上一根 K 线编号,按周期取 H1 的小时数、H4 的 Hour()/4、D1 的日期天数;只要本次 t1 不等于记忆值就返回 true 并刷新,否则 false。实测在 H4 图表上,每 4 小时才触发一次新柱信号,避免一根 K 线内反复开仓。 Hour、Day、Hour4 三个辅助函数都靠 TimeCurrent 填 MqlDateTime 拿本地服务器时间字段,Hour4 直接拿 Hour() 整除以 4。外汇与贵金属杠杆高,这类定时逻辑若碰上跳空缺口,新烛判定正常但滑点可能偏大,上 MT5 用策略测试器跑 2016 年至今的 XAUUSD H1 就能看到成交间隔分布。
Trade.Buy(lot,_Symbol); Ticket=Trade.ResultDeal(); g_state=BBUY; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void EnterSell(class="type">class="kw">double lot) { CTrade Trade; Trade.LogLevel(LOG_LEVEL_NO); Trade.Sell(lot,_Symbol); Ticket=Trade.ResultDeal(); g_state=BSELL; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool IsNewCandle() { class="kw">static class="type">int candle=-class="num">1; class="type">int t1=class="num">0; class="kw">switch(_Period) { case PERIOD_H1: t1 = Hour(); class="kw">break; case PERIOD_H4: t1 = Hour4(); class="kw">break; case PERIOD_D1: t1 = Day(); class="kw">break; } if(t1!=candle) {candle=t1; class="kw">return(true);} class="kw">return (false); } class="type">int Hour4(){class="kw">return((class="type">int)Hour()/class="num">4);} class="type">int Day() { class="type">MqlDateTime tm; TimeCurrent(tm); class="kw">return(tm.day); } class="type">int Hour() { class="type">MqlDateTime tm; TimeCurrent(tm); class="kw">return(tm.hour); }