货币对形态测试: 实际应用和真实交易视角。 第四部分·进阶篇
📊

货币对形态测试: 实际应用和真实交易视角。 第四部分·进阶篇

(2/3)· 当单一货币对均线交叉信号泛滥却胜率堪忧,双篮子统合差值能否成为那道额外滤网

偏理论进阶 第 2/3 篇
很多交易者把统合指标上穿均线当成直接进场依据,结果被密集信号反复打脸。本文换个视角:用基准与报价货币两个篮子的差值图型来识别形态,把噪声过滤交给结构本身。接上篇的形态基础,这一 Part 我们钻进真实回测和实际部署。

「双币 WPR 指标的句柄与画线初始化」

要把欧元与美元各自的威廉指标(WPR)叠到一张副图上,第一步是分别拿到两个自定义指标的句柄。代码里用 iCustom 在当前图表(NULL,0)调名为 testWPReur 与 testWPRusd 的指标,周期参数 WPR 默认 20;任一句柄返回 INVALID_HANDLE 就打印错误并让 OnInit 返回 INIT_FAILED,避免在 MT5 里挂出空指标。 IndicatorSetInteger(INDICATOR_LEVELS,2) 设了两条水平线,值分别为 -60 与 60,颜色 clrRed、线宽 1。这是 WPR 常用的超买超卖边界:价格落入 ±60 外时,欧元或美元单边动能倾向增强,但外汇与贵金属属高风险品种,越界只代表概率倾斜而非反转确认。 缓存区用 ArraySetAsSeries(ind,true) 把索引 0 对齐最新 K 线,SetIndexBuffer 把 ind 绑到 plot 0、ma 绑到 plot 1。plot 0 画线类型 DRAW_LINE、宽度 2、颜色取输入参数 clr(默认 clrGreen);plot 1 同样画线,准备接后面的 MA 平滑。开 MT5 把这段塞进 OnInit,编译后副图应出现绿线 WPR 与一条待填充的品红 MA 通道。

MQL5 / C++
class="macro">#class="kw">property indicator_plots   class="num">2
class="kw">input class="type">int WPR      = class="num">20; class=class="str">"cmt">// WPR 周期
class="kw">input class="type">int maperiod = class="num">10; class=class="str">"cmt">// MA 周期
class="kw">input class="type">class="kw">color  clr   = clrGreen;
class="kw">input class="type">class="kw">color  clrMA = clrMagenta;
class="type">int h,h1;
class="type">class="kw">double ind[],ma[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓存区映射
   h = iCustom(NULL,class="num">0,"testWPReur",WPR);
   if (h == INVALID_HANDLE) {
       Print("Error while creating testWPR");
       class="kw">return (INIT_FAILED);
   }
   h1 = iCustom(NULL,class="num">0,"testWPRusd",WPR);
   if (h1 == INVALID_HANDLE) {
       Print("Error while creating testWPR");
       class="kw">return (INIT_FAILED);
   }   
  IndicatorSetString(INDICATOR_SHORTNAME,"testWPRusd");
  IndicatorSetInteger(INDICATOR_DIGITS,class="num">2);
  IndicatorSetInteger(INDICATOR_LEVELS,class="num">2);
  IndicatorSetInteger(INDICATOR_LEVELSTYLE,class="num">0,STYLE_SOLID);
  IndicatorSetInteger(INDICATOR_LEVELSTYLE,class="num">1,STYLE_SOLID);
  IndicatorSetInteger(INDICATOR_LEVELCOLOR,class="num">0,clrRed);
  IndicatorSetInteger(INDICATOR_LEVELCOLOR,class="num">1,clrRed);
  IndicatorSetInteger(INDICATOR_LEVELWIDTH,class="num">0,class="num">1);
  IndicatorSetInteger(INDICATOR_LEVELWIDTH,class="num">1,class="num">1);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">0,-class="num">60);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">1,class="num">60);
  ArraySetAsSeries(ind,true);
  SetIndexBuffer(class="num">0,ind);
  PlotIndexSetInteger(class="num">0,PLOT_DRAW_TYPE,DRAW_LINE);
  PlotIndexSetInteger(class="num">0,PLOT_LINE_STYLE,STYLE_SOLID);
  PlotIndexSetInteger(class="num">0,PLOT_LINE_WIDTH,class="num">2);
  PlotIndexSetInteger(class="num">0,PLOT_LINE_COLOR,clr);
  PlotIndexSetString(class="num">0,PLOT_LABEL,"_tstWPRusd_");
  ArraySetAsSeries(ma,true);
  SetIndexBuffer(class="num">1,ma);
  PlotIndexSetInteger(class="num">1,PLOT_DRAW_TYPE,DRAW_LINE);

◍ 差值均线指标的绘图与计算落地

指标初始化收尾时,对编号为 1 的绘图缓冲做了实线、线宽 1、颜色 clrMA 的设定,并打上标签 Middle_Basket_line_MA,随后返回 INIT_SUCCEEDED 宣告装载完成。 GetValue 函数负责取数:用 CopyBuffer 分别从句柄 h 与 h1 读取 shift 位置的第 0 缓冲值各 1 个,做差除以 2 后用 NormalizeDouble 按 _Digits 精度舍入返回。这一行就是篮子中间线的核心差值逻辑。 OnCalculate 里只在 prev_calculated==0 或新 K 线多于 1 根时才全量重算:先按 rates_total-WPR 跑 ind 缓冲的差值填充,再退 maperiod 根跑 ma 缓冲的移动均线。否则仅更新 ind[0],避免每 tick 重算历史。 外汇与贵金属市场杠杆高、滑点随机,这类篮子指标仅反映多品种价差的中间状态,信号可能滞后,实盘前务必在 MT5 策略测试器用历史数据验证 WPR 与 maperiod 的敏感性。

MQL5 / C++
PlotIndexSetInteger(class="num">1,PLOT_LINE_STYLE,STYLE_SOLID);
PlotIndexSetInteger(class="num">1,PLOT_LINE_WIDTH,class="num">1);
PlotIndexSetInteger(class="num">1,PLOT_LINE_COLOR,clrMA);
PlotIndexSetString(class="num">1,PLOT_LABEL,"Middle_Basket_line_MA");
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double GetValue(class="type">int shift)
  {
   class="type">class="kw">double dBuf[class="num">1], dBuf1[class="num">1];
   class="type">class="kw">double res=class="num">0.0;
   CopyBuffer(h,class="num">0,shift,class="num">1,dBuf);
   CopyBuffer(h1,class="num">0,shift,class="num">1,dBuf1);
   class="kw">return (NormalizeDouble((dBuf[class="num">0] - dBuf1[class="num">0])/class="num">2, _Digits) );
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标迭代函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime &time[],
                class="kw">const class="type">class="kw">double &open[],
                class="kw">const class="type">class="kw">double &high[],
                class="kw">const class="type">class="kw">double &low[],
                class="kw">const class="type">class="kw">double &close[],
                class="kw">const class="type">long &tick_volume[],
                class="kw">const class="type">long &volume[],
                class="kw">const class="type">int &spread[])
  {
class=class="str">"cmt">//---
   if(prev_calculated==class="num">0 || rates_total>prev_calculated+class="num">1)
     {
      class="type">int rt=rates_total-WPR;
      for(class="type">int i=class="num">1; i<rt; i++)
        {
         ind[i]=GetValue(i);
        }
      rt-=maperiod;
      for(class="type">int i=class="num">1; i<rt; i++)
        {
         ma[i]=GetMA(ind,i,maperiod,_Digits);
        }
     }
   else
     {
      ind[class="num">0] = GetValue(class="num">0);

自定义均值的取数与上手验证点

这段收尾代码把双指标句柄的释放和均值计算拆成了独立函数,逻辑上和 MT5 自带 iMA 调用解耦,方便你塞进自己的指标工程里改算法。 OnDeinit 里对 h 和 h1 分别做 INVALID_HANDLE 判断再 IndicatorRelease,能避免重复释放句柄导致终端报 'invalid handle' 错误;实盘加载/卸载指标时若日志出现该提示,先查这里。 GetMA 用 for 循环对 arr[index+j] 累加 period 根后除以 period,最后 NormalizeDouble(m,digit) 按 _Digits 截断小数位。若你看 EURUSD 的 M15 图表、maperiod=14,函数返回的均值小数位会锁定在券商报价的 4~5 位精度,不会多出浮点噪声。 把这段直接贴进 MQ5 指标源码,改 maperiod 从 14 调到 50,重载后观察均线上移滞后期变长,可验证周期参数对平滑度的真实影响。外汇与贵金属杠杆高,参数回测结论仅代表历史概率,不构成方向建议。

MQL5 / C++
  ma[class="num">0]   = GetMA(ind, class="num">0, maperiod, _Digits);
   }
class=class="str">"cmt">//--- 返回 prev_calculated 的值用于下次调用
   class="kw">return(rates_total);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
     if(h!=INVALID_HANDLE) IndicatorRelease(h);
     if(h1!=INVALID_HANDLE) IndicatorRelease(h1);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double GetMA(class="kw">const class="type">class="kw">double &arr[],class="type">int index,class="type">int period,class="type">int digit)
  {
   class="type">class="kw">double m=class="num">0;
   for(class="type">int j=class="num">0; j<period; j++) m+=arr[index+j];
   m/=period;
   class="kw">return (NormalizeDouble(m,digit));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「H4 盈利 H1 亏损的跨货币验证」

在去年 H1 历史报价上跑完一轮后,结论和先前一致:H1 周期下该形态组合整体亏损,切到 H4 则转为盈利。测试覆盖 EURUSD、GBPUSD、AUDUSD、NZDUSD、USDCAD、USDCHF、USDJPY 七大直盘,没有冒出意外结果,H4 表现依旧令人鼓舞,H1 依旧拖累。 尝试用合并 WPR 图型碰超买/超卖边界来过滤:向上触破超买不做多,向下触破超卖不做空。把标准 ±60 调到 ±50 后在 EURUSD 上重测,H1 与 H4 报告都和前两次无本质区别,说明这种过滤在测试条件下没带来明显改善。外汇与贵金属杠杆高,历史回测不代表未来,实盘前务必在 MT5 策略测试器自跑一遍。 最终把形态降级成过滤器而非主信号:合并 WPR 向上破超卖、且收盘仍站住,同时移动平均线位于组合指标图型下方,才允许做多;做空反向。组合 WPR 回到 0% 就离场。下面这段 OnTimer 是识别与触发的核心,挂在定时器上每根新蜡烛判一次。 代码里 IsNewCandle 控节奏,w 和 m 分别存组合 WPR 与均线当前/前值;d1、d2 是两者间距,须落在 g_dMinSize~g_dMaxSize 之间才认形态。w[0]>m[0] 且前值 wl<ml 走多分支,附加 w[0]<60 防超买;反向下穿则走空,附加 w[0]>-60 防超卖。外汇品种波动结构不同,参数边界建议按品种单独标定。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 定时器函数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
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 && w[class="num">0] < class="num">60)
           {
            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 && w[class="num">0] > -class="num">60)
           {
            EnterSell(lt);
            g_state=BSELL;
           }
        }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
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 软件公司"

◍ EA 骨架里的状态机与参数边界

这段 MT5 智能系统的初始化代码把交易状态拆成了三态枚举:BCLOSE=0 空仓、BBUY=1 做多、BSELL=2 做空,后续逻辑会按这个状态机切换,而不是在 OnTick 里直接堆条件判断。 输入参数给了 wpr=20(WPR 周期)、ma=10(均线周期)、lt=0.01(固定手数)。注意 SELL1LIMIT=50 与 BUY1LIMIT=-50 这组对称阈值,意味着空单首限挂在 50 点、多单首限挂在 -50 点,第二层区间则分别是 40~20 与 -40~-20,属于典型的网格分层挂单结构。 OnInit 里用 iCustom 加载名为 "testWPR&MA3" 的自定义指标,若返回 INVALID_HANDLE 则打印错误并 INIT_FAILED,否则状态置 BCLOSE 并 EventSetTimer(1) 开 1 秒定时器。g_dMinSize=2.0、g_dMaxSize=50.0 这两个 double 变量大概率是后续过滤挂单间距用的上下限。 外汇与贵金属杠杆高,这类固定手数 + 分层挂单的 EA 在单边行情中可能快速放大浮亏,上 MT5 前务必先用策略测试器跑历史数据验证阈值。

MQL5 / C++
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Trade\Trade.mqh>
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=class="str">"cmt">//手数
class="type">int SELLPROFIT =  class="num">0;
class="type">int SELL1LIMIT =  class="num">50;
class="type">int SELL2FROM  =  class="num">40;
class="type">int SELL2TO    =  class="num">20;
class="type">int BUYPROFIT  =  class="num">0;
class="type">int BUY1LIMIT  = -class="num">50;
class="type">int BUY2FROM   = -class="num">40;
class="type">int BUY2TO     = -class="num">20;
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);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统逆初函数                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
   IndicatorRelease(h);
   EventKillTimer();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统逐笔报价函数                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

定时器里的开平逻辑怎么落进EA

EA 用 OnTimer 做周期轮询,只在 IsNewCandle 判定出新 K 线时才处理一次信号,避免同一根 bar 内反复刷单。新柱触发后先把上根 w[0] 存进 wl,再用 CopyBuffer(h,0,1,1,w) 把指标缓冲第 1 号位移(即刚闭合那根)拉进当前数组,ml 同理取自缓冲 1。 平仓分支很直白:若状态是 BBUY 且 w[0]>=BUYPROFIT 就 CloseAllPos;BSELL 且 w[0]<=SELLPROFIT 同样清仓。这两个阈值由宏或外部参数给定,实战里黄金 5 分钟图把 BUYPROFIT 设 0.35 可能比 0.5 更早抽身,但回撤次数也会增多。 进场挂在 BCLOSE 状态:买的条件是 w[0] 落在 BUY2FROM~BUY2TO 区间,且上根 wl<=BUY1LIMIT,同时 w[0]>m[0](快线高于慢线);卖则对称,w[0] 在 SELL2FROM~SELL2TO,wl>=SELL1LIMIT 且 w[0]<m[0]。命中即 EnterBuy/EnterSell 并 return,不重叠开仓。 CloseAllPos 与 Enter 系列都用 CTrade,LogLevel 设 NO 屏蔽日志刷屏,PositionClose 用全局 Ticket 平掉那一张。Enter 里 Trade.Buy 后立刻 Ticket=Trade.ResultDeal() 把成交单号接住,g_state 切到对应持仓态,下一根定时器再来时才走止盈判断。外汇与贵金属杠杆高,这类定时市价单在滑点行情可能少赚几个点差,上 MT5 用策略测试器跑 2023 年至今 XAUUSD 就能看到实际成交间隙。

MQL5 / C++
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);
       if(g_state==BBUY && (w[class="num">0]>=BUYPROFIT))
         {
          CloseAllPos();
         }
       if(g_state==BSELL && (w[class="num">0]<=SELLPROFIT))
         {
          CloseAllPos();
         }
       if(g_state==BCLOSE && w[class="num">0]>=BUY2FROM && w[class="num">0]<=BUY2TO && wl<=BUY1LIMIT && w[class="num">0] > m[class="num">0])
         {
           EnterBuy(lt);
           class="kw">return;        
         }
       
       if(g_state==BCLOSE && w[class="num">0]<=SELL2FROM && w[class="num">0]>=SELL2TO && wl>=SELL1LIMIT && w[class="num">0] < m[class="num">0])
         {
           EnterSell(lt);
           class="kw">return;        
         }
     }
  }
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);
   Trade.Buy(lot,_Symbol);
   Ticket=Trade.ResultDeal();
   g_state=BBUY;
  }
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="type">bool IsNewCandle()
  {
让小布替你跑这套差值诊断
把双篮子统合指标与均线的距离阈值监控交给小布盯盘,你只在形态清晰时看一眼信号质量,不用自己天天拉图比对 Delta1、Delta2。

常见问题

距离过窄意味着突破不够清晰,容易受到随机波动干扰,这类信号在回测中往往对应需要额外过滤的坏形态,放松距离限制虽增加次数但会侵蚀原则。
小布盯盘的 AIGC 看板可自定义统合指标与均线差值监控,把本文的 Delta 阈值设进去就能自动标出候选形态,省去手动翻图。
永久在场意味着不断开平仓,摩擦成本和滑点会吞掉正面特性,更务实的做法是把坏形态作为平仓离场而不开新单。
主体 EA 的入场模块从整个篮子改为单个货币对,数据依赖轻微改动的 testWPR&MA3 指标以匹配差值图型的形态识别条件。
完整铺垫在《货币对形态测试: 实际应用和真实交易视角·基础篇》,本篇默认读者已熟悉篮子构建方式。