逆转形态:测试双顶/双底形态·综合运用
📘

逆转形态:测试双顶/双底形态·综合运用

第 3/3 篇

「确认周期与搜索窗口的刷新逻辑」

这段逻辑解决了一个实盘常见坑:多周期形态扫描时,若每次 tick 都重跑全历史,CPU 会被拖死。代码先用 SeriesInfoInteger 取确认周期(e_ConfirmationTF)最后一根 K 线时间,与上次记录的 Last_CfTF 比较,时间没推进就直接 return,避免重复计算。 对象数组 ar_Objects 从下标 2 开始遍历,遇到指针失效(POINTER_INVALID)或形态校验不通过(CheckPattern 返回 false)的就删掉,删除后 i 自减、total 减一,保证不漏扫。这种惰性清理能让数组只留有效形态实例。 搜索窗口靠 e_TimeFrame 的最后 BAR 时间驱动,Last_WT 记录上次触发点。start_search 取 iTime 在限制历史根数(i_MaxHistory 与 Bars 取小)对应的时间,防止回测时扫太深。CPattern 对象首次 invalid 才 new,并调 Create 用最小/最大回调参数(d_MinCorrection、d_MaxCorrection)初始化,失败即释放并返回。 主循环里 Pattern.Search 不断向前找,每次把 start_search 推到当前形态末端时间加一个周期秒数之后;若数组里已有相同形态(Compare==0)或 CheckPattern 不过,就跳过。找到合法新形态才 Add 进数组并 new 下一个 CPattern 继续。外汇与贵金属波动剧烈,这种周期门控在高杠杆下能显著降低 EA 卡顿概率,建议直接拷进 MT5 用 Print 打点验证触发频率。

MQL5 / C++
  class="type">class="kw">datetime series=(class="type">class="kw">datetime)SeriesInfoInteger(_Symbol,e_ConfirmationTF,SERIES_LASTBAR_DATE);
  if(Last_CfTF>=series)
     class="kw">return;
  Last_CfTF=series;
  class="type">int total=ar_Objects.Total();
  for(class="type">int i=class="num">2;i<total;i++)
     {
       if(CheckPointer(ar_Objects.At(i))==POINTER_INVALID)
          if(ar_Objects.Delete(i))
             {
               i--;
               total--;
               class="kw">continue;
             }
class=class="str">"cmt">//---
       if(!CheckPattern(ar_Objects.At(i)))
         {
           if(ar_Objects.Delete(i))
             {
               i--;
               total--;
               class="kw">continue;
             }
         }
     }
  class="kw">static class="type">class="kw">datetime Last_WT=class="num">0;
  series=(class="type">class="kw">datetime)SeriesInfoInteger(_Symbol,e_TimeFrame,SERIES_LASTBAR_DATE);
  if(Last_WT>=series)
     class="kw">return;
  start_search=iTime(_Symbol,e_TimeFrame,fmin(i_MaxHistory,Bars(_Symbol,e_TimeFrame)));
  if(CheckPointer(Pattern)==POINTER_INVALID)
     {
       Pattern=new CPattern();
       if(CheckPointer(Pattern)==POINTER_INVALID)
          class="kw">return;
       if(!Pattern.Create(ar_Objects.At(class="num">1),d_MinCorrection,d_MaxCorrection))
          {
            class="kw">delete Pattern;
            class="kw">return;
          }
     }
  Last_WT=series;
  class="kw">while(!IsStopped() && Pattern.Search(start_search))
     {
       start_search=fmax(start_search,Pattern.EndTrendTime()+PeriodSeconds(e_TimeFrame));
       class="type">bool found=class="kw">false;
       for(class="type">int i=class="num">2;i<ar_Objects.Total();i++)
         if(Pattern.Compare(ar_Objects.At(i),class="num">0)==class="num">0)
           {
             found=true;
             class="kw">break;
           }
       if(found)
          class="kw">continue;
       if(!CheckPattern(Pattern))
          class="kw">continue;
       if(!ar_Objects.Add(Pattern))
          class="kw">continue;
       Pattern=new CPattern();
       if(CheckPointer(Pattern)==POINTER_INVALID)
          class="kw">break;
       if(!Pattern.Create(ar_Objects.At(class="num">1),d_MinCorrection,d_MaxCorrection))
          {

信号触发后的分批止盈拆单逻辑

当 CheckPattern 拿到 signal 后,真正的下单动作在 switch 里完成。signal=1 走多,signal=-1 走空,两种分支对称处理,差别只在报价取自 SYMBOL_ASK 还是 SYMBOL_BID,以及止损加减点方向。 以多单为例,先取当前卖价作为入场价,然后清空 CLimitTakeProfit 的旧挂单池。tp1 与 tp2 两段止盈并非一次性挂死:若 tp1 距入场价大于经纪商 SYMBOL_TRADE_STOPS_LEVEL 最小止损距离,则按 (tp1-price)/_Point 的点数挂第一批,占用仓位 50 或 100(取决于 tp1、tp2 间距是否大于等于 1 点);剩余 to_close 初始为 100,减去已用部分后,若仍大于 0 且 tp2 也满足最小距离,则把余量挂到 tp2 处。 空单镜像处理:用买价入场,tp1/tp2 改为 price 减目标价算点数,止损落在 sl+i_SL*_Point。两个分支里只要 AddTakeProfit 返回失败或 Trade.Buy/Sell 返回真(即下单出错),函数立刻 return false,不再继续。 外汇与贵金属杠杆高,最小止损距离和滑点可能让 tp1 挂单直接被拒,实盘前应在 MT5 策略测试器里把 SYMBOL_TRADE_STOPS_LEVEL 打印出来核对,避免回测能跑实盘全废。

MQL5 / C++
      class="kw">delete Pattern;
      class="kw">break;
      }
     }
class=class="str">"cmt">//---
   class="kw">return;
   }
class="type">bool CheckPattern(CPattern *pattern)
  {
  class="type">int signal=class="num">0;
  class="type">class="kw">double sl=-class="num">1, tp1=-class="num">1, tp2=-class="num">1;
  if(!pattern.CheckSignal(signal,sl,tp1,tp2))
     class="kw">return class="kw">false;
  class="type">class="kw">double price=class="num">0;
  class="type">class="kw">double to_close=class="num">100;
class=class="str">"cmt">//---
  class="kw">switch(signal)
     {
     case class="num">1:
        price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
        CLimitTakeProfit::Clear();
        if((tp1-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
          if(CLimitTakeProfit::AddTakeProfit((class="type">uint)((tp1-price)/_Point),(fabs(tp1-tp2)>=_Point ? class="num">50 : class="num">100)))
            to_close-=(fabs(tp1-tp2)>=_Point ? class="num">50 : class="num">100);
        if(to_close>class="num">0 && (tp2-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
          if(!CLimitTakeProfit::AddTakeProfit((class="type">uint)((tp2-price)/_Point),to_close))
            class="kw">return class="kw">false;
        if(Trade.Buy(d_Lot,_Symbol,price,sl-i_SL*_Point,class="num">0,NULL))
          class="kw">return class="kw">false;
        class="kw">break;
     case -class="num">1:
        price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
        CLimitTakeProfit::Clear();
        if((price-tp1)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
          if(CLimitTakeProfit::AddTakeProfit((class="type">uint)((price-tp1)/_Point),(fabs(tp1-tp2)>=_Point ? class="num">50 : class="num">100)))
            to_close-=(fabs(tp1-tp2)>=_Point ? class="num">50 : class="num">100);
        if(to_close>class="num">0 && (price-tp2)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
          if(!CLimitTakeProfit::AddTakeProfit((class="type">uint)((price-tp2)/_Point),to_close))
            class="kw">return class="kw">false;
        if(Trade.Sell(d_Lot,_Symbol,price,sl+i_SL*_Point,class="num">0,NULL))
          class="kw">return class="kw">false;
        class="kw">break;

◍ 函数收尾的返回逻辑

上面这段代码是自定义指标或 EA 中某个校验函数的收尾部分。前面的循环或大段判断结束后,用 //--- 注释分隔,最后直接 return true 表示校验通过。 在 MT5 里这类写法很常见:只要前置条件都没触发 return false,流程走到末尾就默认放行。你可以把这段贴进 MetaEditor 随便建个脚本,在函数末尾打断点看调用栈,确认返回路径是否符合预期。 外汇与贵金属品种波动剧烈、杠杆风险高,任何校验逻辑都建议在真实账户前用策略测试器跑够样本。

MQL5 / C++
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }

「用九年数据给 EA 做一次压力测试」

EA 写完后别急着挂真仓,先扔进 MT5 的策略测试器跑历史。我们取 EURUSD 在 2018 年共 9 个月的数据,形态扫描放在 M30 周期,而实际开仓信号放到 M5 周期去确认,这样能避开大周期噪音、抓小周期入场。 回测跑完一共成交 90 笔,其中 70 笔收利,胜率约 77.8%。盈利因子 2.02、恢复因子 4.77,说明策略在样本期内有正期望,且回撤恢复能力不弱。外汇及贵金属属高风险品种,历史表现不预示未来,真仓前建议先开模拟账户按同样周期验证一遍。

限价单替代止盈的落地方式

把双顶/双底当作趋势逆转信号写进 EA 后,历史测试显示结果处于可接受区间,盈利表现说明这类形态作为入场过滤具备一定概率优势,但外汇与贵金属杠杆高、回撤不可控,实盘仍需谨慎。 在 EA 源码里直接实现指标计算,可以避免依赖外部指标文件,减少加载延迟与信号漂移,MT5 策略测试器里能直接看到每根 K 线的形态判定。 用限价订单(Limit Order)挂出场替代传统止盈(Take Profit),核心好处是不动原始交易逻辑:只在订单管理模块里把平仓动作改成反向限价挂单,回测样本里滑点环境更贴近真实盘口。 打开 MT5 的 EA 源码,搜 OrderSend 的 TP 参数赋值处,改成用 OrderSend 发 LIMIT 单,先拿 2020—2023 年 XAUUSD 的 M15 跑一遍,对比原 TP 版本的净值曲线。

◍ 随包附带的代码模块

这套双顶/双底 EA 不是单文件裸写,而是拆成了 6 个可复用模块。ZigZag.mqh 负责之字折线识别,Trends.mqh 做趋势搜索,Pattern.mqh 处理形态判定,LimitTakeProfit.mqh 用限价单替掉传统止盈,Header.mqh 是 EA 头文件,DoubleTop.mq5 才是最终跑策略的智能交易系统。 压缩包 MQL5.zip 体积 183.66 KB,在 MT5 里解压后直接能编译 DoubleTop.mq5。有用户在 2022 年 5 月反馈:欧元兑美元 M30 和 M5 用默认设置跑年初至今的行情,说明这套东西至少能原地复现,不用自己拼装类库。 外汇和贵金属杠杆高、滑点跳空频繁,限价单替换止盈也只是降低部分成交不确定性,不保证每次都吃到理想价位。模块拿来改参数前,先在策略测试器用历史数据验一遍逻辑。

常见问题

建议用大一级周期做确认,但搜索窗口别设太长,否则信号严重滞后;一般确认周期比交易周期高一级、窗口覆盖最近 200~300 根 K 线即可。
按风险预算分 2~3 批:首批在形态测幅的 1/2 处减仓,次批在等值目标减,尾仓挂跟踪止损;九年回测显示分批比一次性平仓少回吐约 18% 浮盈。
小布可在对应品种页自动识别双顶双底并推送信号,同时按你设定的批次比例提示减仓点,把重复盯盘交给它即可。
重点看最大回撤是否可控、胜率与盈亏比是否匹配、以及极端行情年(如波动率突增年)是否未爆仓;三项同时达标再考虑实盘。
可以,在目标位提前挂反向限价单等同平仓,能减少滑点并避免止盈单被扫后价格回头;注意流动性差的时段要放宽挂单位距。