逆转形态:测试头肩形态·进阶篇
📉

逆转形态:测试头肩形态·进阶篇

(2/3)· 为什么你画对头肩却总被假突破洗出场?本文用智能交易系统拆解形态胜率真相

含代码示例偏理论 第 2/3 篇
很多交易者把头肩形态当万能反转信号,忽略颈线斜率和点差对止损的侵蚀。左肩假突破诱多后,右肩未成便提前抄底,账户在震荡里被反复磨损。头肩不是看图说话,而是概率结构。

头肩形态过滤与信号确认的实现细节

这段逻辑先算头肩结构中几个关键距离:头部高度 header 是头部极值与修正终点价差绝对值,反转幅度 revers 是头部极值与右肩起点价差绝对值,右肩长度 r_shoulder 是趋势终点与右肩起点价差绝对值。若修正占趋势比小于 d_MinCorrection,或头部大于趋势减修正,或头反转不对称度(1 - 较小/较大)超 d_MaxCorrection,或右肩相对反转、修正相对头部回撤不足 d_MinCorrection,直接 continue 跳过,说明形态概率上不达标。 确认信号时,先取确认周期下从趋势终点到当前的 K 线,用 iBarShift 定位。若品种未初始化、shift 无效或 CopyRates 条数不足,返回 false,避免外汇贵金属这种高波动品种上误触发。 对于上升头肩(up_trend 为真),若价格刺穿头部极值或 tp1 已设且低点破 tp1,直接否掉。颈线用两点斜率 koef 推算:neck = koef*(shift2-shift-i)+右肩起点价。收盘价未破颈线就 continue;一破就将 break_neck 置真,并算 tp1 = 颈线 - (头-颈线)*0.9、tp2 = 颈线 - (颈线-趋势起点)*0.9,取两者较大值为 tp1。 开 MT5 把 d_MinCorrection、d_MaxCorrection 调成 0.1~0.3 区间跑一遍,能看出不对称过滤对假突破的剔除效果;外汇贵金属杠杆高,形态失效也常见,任何信号都只是概率倾向。

MQL5 / C++
class="type">class="kw">double header=MathAbs(s_HeadExtremum.Price-s_EndCorrection.Price);
class="type">class="kw">double revers=MathAbs(s_HeadExtremum.Price-s_StartRShoulder.Price);
class="type">class="kw">double r_shoulder=MathAbs(s_EndTrend.Price-s_StartRShoulder.Price);
if((correction/trend)<d_MinCorrection || header>(trend-correction)  ||
   (class="num">1-fmin(header,revers)/fmax(header,revers))>=d_MaxCorrection      ||
   (class="num">1-r_shoulder/revers)<d_MinCorrection || (class="num">1-correction/header)<d_MinCorrection)
   class="kw">continue;
b_found= true;
class=class="str">"cmt">//---
   class="kw">break;
   }
class=class="str">"cmt">//---
  class="kw">return b_found;
  }
class="type">bool CHS_Pattern::CheckSignal(class="type">int &signal, class="type">class="kw">double &sl, class="type">class="kw">double &tp1, class="type">class="kw">double &tp2)
  {
  if(!b_found)
    class="kw">return class="kw">false;
  class="type">class="kw">string symbol=C_Trends.Symbol();
  if(symbol=="Not Initilized")
    class="kw">return class="kw">false;
  class="type">class="kw">datetime start_time=s_EndTrend.TimeStartBar+PeriodSeconds(C_Trends.Timeframe());
  class="type">int shift=iBarShift(symbol,e_ConfirmationTF,start_time);
  if(shift<class="num">0)
    class="kw">return class="kw">false;
  class="type">MqlRates rates[];
  class="type">int total=CopyRates(symbol,e_ConfirmationTF,class="num">0,shift+class="num">1,rates);
  if(total<=class="num">0)
    class="kw">return class="kw">false;
class=class="str">"cmt">//---
  signal=class="num">0;
  sl=tp1=tp2=-class="num">1;
  class="type">bool up_trend=C_Trends.IsHigh(s_EndTrend);
  class="type">int shift1=iBarShift(symbol,e_ConfirmationTF,s_EndCorrection.TimeStartBar,true);
  class="type">int shift2=iBarShift(symbol,e_ConfirmationTF,s_StartRShoulder.TimeStartBar,true);
  if(shift1<=class="num">0 || shift2<=class="num">0)
    class="kw">return class="kw">false;
  class="type">class="kw">double koef=(s_StartRShoulder.Price-s_EndCorrection.Price)/(shift1-shift2);
  class="type">bool break_neck=class="kw">false;
  for(class="type">int i=class="num">0;i<total;i++)
    {
     if(up_trend)
       {
        if((tp1>class="num">0 && rates[i].low<=tp1) || rates[i].high>s_HeadExtremum.Price)
          class="kw">return class="kw">false;
        class="type">class="kw">double neck=koef*(shift2-shift-i)+s_StartRShoulder.Price;
        if(!break_neck)
          {
           if(rates[i].close>neck)
             class="kw">continue;
           break_neck=true;
           tp1=neck-(s_HeadExtremum.Price-neck)*class="num">0.9;
           tp2=neck-(neck-s_StartTrend.Price)*class="num">0.9;
           tp1=fmax(tp1,tp2);
           class="kw">continue;
          }

「颈线突破后的止损与信号锁定」

头肩形态判定里,颈线(neck)不是画完就完事。价格收破颈线之前,算法会持续跟踪极值来动态垫高或压低止损位:向上破位时用 fmax 保留最高价作 sl,向下破位时用 fmin 保留最低价作 sl,避免止损被毛刺洗掉。 一旦收盘价站上颈线且 sl 已初始化,循环 continue 继续向后看;但如果剩余未处理 K 线数 (total-i) 大于 2 根仍未确认,直接 return false 丢弃该形态——这意味着至少留 2 根缓冲来确认有效性。 向下破位分支中,tp1 先按 (颈线-头部极值)*0.9 估算,再与 (颈线-趋势起点)*0.9 取较小值,fmin 保证目标不过分乐观。若某根 high 摸到 tp1 或 low 跌破头部极值,形态直接判废 return false。 颈线首次被 close 跌破才置 break_neck=true 并算 tp,之后若 low 回插颈线下方则继续刷新 sl。最终 signal 赋 1 或 -1 并 break,函数返回 true 表示形态可用。外汇与贵金属波动剧烈,这类形态失效概率不低,实盘前务必在 MT5 用历史数据跑通该逻辑。

MQL5 / C++
   if(rates[i].high>neck)
      {
         if(sl==-class="num">1)
            sl=rates[i].high;
         else
            sl=fmax(sl,rates[i].high);
      }
      if(rates[i].close>neck || sl==-class="num">1)
         class="kw">continue;
      if((total-i)>class="num">2)
         class="kw">return class="kw">false;
class=class="str">"cmt">//---
      signal=-class="num">1;
      class="kw">break;
      }
   else
      {
      if((tp1>class="num">0 && rates[i].high>=tp1) || rates[i].low<s_HeadExtremum.Price)
         class="kw">return class="kw">false;
      class="type">class="kw">double neck=koef*(shift2-shift-i)+s_StartRShoulder.Price;
      if(!break_neck)
         {
         if(rates[i].close<neck)
            class="kw">continue;
         break_neck=true;
         tp1=neck+(neck-s_HeadExtremum.Price)*class="num">0.9;
         tp2=neck+(s_StartTrend.Price-neck)*class="num">0.9;
         tp1=fmin(tp1,tp2);
         class="kw">continue;
         }
      if(rates[i].low<neck)
         {
         if(sl==-class="num">1)
            sl=rates[i].low;
         else
            sl=fmin(sl,rates[i].low);
         }
      if(rates[i].close<neck || sl==-class="num">1)
         class="kw">continue;
      if((total-i)>class="num">2)
         class="kw">return class="kw">false;
class=class="str">"cmt">//---
      signal=class="num">1;
      class="kw">break;
      }
   }
class=class="str">"cmt">//---
  class="kw">return true;
  }

◍ 把两套逆转形态塞进一个EA

两个独立的逆转形态EA若各跑各的,资金占用和切换成本都不划算。既然搜索类本来就是继承关系,合并成单一程序的工作量很小:给基类加Type虚方法,CPattern返回101、CHS_Pattern返回102,靠数值区分形态类型;形态多了再换枚举,可读性更好。 类比较用Compare方法按Type和三段趋势/修正的起始时间逐级比对,返回-1/0/1。EA侧只需补一个NewClass函数,用switch按type生成对应实例,OnTick里循环pat从0到1依次调NewClass并跑历史搜索,入场模块因继承结构无需改动。 合并后用相同参数回测,128笔交易里约60%盈利,盈利因子1.94、恢复因子3.85,说明两策略在样本期内倾向互补而非互斥。外汇与贵金属杠杆高,这类历史表现只代表过去概率,实盘前务必在MT5用TwoPatterns.mq5自行验证。 别把数值常量当长久方案 形态只有两种时用101/102硬编码没问题,一旦要加第三、第四种,switch和Compare里的魔法数字会迅速失控,早点改枚举省得后期重构。

MQL5 / C++
class="type">int CPattern::Compare(class="kw">const CPattern *node,class="kw">const class="type">int mode=class="num">0) class="kw">const
  {
   if(Type()>node.Type())
      class="kw">return -class="num">1;
   else
      if(Type()<node.Type())
         class="kw">return class="num">1;
class=class="str">"cmt">//---
   if(s_StartTrend.TimeStartBar>node.StartTrend().TimeStartBar)
      class="kw">return -class="num">1;
   else
      if(s_StartTrend.TimeStartBar<node.StartTrend().TimeStartBar)
         class="kw">return class="num">1;
class=class="str">"cmt">//---
   if(s_StartCorrection.TimeStartBar>node.StartCorrection().TimeStartBar)
      class="kw">return -class="num">1;
   else
      if(s_StartCorrection.TimeStartBar<node.StartCorrection().TimeStartBar)
         class="kw">return class="num">1;
class=class="str">"cmt">//---
   if(s_EndCorrection.TimeStartBar>node.EndCorrection().TimeStartBar)
      class="kw">return -class="num">1;
   else
      if(s_EndCorrection.TimeStartBar<node.EndCorrection().TimeStartBar)
         class="kw">return class="num">1;
class=class="str">"cmt">//---
   class="kw">return class="num">0;
  }
CPattern *NewClass(class="type">int type)
  {
   class="kw">switch(type)
     {
      case class="num">0:
         class="kw">return new CPattern();
         class="kw">break;
      case class="num">1:
         class="kw">return new CHS_Pattern();
         class="kw">break;
     }
class=class="str">"cmt">//---
   class="kw">return NULL;
  }
class="type">void OnTick()
  {
class=class="str">"cmt">//---
.........................
.........................
.........................
class=class="str">"cmt">//---
   for(class="type">int pat=class="num">0;pat<class="num">2;pat++)
     {
      Pattern=NewClass(pat);
      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">continue;
        }
class=class="str">"cmt">//---
      class="type">class="kw">datetime ss=start_search;
      class="kw">while(!IsStopped() && Pattern.Search(ss))
        {
         ss=fmax(ss,Pattern.EndTrendTime()+PeriodSeconds(e_TimeFrame));
         class="type">bool found=class="kw">false;

图形对象数组的去重与构建循环

这段循环是形态识别里最容易被忽略的脏活:它从索引 2 开始遍历已识别对象数组,跳过前两个基准元素,逐个用 Compare 方法比对当前 Pattern 是否与已有对象重复。 for(int i=2;i<ar_Objects.Total();i++) { CPattern *temp=ar_Objects.At(i); if(Pattern.Compare(temp,0)==0) { found=true; break; } } 若 found 为 true 直接 continue,避免把相同形态重复塞进数组;随后连续用 CheckPattern 验证形态有效性、用 ar_Objects.Add 尝试加入,任何一步失败都跳过本次迭代。 Pattern=NewClass(pat); if(CheckPointer(Pattern)==POINTER_INVALID) break; 新建形态实例后立刻用 CheckPointer 判空,指针无效就 break 退出,防止后续对野指针调用 Create。Create 失败时 delete 并 break,函数末尾再对残留 Pattern 做一次判空释放。 在 MT5 里跑这类逻辑时,建议把 ar_Objects.Total() 在循环外缓存成变量,数组频繁增减时能少几次方法调用开销。外汇与贵金属市场高杠杆、价格跳空频繁,这类对象管理代码若漏掉指针释放,实盘可能跑几小时就内存泄漏崩 EA。

MQL5 / C++
for(class="type">int i=class="num">2;i<ar_Objects.Total();i++)
  {
  CPattern *temp=ar_Objects.At(i);
  if(Pattern.Compare(temp,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=NewClass(pat);
if(CheckPointer(Pattern)==POINTER_INVALID)
  class="kw">break;
if(!Pattern.Create(ar_Objects.At(class="num">1),d_MinCorrection,d_MaxCorrection))
  {
  class="kw">delete Pattern;
  class="kw">break;
  }
}
if(CheckPointer(Pattern)!=POINTER_INVALID)
  class="kw">delete Pattern;
}
class=class="str">"cmt">//---
 class="kw">return;
}

「同一套参数在不同品种上裂成两半」

把 EA 原样跑在 6 个主要货币对上、测试参数一字未改,结果直接劈成两半:EURUSD 盈利 743.57、盈利因子 1.94,EURJPY 盈利 125.13、因子 1.47,GBPJPY 微利 33.93、因子 1.04;而 EURGBP 亏 191.7、因子 0.82,GBPUSD 亏 371.05、因子 0.60,USDJPY 亏 657.38、因子仅 0.31。一半赚一半亏,说明价格行为在不同品种上根本不是同一套分布。 外汇和贵金属都是高杠杆高风险品种,跨品种直接套用同一 EA 参数大概率会吃掉你一半的账户。上面这张表本身就是证据:不优化就上实盘,亏损那边的因子已经掉到 0.3 附近。 只对止损回退做针对性优化,局面就变了。EURUSD 利润抬到 1020.28(回退 350),EURJPY 到 532.54(400),GBPJPY 到 208.69(300),EURGBP 翻正到 91.45(450),GBPUSD 和 USDJPY 亏损也收窄到 -315.87 和 -453.08(均 100)。整体篮子的盈利能力倾向改善,但 USDJPY 因子仍只有 0.33,远没到安全区。 别把正态当圣经 品种间相关性会骗人,GBPUSD 与 EURUSD 平时同涨同跌,可同一 EA 一个因子 1.94、一个 0.60。开 MT5 把这两对拉进同一次测试,逐项改止损回退,你会看到参数敏感度完全不对称。

把回测劳动交给小布
这些形态诊断与小布盯盘的 AIGC 已内置,打开对应品种页即可看到颈线角度与假突破密度,你只管判断趋势背景。

常见问题

左倾颈线反映下跌过程中低点逐级下移,空头控盘更坚决,突破后延续概率倾向更高,但仍受宏观流动性扰动。
回滚入场止损更贴近极值,同等风险下仓位可放大,潜在盈亏比可能优于突破追单,但会跳过不回踩的形态。
点差在新闻期扩大、流动性断层会造成颈线误触,贵金属受避险情绪跳空,高风险品种需降低杠杆。
可以,小布在价格行为面板输出形态候选与颈线斜度,并联动双顶系列一并提示,省去手动画线。
按时间帧优先级过滤,同根K线只放行强形态信号,并用独立魔法码隔离订单,回测更干净。