形态与示例(第一部分):多顶·综合运用
📐

形态与示例(第一部分):多顶·综合运用

(3/3)·当视觉形态遇上 MQL5 代码,双顶只是起点,多顶与头肩的混合识别才是算法交易的练手场

偏理论进阶 第 3/3 篇
把双顶当万能反转信号的人,往往忽略它在震荡市里假突破的概率偏高。多数交易者凭肉眼数顶,却说不清代码该如何界定“顶”和“无效形态”。用统计而非直觉校验形态,才是算法交易该走的路。

◍ 极值提取与形态方向的代码骨架

在 MT5 里做双顶双底识别,第一步是把两段索引之间的真实 High/Low 捞出来。CalculateMaximum 从 Index0 扫到 Index1,只要 High[i] 大于暂存值就刷新价格、时间和位置,最终写回 Top 结构的 Price 与 IndexExtremum。CalculateMinimum 逻辑对称,只是把比较符号翻成小于号找最低点。 PrepareExtremums 不干复杂活,直接把 TopsUpAll / TopsDownAll 的前 TopsM 个极值拷贝到工作数组,等于只保留离盘面最近的那几根。若 TopsM 设成 5,就只处理最近 5 个峰谷,老数据不进计算。 DirectionOfFormation 靠时间先后定方向:当下行极值时间晚于上行极值且末端谷仍激活,判为双底(FormationDirection=-1);反过来是双顶(=1)。两者都不满足就置 0,说明峰谷没凑齐,后续不宜硬判。 外汇与贵金属波动大,这类极值识别只解决“形状在不在”,不预示突破概率,实盘须叠加成交量或均线过滤。

MQL5 / C++
class="type">void ExtremumsPatternFamilySearcher::CalculateMaximum(Top &T,class="type">int Index0,class="type">int Index1)class=class="str">"cmt">// if class="num">2 intermediate points are found, find High between them
  {
  class="type">class="kw">double MaxValue=High[Index0];
  class="type">class="kw">datetime MaxTime=Time[Index0];
  class="type">int MaxIndex=Index0;
  for(class="type">int i=Index0;i<=Index1;i++)
    {
    if ( High[i] > MaxValue )
      {
      MaxValue=High[i];
      MaxTime=Time[i];
      MaxIndex=i;
      }
    }
  T.DatetimeExtremum=MaxTime;
  T.IndexExtremum=MaxIndex;
  T.Price=MaxValue;
  }

class="type">void ExtremumsPatternFamilySearcher::CalculateMinimum(Top &T,class="type">int Index0,class="type">int Index1)class=class="str">"cmt">//if class="num">2 intermediate points are found, find Low between them
  {
  class="type">class="kw">double MinValue=Low[Index0];
  class="type">class="kw">datetime MinTime=Time[Index0];
  class="type">int MinIndex=Index0;
  for(class="type">int i=Index0;i<=Index1;i++)
    {
    if ( Low[i] < MinValue )
      {
      MinValue=Low[i];
      MinTime=Time[i];
      MinIndex=i;
      }
    }
  T.DatetimeExtremum=MinTime;
  T.IndexExtremum=MinIndex;
  T.Price=MinValue;    
  }
class="type">bool ExtremumsPatternFamilySearcher::PrepareExtremums()class=class="str">"cmt">// assign the tops with which we will work
  {
  class="type">int Quantity;class=class="str">"cmt">// an auxiliary counter for random tops
  class="type">int PrevIndex;class=class="str">"cmt">// an auxiliary index for maintaining the order of indexes(increment only)

  for(class="type">int i=class="num">0;i<TopsM;i++)class=class="str">"cmt">// simply select the tops that are closest to the market
    {
    TopsUp[i]=TopsUpAll[i];
    TopsDown[i]=TopsDownAll[i];
    }
  class="kw">return true;  
  }
class="type">void ExtremumsPatternFamilySearcher::DirectionOfFormation()class=class="str">"cmt">// determine whether it is a class="type">class="kw">double top(class="num">1) or class="type">class="kw">double bottom(-class="num">1) (only if all tops and bottoms are found - if not found, then class="num">0)
  {
  if ( TopsDown[class="num">0].DatetimeExtremum > TopsUp[class="num">0].DatetimeExtremum && TopsDown[ArraySize(TopsDown)-class="num">1].bActive )
    {
    StartTop=TopsDown[ArraySize(TopsDown)-class="num">1];
    EndTop=TopsDown[class="num">0];    
    FormationDirection=-class="num">1;
    }
  else if ( TopsDown[class="num">0].DatetimeExtremum < TopsUp[class="num">0].DatetimeExtremum && TopsUp[ArraySize(TopsUp)-class="num">1].bActive )
    {
    StartTop=TopsUp[ArraySize(TopsUp)-class="num">1];
    EndTop=TopsUp[class="num">0];
    FormationDirection=class="num">1;  
    }
  else FormationDirection=class="num">0;  
  }
class="type">bool ExtremumsPatternFamilySearcher::IsExtremumsAbsolutely()class=class="str">"cmt">// require the selected extrema to be the most extreme ones
  {
  if ( bRandomExtremumsM )class=class="str">"cmt">// check only if we have a random selection of tops(in other case the check should be considered completed)
    {
    if ( FormationDirection == class="num">1 )
      {

双方向极值间的颈线过滤逻辑

这段逻辑在形态识别里干一件事:给定随机抽出的上/下极值索引数组,校验两个选定极值之间是否还存在「更高(或更低)的毛刺」,从而判定该组极值能否构成经典多重顶/底的候选。 向上方向(FormationDirection==1)时,先取 RandomIndexUp[0] 为起点、数组末位为终点,遍历中间每根 K 线 i。若某根 i 的价格不低于已知 TopsUp 中任意顶,就再去比对 i 是否本就属于随机选中的极值集合;只要有一个 i 不在集合内,直接 return false,说明中间混入了非选定但更高的顶,形态不成立。 向下方向镜像处理:用 RandomIndexDown 框定区间,若中间 K 线价格不高于 TopsDown 中任意底且不在随机索引内,同样 return false。两种方向都通过才 return true;方向参数异常则返回 false。外汇与贵金属杠杆高,这类形态过滤只能降低假信号概率,不能排除突破失效风险。 后面 FindNeckUp 接收两个 Top 引用,初始化颈线最低价为起点极值处的 Low、时间为对应 Time,这是后续连线的基准点,开 MT5 把这两行打日志就能看到颈线锚定位置。

MQL5 / C++
class="type">int StartIndex=RandomIndexUp[class="num">0];
class="type">int EndIndex=RandomIndexUp[ArraySize(RandomIndexUp)-class="num">1];
for(class="type">int i=StartIndex+class="num">1;i<EndIndex;i++)class=class="str">"cmt">// check all tops between the selected ones
  {
  for(class="type">int j=class="num">0;j<ArraySize(TopsUp);j++)
    {
    if ( TopsUpAll[i].Price >= TopsUp[j].Price )
      {
      for(class="type">int k=class="num">0;k<ArraySize(RandomIndexUp);k++)
        {
        if ( i != RandomIndexUp[k] ) class="kw">return class="kw">false;
        }
      }
    }
  }
class="kw">return true;
  }
else if ( FormationDirection == -class="num">1 )
  {
  class="type">int StartIndex=RandomIndexDown[class="num">0];
  class="type">int EndIndex=RandomIndexDown[ArraySize(RandomIndexDown)-class="num">1];
  for(class="type">int i=StartIndex+class="num">1;i<EndIndex;i++)class=class="str">"cmt">// check all tops between the selected ones
    {
    for(class="type">int j=class="num">0;j<ArraySize(TopsDown);j++)
      {
      if ( TopsDownAll[i].Price <= TopsDown[j].Price )
        {
        for(class="type">int k=class="num">0;k<ArraySize(RandomIndexDown);k++)
          {
          if ( i != RandomIndexDown[k] ) class="kw">return class="kw">false;
          }
        }
      }
    }
  class="kw">return true;    
  }
else class="kw">return class="kw">false;    
  }
else
  {
  class="kw">return true;
  }
}
class="type">void ExtremumsPatternFamilySearcher::FindNeckUp(Top &TStart,Top &TEnd)class=class="str">"cmt">// find the neck line based on the two extreme tops(for the classic multiple top)
  {
  class="type">class="kw">double PriceMin=Low[TStart.IndexExtremum];
  class="type">class="kw">datetime TimeMin=Time[TStart.IndexExtremum];

「颈线锚点与最远极值的定位逻辑」

多重顶/底形态里,颈线不是随手画的水平线,而是用两个极值拐点反推出来的。向上找颈线时,代码从起始顶的 K 线索引扫到结束顶的索引,逐根比对 Low 取最小价作为 PriceMin,同时记录 TimeMin;向下找颈线则比对 High 取最大价。 拿到锚点后,Neck 结构体会把 Price0、TimeX、Time0、Price1、Time1 全部赋值,再调 CalculateKC() 算直线方程。DirectionOfFormation 用 true/false 区分形态方向,这步直接决定后面过滤条件怎么走。 SearchFarestTop() 负责从已识别的极值数组里挑「离颈线最远」的那个顶或底。多重顶下用 TopsUp[i].Price - Neck.Price0 做距离判据,多重底下用 Neck.Price0 - TopsDown[i].Price。注意原代码在多重底分支里,更新 MaxTranslation 时误写了 TopsDown[0] 而非 TopsDown[i],这会导致最远底判定可能偏向首元素,开 MT5 跑回测时建议先修掉这行再验证。 外汇与贵金属杠杆高、波动突变频繁,这类形态识别只是概率辅助,实盘须配合止损。把上面逻辑复制到自己的 EA 里,改 FormationDirection 参数就能切换多顶/多底扫描。

MQL5 / C++
for(class="type">int i=TStart.IndexExtremum;i>=TEnd.IndexExtremum;i--)class=class="str">"cmt">// define the lowest point
    {
    if ( Low[i] < PriceMin )
      {
      PriceMin=Low[i];
      TimeMin=Time[i];
      }
    }
  class=class="str">"cmt">// define the parameters of the anchor point and all parameters of the line equation
  Neck.Price0=PriceMin;
  Neck.TimeX=TimeMin;
  Neck.Time0=Time[class="num">0];
  Neck.Price1=PriceMin;
  Neck.Time1=TStart.DatetimeExtremum;
  Neck.DirectionOfFormation=true;
  Neck.CalculateKC();
  }

class="type">void ExtremumsPatternFamilySearcher::FindNeckDown(Top &TStart,Top &TEnd)class=class="str">"cmt">// find the neck line based on two extreme bottoms(for the classic multiple bottom)
  {
  class="type">class="kw">double PriceMax=High[TStart.IndexExtremum];
  class="type">class="kw">datetime TimeMax=Time[TStart.IndexExtremum];
  for(class="type">int i=TStart.IndexExtremum;i>=TEnd.IndexExtremum;i--)class=class="str">"cmt">// define the lowest point
    {
    if ( High[i] > PriceMax )
      {
      PriceMax=High[i];
      TimeMax=Time[i];
      }
    }
  class=class="str">"cmt">// define the parameters of the anchor point and all parameters of the line equation
  Neck.Price0=PriceMax;
  Neck.TimeX=TimeMax;
  Neck.Time0=Time[class="num">0];
  Neck.Price1=PriceMax;
  Neck.Time1=TStart.DatetimeExtremum;
  Neck.DirectionOfFormation=class="kw">false;
  Neck.CalculateKC();
  }
class="type">void ExtremumsPatternFamilySearcher::SearchFarestTop()class=class="str">"cmt">// define the farthest top
  {
  class="type">class="kw">double MaxTranslation;class=class="str">"cmt">// temporary variable to determine the highest top
  if ( FormationDirection == class="num">1 )class=class="str">"cmt">// if we deal with a multiple top
    {
    MaxTranslation=TopsUp[class="num">0].Price-Neck.Price0;class=class="str">"cmt">// temporary variable to determine the highest top
    FarestTop=TopsUp[class="num">0];
    for(class="type">int i=class="num">1;i<ArraySize(TopsUp);i++)
      {
      if ( TopsUp[i].Price-Neck.Price0 > MaxTranslation )
        {
        MaxTranslation=TopsUp[i].Price-Neck.Price0;
        FarestTop=TopsUp[i];
        }
      }
    }
  if ( FormationDirection == -class="num">1 )class=class="str">"cmt">// if we deal with a multiple bottom
    {
    MaxTranslation=Neck.Price0-TopsDown[class="num">0].Price;class=class="str">"cmt">// temporary variable to determine the lowest bottom
    FarestTop=TopsDown[class="num">0];
    for(class="type">int i=class="num">1;i<ArraySize(TopsDown);i++)
      {
      if ( Neck.Price0-TopsDown[i].Price > MaxTranslation )
        {
        MaxTranslation=Neck.Price0-TopsDown[class="num">0].Price;
        FarestTop=TopsDown[i];
        }
      }
    }
  }
class="type">bool ExtremumsPatternFamilySearcher::bBalancedExtremums()class=class="str">"cmt">// balance the tops
  {
  class="type">class="kw">double Lowest;class=class="str">"cmt">// the lowest top for the multiple top

◍ 头肩形态的高度与时间对称性过滤

在 MT5 里自动识别多重顶/底(头肩类)形态时,光有极值点还不够,必须卡掉「头远大于肩」和「波宽失衡」的畸形结构。下面这段逻辑用两个阈值变量 RelativeUnstabilityM 与 RelativeUnstabilityTimeM 做硬性过滤,不达标直接 return false,形态判定作废。 对于多重顶(FormationDirection==1),先扫 TopsUp 数组找出最低的那个峰位 Lowest,算它到颈线 Neck.Price0 的垂直距离 AbsMin;若 AbsMin 为 0 说明贴着颈线,无意义。接着用 (最远峰到颈线距离 - AbsMin)/AbsMin 与 RelativeUnstabilityM 比较,比值超标就认为头部相对最小杠杆过大,形态不可信。多重底镜像处理,用 Highest 到颈线的距离做同样判定。 时间轴上,bBalancedExtremumsTime() 专门看相邻极值点的 IndexExtremum 间距。以多重顶为例,遍历 TopsUp 算每两段波宽,取最低 Lowest 与最高 Highest;若 (Highest-Lowest)/Lowest 大于 RelativeUnstabilityTimeM,说明某条腿过宽,水平方向不平衡,也一票否决。外汇与贵金属波动跳变频繁,这类过滤能显著降低假信号,但形态失败概率仍高,仅作辅助。 把 RelativeUnstabilityM 设成 0.5、RelativeUnstabilityTimeM 设成 1.0 这类经验值,挂上 EURUSD 的 M15 历史数据回测,能直观看到多少候选形态被砍掉。

MQL5 / C++
class="type">class="kw">double Highest;class=class="str">"cmt">// the highest bottom for the multiple bottom
class="type">class="kw">double AbsMin;class=class="str">"cmt">// distance from the neck to the nearest top
if ( FormationDirection == class="num">1 )class=class="str">"cmt">// for the multiple top
    {
    Lowest=TopsUp[class="num">0].Price;
    for(class="type">int i=class="num">1;i<ArraySize(TopsUp);i++)class=class="str">"cmt">// find the lowest top
      {
      if ( TopsUp[i].Price < Lowest ) Lowest=TopsUp[i].Price;
      }
    AbsMin=Lowest-Neck.Price0;class=class="str">"cmt">// determine distance from the lowest top to the neck
    if ( AbsMin == class="num">0.0 ) class="kw">return class="kw">false;
    if ( ((FarestTop.Price - Neck.Price0)-AbsMin)/AbsMin >= RelativeUnstabilityM ) class="kw">return class="kw">false;class=class="str">"cmt">// if the head is too much bigger than the lowest leverage
    }
  else if ( FormationDirection == -class="num">1 )class=class="str">"cmt">// for the multiple bottom
    {
    Highest=TopsDown[class="num">0].Price;
    for(class="type">int i=class="num">1;i<ArraySize(TopsDown);i++)class=class="str">"cmt">// find the highest top
      {
      if ( TopsDown[i].Price > Highest ) Highest=TopsDown[i].Price;
      }
    AbsMin=Neck.Price0-Highest;class=class="str">"cmt">// determine distance from the highest top to the neck
    if ( AbsMin == class="num">0.0 ) class="kw">return class="kw">false;
    if ( ((Neck.Price0-FarestTop.Price)-AbsMin)/AbsMin >= RelativeUnstabilityM ) class="kw">return class="kw">false;class=class="str">"cmt">// if the head is too much bigger than the lowest leverage
    }
  else class="kw">return class="kw">false;
  class="kw">return true;  
  }
class="type">bool ExtremumsPatternFamilySearcher::bBalancedExtremumsTime()class=class="str">"cmt">// balance the sizes of shoulders and head along the horizontal axis
  {
  class="type">class="kw">double Lowest;class=class="str">"cmt">// minimum distance between the tops
  class="type">class="kw">double Highest;class=class="str">"cmt">// maximum distance between the tops
  if ( FormationDirection == class="num">1 )class=class="str">"cmt">// for the multiple top
    {
    Lowest=TopsUp[class="num">1].IndexExtremum-TopsUp[class="num">0].IndexExtremum;
    Highest=TopsUp[class="num">1].IndexExtremum-TopsUp[class="num">0].IndexExtremum;
    for(class="type">int i=class="num">1;i<ArraySize(TopsUp)-class="num">1;i++)class=class="str">"cmt">// find the lowest top
      {
      if ( TopsUp[i+class="num">1].IndexExtremum-TopsUp[i].IndexExtremum < Lowest ) Lowest=TopsUp[i+class="num">1].IndexExtremum-TopsUp[i].IndexExtremum;
      if ( TopsUp[i+class="num">1].IndexExtremum-TopsUp[i].IndexExtremum > Highest ) Highest=TopsUp[i+class="num">1].IndexExtremum-TopsUp[i].IndexExtremum;
      }
    if ( class="type">class="kw">double(Highest-Lowest)/class="type">class="kw">double(Lowest) > RelativeUnstabilityTimeM ) class="kw">return class="kw">false;class=class="str">"cmt">// if the width of one of the waves differs much
    }
  else if ( FormationDirection == -class="num">1 )class=class="str">"cmt">// for the multiple bottom
    {

双顶颈线左侧交截的过滤逻辑

这段 MT5 代码在做一件事:先算出双顶(或双底)相邻波峰之间的 K 线跨度极值,再拿相对不稳定阈值去卡掉变形太夸张的形态。Lowest 和 Highest 初值取最近两个顶的索引差,循环里不断刷新最小/最大跨度;若 (Highest-Lowest)/Lowest 超过 RelativeUnstabilityTimeM,直接 return false,说明某段波动宽度偏离太大,形态可信度倾向偏低。 CorrectNeckUpLeft 针对双顶:从 StartTop.Index1 往右扫到 BarsM,若某根 High 刺穿 FarestTop.Price 就判假突破返 false;只有 Close、Open、High、Low 四价同时低于 Neck.Price0,才把 Neck.Time1/Index1 钉在该根并返 true。双底镜像由 CorrectNeckDownLeft 处理,条件全部取反。 实盘里把 RelativeUnstabilityTimeM 设 0.5 意味着允许波宽差最多 50%,设 0.2 则只容 20% 偏差,过滤更严但可能漏掉真实宽幅双顶。外汇与贵金属杠杆高,这类形态识别只是概率辅助,进场前仍需看更大周期确认。

MQL5 / C++
   Lowest=TopsDown[class="num">1].IndexExtremum-TopsDown[class="num">0].IndexExtremum;
   Highest=TopsDown[class="num">1].IndexExtremum-TopsDown[class="num">0].IndexExtremum;
   for(class="type">int i=class="num">1;i<ArraySize(TopsDown)-class="num">1;i++)class=class="str">"cmt">// find the lowest top
     {
     if ( TopsDown[i+class="num">1].IndexExtremum-TopsDown[i].IndexExtremum < Lowest ) Lowest=TopsDown[i+class="num">1].IndexExtremum-TopsDown[i].IndexExtremum;
     if ( TopsDown[i+class="num">1].IndexExtremum-TopsDown[i].IndexExtremum > Highest ) Highest=TopsDown[i+class="num">1].IndexExtremum-TopsDown[i].IndexExtremum;
     }
   if ( class="type">class="kw">double(Highest-Lowest)/class="type">class="kw">double(Lowest) > RelativeUnstabilityTimeM ) class="kw">return class="kw">false;class=class="str">"cmt">// if the width of one of the waves differs much 
   }
  else class="kw">return class="kw">false;
  class="kw">return true;
  }
class="type">bool ExtremumsPatternFamilySearcher::CorrectNeckUpLeft()class=class="str">"cmt">// next the neck line must be corrected so that it finds an intersection with the price on the left
  {
  class="type">bool bCrossNeck=class="kw">false;class=class="str">"cmt">// indicates if the neck was crossed
  if ( Neck.DirectionOfFormation )class=class="str">"cmt">// if the neck is found for a class="type">class="kw">double top
   {
   for(class="type">int i=StartTop.Index1;i<BarsM;i++)class=class="str">"cmt">// define the intersection point
     {
     if ( High[i] >= FarestTop.Price )class=class="str">"cmt">// if the movement goes beyond the formation, then the formation is fake
       {
       class="kw">return class="kw">false;
       }       
     if ( Close[i] < Neck.Price0 && Open[i] < Neck.Price0 && High[i] < Neck.Price0 && Low[i] < Neck.Price0  )
       {
       Neck.Time1=Time[i];
       Neck.Index1=i;
       class="kw">return true;
       }
     }
   }
  class="kw">return class="kw">false;
  }
  
class="type">bool ExtremumsPatternFamilySearcher::CorrectNeckDownLeft()class=class="str">"cmt">// next the neck line must be corrected so that it finds an intersection with the price on the left
  {
  class="type">bool bCrossNeck=class="kw">false;class=class="str">"cmt">// indicates if the neck was crossed
  if ( !Neck.DirectionOfFormation )class=class="str">"cmt">// if the neck is found for a class="type">class="kw">double bottom
   {
   for(class="type">int i=StartTop.Index1;i<BarsM;i++)class=class="str">"cmt">// define the intersection point
     {
     if ( Low[i] <= FarestTop.Price )class=class="str">"cmt">//  if the movement goes beyond the formation, then the formation is fake
       {
       class="kw">return class="kw">false;
       }       
     if ( Close[i] > Neck.Price0 && Open[i] > Neck.Price0 && High[i] > Neck.Price0 && Low[i] > Neck.Price0 )
       {
       Neck.Time1=Time[i];
       Neck.Index1=i;

「颈线右侧交叉的修正与假突破判定」

双顶结构里,颈线找到后还得向右修正,确认价格和颈线在右侧有交点。代码用 CorrectNeckUpRight() 处理双顶:从 EndTop 极点索引向左扫到 i>1,若中途 High[i] 超过 FarestTop.Price 或 Low[i] 跌破 Neck.Price0,直接 return -1,说明形态失效概率高。 若最新收盘 Close[0] 已落在 Neck.Price0 下方,就把 Neck.Time0 设为当前时间并返回 1,代表右交点已成立。否则返回 0,等待后续 K 线推进。 双底镜像逻辑在 CorrectNeckDownRight():只有当 !Neck.DirectionOfFormation 才进入,循环里判 Low[i] < FarestTop.Price 或 High[i] > Neck.Price0 即 return -1。Close[0] >= Neck.Price0 时记录 Time0 并返回 1。 bWasTrend() 负责确认形态前驱趋势:FormationDirection==1 时从 Neck.Index1 向右到 BarsM,只要 High[i] > Neck.Price0 就 return false,意味着价格已刺穿颈线、双顶假设作废。外汇与贵金属波动大,这类刺穿在重大数据行情中常出现假信号,建议用 MT5 历史回放逐根验证上述 return 分支。

MQL5 / C++
class="type">int ExtremumsPatternFamilySearcher::CorrectNeckUpRight()class=class="str">"cmt">// next the neck line must be corrected so that it finds an intersection with the price on the right
  {
  class="type">bool bCrossNeck=class="kw">false;class=class="str">"cmt">// indicates if the neck was crossed
  if ( Neck.DirectionOfFormation )class=class="str">"cmt">// if the neck is found for a class="type">class="kw">double top
    {
    for(class="type">int i=EndTop.IndexExtremum;i>class="num">1;i--)class=class="str">"cmt">// define the intersection point
      {
      if ( High[i] > FarestTop.Price || Low[i] < Neck.Price0 )class=class="str">"cmt">// if the movement goes beyond the formation, then the formation is fake
        {
        class="kw">return -class="num">1;
        }       
      }
    }
    
  if ( Close[class="num">0] <= Neck.Price0 )
    {
    Neck.Time0=Time[class="num">0];
    class="kw">return class="num">1;
    }     
  class="kw">return class="num">0;
  }
class="type">int ExtremumsPatternFamilySearcher::CorrectNeckDownRight()class=class="str">"cmt">// next the neck line must be corrected so that it finds an intersection with the price on the right
  {
  class="type">bool bCrossNeck=class="kw">false;class=class="str">"cmt">// indicates if the neck was crossed
  if ( !Neck.DirectionOfFormation )class=class="str">"cmt">// if the neck is found for a class="type">class="kw">double bottom
    {
    for(class="type">int i=EndTop.IndexExtremum;i>class="num">1;i--)class=class="str">"cmt">// define the intersection point
      {
      if ( Low[i] < FarestTop.Price || High[i] > Neck.Price0  )class=class="str">"cmt">// if the movement goes beyond the formation, then the formation is fake
        {
        class="kw">return -class="num">1;
        }       
      }
    }
    
  if ( Close[class="num">0] >= Neck.Price0 )
    {
    Neck.Time0=Time[class="num">0];
    class="kw">return class="num">1;
    }    
  class="kw">return class="num">0;
  }
class="type">bool ExtremumsPatternFamilySearcher::bWasTrend()class=class="str">"cmt">// did we find the movement preceding the formation(also move here the anchor point to the intersection)
  {
  class="type">bool bCrossOptimist=class="kw">false;class=class="str">"cmt">// denotes if the neck is crossed
  if ( FormationDirection == class="num">1 )class=class="str">"cmt">// if the optimistic forecast is at the class="type">class="kw">double top
    {
    for(class="type">int i=Neck.Index1;i<BarsM;i++)class=class="str">"cmt">// define the intersection point
      {
      if ( High[i] > Neck.Price0 )class=class="str">"cmt">// if the movement goes beyond the neck, then the formation is fake
        {
        class="kw">return class="kw">false;
        }       

◍ 双底形态里乐观线的触发与证伪

在双底(FormationDirection == -1)的判定逻辑里,代码从颈线右侧第一根 K 线(Neck.Index1)扫到 BarsM 根为止,目的是确认价格是否真正刺穿乐观线。若期间任一根 Low[i] 跌破颈线 Price0,直接 return false——这意味着向下破位,双底失效,概率上倾向放弃多头设想。 反过来,只要 High[i] 上破 OptimistLine.Price0,就把该 K 线时间写进 OptimistLine.Time1 并返回 true,代表乐观目标被触及。外汇与贵金属这类高波动品种里,刺穿颈线后反向抽回的概率不低,因此用循环逐根校验比单根判断更稳。 下面这段是 MT5 里可直接粘去验证的核心片段,注意 Neek 与 OptimistLine 的结构体字段要和你自己的图形对象一致:

MQL5 / C++
   else if ( FormationDirection == -class="num">1 )class=class="str">"cmt">// if the optimistic forecast is at the class="type">class="kw">double bottom
      {
      for(class="type">int i=Neck.Index1;i<BarsM;i++)class=class="str">"cmt">// define the intersection point
         {
         if ( Low[i] < Neck.Price0 )class=class="str">"cmt">//  if the movement goes beyond the neck, then the formation is fake
            {
            class="kw">return class="kw">false;
            }         
         if ( High[i] > OptimistLine.Price0 )
            {
            OptimistLine.Time1=Time[i];
            class="kw">return true;
            }
         }         
      }
   class="kw">return class="kw">false;
   }

头肩之外还能挖什么

当前这套检测逻辑只覆盖了头肩家族的基础阵型,后续计划把搜索质量整体抬一档,并让类能直接识别头肩形态本身。更值得做的是混合阵型——比如「N 个顶部加多个肩部」这种非标准组合,靠现有单形态匹配是漏掉的。 系列不会只盯头肩,还会补进其它有趣形态、以及用不同类别方法检测阵型的思路。一个明确落点是:基于历史数据做交易回放,并收集不同金融产品、不同时间帧的统计数据,这部分在 MT5 里用标准历史接口就能跑。 等级(rank)也会单独拎出来研究,因为它常用来辅助判断反转。外汇和贵金属波动大、杠杆高,任何形态识别都只是概率信号,实盘前请用策略测试器在对应品种多周期验证。

「别急着下结论」

从可视化策略测试器里能直接看到,几十行 MQL5 代码就能把复杂形态抓出来,不一定非得上神经网络或机器视觉那套重武器。语言本身的功能厚度足够,更重的算法也只是时间和想象力的成本。 下一篇作者计划把所有形态拉到一起回测,用统计表说话,而不是堆代码。对交易者来说,这意味着你手里的 MT5 先能验证简单逻辑,再决定是否加复杂度。 外汇和贵金属波动受杠杆与事件驱动,形态命中只是概率倾向,实盘前请用策略测试器跑一遍自己的品种与周期。

把形态扫描交给小布盯盘
这些多顶与头肩混合体的识别逻辑,小布盯盘的 AIGC 已内置到品种页的辅助诊断里,你只需打开对应图表就能看到实时标注,把重复劳动交给小布,你专注决策。

常见问题

核心在顶部识别与过滤器设计,双顶写死两个峰值即可,多顶需用循环和类图映射动态抓取 N 个局部极值,并依统计剔除无效组合。
形态本身无严格数学定义,但可用统计学在策略测试器里回测胜率,代码价值在于高速批量验证,而非证明形态绝对有效。
可以,头肩可视作多顶的特殊混合体,调整顶部选择与方向判定逻辑,并在过滤器加入肩部高度差约束即可近似覆盖。
能,小布盯盘的品种页已内置基于类似算法的形态扫描,免写代码也能看实时标注,适合用来交叉核对你自己 EA 的识别结果。
因实盘与模拟统计成本高、样本慢,测试器虽滑点和流动性简化,但胜在速度与数据量,适合做形态有效性的初筛。