逆转形态:测试双顶/双底形态·进阶篇
📘

逆转形态:测试双顶/双底形态·进阶篇

第 2/3 篇

趋势容器怎么从之字转向里捞极值

CTrends 类把 ZigZag 的零散拐点收拢成可遍历的趋势段。对外暴露的 Create 接收 CZigZag 指针与最小修正幅度 min_correction,IsHigh / Extremum / ExtremumByTime 负责按位置或时间反查极值,Total 内部先跑 Calculate 再回传 i_total,Symbol 与 Timeframe 在未初始化时分别回吐 "Not Initilized" 和 PERIOD_CURRENT。 Calculate 是这套封装的发动机。它先用 CheckPointer 拦掉无效 ZigZag,若底层 Total 为 0 直接返回 true;否则用上一段末端的 TimeStartBar 做 ExtremumByTime 定位续算起点,避免每次全量重算。 当定位返回 -1,说明末端时间戳已错位,代码会回退到 i_total-2 的极点时间再试一次,仍失败就释放 Trends 数组并把 i_total 清零。这种增量计算在 15 分钟以上周期回看 500 根 K 线时,重复调用 Total 的耗时可能比全量重建低一个数量级,值得在 MT5 里打点验证。

MQL5 / C++
  ~CTrends();
class=class="str">"cmt">//--- 类初始化方法
   class="kw">virtual class="type">bool      Create(CZigZag *pointer, class="type">class="kw">double min_correction);
class=class="str">"cmt">//--- 获取有关极值的信息
   class="kw">virtual class="type">bool      IsHigh(s_Extremum &pointer) class="kw">const;
   class="kw">virtual class="type">bool      Extremum(s_Extremum &pointer, class="kw">const class="type">int position=class="num">0);
   class="kw">virtual class="type">int       ExtremumByTime(class="type">class="kw">datetime time);
class=class="str">"cmt">//--- 获取一般信息
   class="kw">virtual class="type">int       Total(class="type">void)            {   Calculate(); class="kw">return i_total;   }
   class="kw">virtual class="type">class="kw">string    Symbol(class="type">void) class="kw">const     {   if(CheckPointer(C_ZigZag)==POINTER_INVALID) class="kw">return "Not Initilized"; class="kw">return C_ZigZag.Symbol();   }
   class="kw">virtual ENUM_TIMEFRAMES Timeframe(class="type">void) class="kw">const     {   if(CheckPointer(C_ZigZag)==POINTER_INVALID) class="kw">return PERIOD_CURRENT; class="kw">return C_ZigZag.Timeframe();   }

class="kw">protected:
   class="kw">virtual class="type">bool      Calculate(class="type">void);
   class="kw">virtual class="type">bool      AddTrendPoint(s_Extremum &pointer);
   };
class="type">bool CTrends::Calculate(class="type">void)
  {
  if(CheckPointer(C_ZigZag)==POINTER_INVALID)
     class="kw">return class="kw">false;
class=class="str">"cmt">//---
  if(C_ZigZag.Total()==class="num">0)
     class="kw">return true;
  class="type">int start=(i_total<=class="num">0 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-class="num">1].TimeStartBar));
  class="kw">switch(start)
    {
     case class="num">0:
        class="kw">return true;
        class="kw">break;
     case -class="num">1:
        start=(i_total<=class="num">1 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-class="num">2].TimeStartBar));
        if(start<class="num">0 || ArrayResize(Trends,i_total-class="num">1)<=class="num">0)
          {
           ArrayFree(Trends);
           i_total=class="num">0;
           start=C_ZigZag.Total();
          }
        else
           i_total=ArraySize(Trends);
        if(start==class="num">0)
           class="kw">return true;
        class="kw">break;
    }
  s_Extremum   base[];
  if(!C_ZigZag.Extremums(base,class="num">0,start))
     class="kw">return class="kw">false;
  class="type">int total=ArraySize(base);
  if(total<=class="num">0)

◍ 趋势点合并与调整过滤的实现细节

这段逻辑干的事,是把基础极值点序列 base[] 往 Trends[] 里塞,同时按最小调整阈值 d_MinCorrection 做冗余剔除。若 i_total==0 说明趋势数组还空着,先尝试把 base 最后一个点加进去,加失败直接 return false 中断。 主循环从 base 末尾倒着扫(i 从 total-1 到 0)。trends_pos 指向 Trends 当前末位;若已有趋势点的 TimeStartBar 不早于 base[i],说明时间重叠直接 continue 跳过。 当末位趋势点是高点(IsHigh)时分支最绕:若 base[i] 也是高点且价格更高,就更新末位趋势的 Price 和 TimeStartBar,等同把更早的次高点吸收掉。若 base[i] 是低点,且前面至少有两个趋势点、前高高于当前低点、再前高又高于末位高点,才计算 trend 与 correction 的差值比。 判定式 fabs(1-correction/trend)>d_MinCorrection 成立时,说明这段回调幅度不够格,于是把前高点的位置改成 base[i]、缩小 i_total、ArrayResize 重裁 Trends 数组并 continue。否则调用 AddTrendPoint 把 base[i] 作为新调整点收进序列。 CPattern 类把这些极值点打包成四个 s_Extremum 成员:趋势起、调整起、调整终、趋势终,再配 d_MinCorrection / d_MaxCorrection 双阈值。开 MT5 把 d_MinCorrection 从 0.1 调到 0.382 跑同一段 EURUSD H1,被吞掉的毛刺点数可能差出 3 到 5 倍,外汇与贵金属杠杆品种请先意识到重画风险。

MQL5 / C++
  class="kw">return true;
  if(i_total==class="num">0)
    if(!AddTrendPoint(base[total-class="num">1]))
      class="kw">return class="kw">false;
  for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
    {
      class="type">int trends_pos=i_total-class="num">1;
      if(Trends[trends_pos].TimeStartBar>=base[i].TimeStartBar)
        class="kw">continue;
      if(IsHigh(Trends[trends_pos]))
        {
          if(IsHigh(base[i]))
            {
              if(Trends[trends_pos].Price<base[i].Price)
                {
                  Trends[trends_pos].Price=base[i].Price;
                  Trends[trends_pos].TimeStartBar=base[i].TimeStartBar;
                }
              class="kw">continue;
            }
          else
            {
              if(trends_pos>class="num">1 && Trends[trends_pos-class="num">1].Price>base[i].Price  && Trends[trends_pos-class="num">2].Price>Trends[trends_pos].Price)
                {
                  class="type">class="kw">double trend=fabs(Trends[trends_pos].Price-Trends[trends_pos-class="num">1].Price);
                  class="type">class="kw">double correction=fabs(Trends[trends_pos].Price-base[i].Price);
                  if(fabs(class="num">1-correction/trend)>d_MinCorrection)
                    {
                      Trends[trends_pos-class="num">1].Price=base[i].Price;
                      Trends[trends_pos-class="num">1].TimeStartBar=base[i].TimeStartBar;
                      i_total--;
                      ArrayResize(Trends,i_total);
                      class="kw">continue;
                    }
                }
              AddTrendPoint(base[i]);
            }
        }
class CPattern : class="kw">public CObject
  {
class="kw">private:
   s_Extremum     s_StartTrend;       class=class="str">"cmt">//趋势起始点
   s_Extremum     s_StartCorrection;  class=class="str">"cmt">//调整起始点
   s_Extremum     s_EndCorrection;    class=class="str">"cmt">//调整结束点
   s_Extremum     s_EndTrend;         class=class="str">"cmt">//趋势完成点
   class="type">class="kw">double         d_MinCorrection;    class=class="str">"cmt">//最小调整
   class="type">class="kw">double         d_MaxCorrection;    class=class="str">"cmt">//最大调整
class=class="str">"cmt">//---

「形态类的骨架与极值抓取」

在 MT5 里做价格行为识别,先得把一段走势抽象成可复用的类。下面这段 CPattern 声明把趋势和回调的四个极值(StartTrend、StartCorrection、EndCorrection、EndTrend)封成了结构体成员,外部只管调 StartTrend() 这类内联方法拿数据,不用碰底层数组。 类初始化靠 Create():传入 CTrends 指针和回撤区间阈值 min_correction / max_correction。函数开头就用 CheckPointer 拦掉无效指针,随后把四个极值 Clear() 清零、b_found 置 false,相当于每次重新扫描前把状态机复位。 Search() 才是真正跑形态的地方。它要求 C_Trends 里至少有 4 个极值(Total()<4 直接返回 false),再用 ExtremumByTime 定位起点,倒序从 i 往 0 扫。循环里有个硬条件:(i+3) 不能超过总数,否则 continue——也就是说,一段完整 AB=CD 类结构最少占用连续 4 个极值槽位。 拿外汇或贵金属回测时记住:这类识别对点差和周末跳空敏感,小周期(如 M5)假信号概率偏高,建议先用 H1 以上周期验证极值抓取是否跟手。

MQL5 / C++
  class="type">bool                b_found;                 class=class="str">"cmt">//"已形态检测" 标志
class=class="str">"cmt">//---
  CTrends             *C_Trends;
class="kw">public:
                      CPattern();
                     ~CPattern();
class=class="str">"cmt">//--- 类初始化
  class="kw">virtual class="type">bool        Create(CTrends *trends, class="type">class="kw">double min_correction, class="type">class="kw">double max_correction);
class=class="str">"cmt">//--- 搜索形态和入场点的方法
  class="kw">virtual class="type">bool        Search(class="type">class="kw">datetime start_time);
  class="kw">virtual class="type">bool        CheckSignal(class="type">int &signal, class="type">class="kw">double &sl, class="type">class="kw">double &tp1, class="type">class="kw">double &tp2);
class=class="str">"cmt">//--- 比较对象的方法
  class="kw">virtual class="type">int         Compare(class="kw">const CPattern *node,class="kw">const class="type">int mode=class="num">0) class="kw">const;
class=class="str">"cmt">//--- 获取形态极值数据的方法
  s_Extremum          StartTrend(class="type">void)        class="kw">const {  class="kw">return s_StartTrend;        }
  s_Extremum          StartCorrection(class="type">void)  class="kw">const {  class="kw">return s_StartCorrection;  }
  s_Extremum          EndCorrection(class="type">void)    class="kw">const {  class="kw">return s_EndCorrection;    }
  s_Extremum          EndTrend(class="type">void)         class="kw">const {  class="kw">return s_EndTrend;         }
  class="kw">virtual class="type">class="kw">datetime    EndTrendTime(class="type">void)                   {  class="kw">return s_EndTrend.TimeStartBar;  }
  };
class="type">bool CPattern::Create(CTrends *trends,class="type">class="kw">double min_correction,class="type">class="kw">double max_correction)
  {
  if(CheckPointer(trends)==POINTER_INVALID)
     class="kw">return class="kw">false;
class=class="str">"cmt">//---
  C_Trends=trends;
  b_found=class="kw">false;
  s_StartTrend.Clear();
  s_StartCorrection.Clear();
  s_EndCorrection.Clear();
  s_EndTrend.Clear();
  d_MinCorrection=min_correction;
  d_MaxCorrection=max_correction;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CPattern::Search(class="type">class="kw">datetime start_time)
  {
  if(CheckPointer(C_Trends)==POINTER_INVALID || C_Trends.Total()<class="num">4)
     class="kw">return class="kw">false;
  class="type">int start=C_Trends.ExtremumByTime(start_time);
  if(start<class="num">0)
     class="kw">return class="kw">false;
  b_found=class="kw">false;
  for(class="type">int i=start;i>=class="num">0;i--)
    {
     if((i+class="num">3)>=C_Trends.Total())
        class="kw">continue;
     if(!C_Trends.Extremum(s_StartTrend,i+class="num">3) || !C_Trends.Extremum(s_StartCorrection,i+class="num">2) ||
        !C_Trends.Extremum(s_EndCorrection,i+class="num">1) || !C_Trends.Extremum(s_EndTrend,i))
        class="kw">continue;
     class="type">class="kw">double trend=s_StartCorrection.Price-s_StartTrend.Price;

确认周期里怎么给颈线破位定止损

CheckSignal 先在 b_found 为假或品种未初始化时直接返回 false,避免拿空结构去算信号。随后用 s_EndTrend.TimeStartBar 加上当前周期秒数得到确认起点,iBarShift 在 e_ConfirmationTF 上找对应 K 线偏移,shift<0 说明确认 K 线还没走完,同样弃用。 CopyRates 从 0 到 shift+1 拉出 MqlRates 数组,total<=0 即取数失败。up_trend 决定 extremum 取修正起点与趋势终点中的较高还是较低价,exit_level 用 2*修正终点价 - extremum 算出反向测幅位。 上升例中,若任一根 low<=exit_level 或 high>extremum 直接 false,意味着价格打穿测幅或创趋势新高就取消形态。break_neck 前的逻辑要求 close 站上 s_EndCorrection.Price 才翻牌,否则 continue 跳过。 翻牌后,high 超过修正终点价时把 sl 更新为该 high 的极大值;若 close 回破修正终点价且剩余 K 线多于 2 根则 false。最后留 signal=-1 表示空头确认,外汇与贵金属杠杆高,这套判定仅过滤形态,实盘仍可能假突破。

MQL5 / C++
class="type">bool CPattern::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;
  signal=class="num">0;
  sl=tp1=tp2=-class="num">1;
  class="type">bool up_trend=C_Trends.IsHigh(s_EndTrend);
  class="type">class="kw">double extremum=(up_trend ? fmax(s_StartCorrection.Price,s_EndTrend.Price) : fmin(s_StartCorrection.Price,s_EndTrend.Price));
  class="type">class="kw">double exit_level=class="num">2*s_EndCorrection.Price - extremum;
  class="type">bool break_neck=class="kw">false;
  for(class="type">int i=class="num">0;i<total;i++)
    {
    if(up_trend)
      {
      if(rates[i].low<=exit_level || rates[i].high>extremum)
        class="kw">return class="kw">false;
      if(!break_neck)
        {
        if(rates[i].close>s_EndCorrection.Price)
          class="kw">continue;
        break_neck=true;
        class="kw">continue;
        }
      if(rates[i].high>s_EndCorrection.Price)
        {
        if(sl==-class="num">1)
          sl=rates[i].high;
        else
          sl=fmax(sl,rates[i].high);
        }
      if(rates[i].close<s_EndCorrection.Price || sl==-class="num">1)
        class="kw">continue;
      if((total-i)>class="num">2)
        class="kw">return class="kw">false;
      signal=-class="num">1;

◍ 目标位算法与参数入口拆解

上面这段逻辑处理的是修正浪结束后的目标测算:先取趋势起点与修正终点中的较高价作为顶部参考,再按 0.9 倍价差向外推两个候选止盈,最后保留较大值作为实际 tp1。 double top=fmax(s_StartCorrection.Price,s_EndTrend.Price); // 取修正起点价与趋势终点价二者较大值赋给 top tp1=s_EndCorrection.Price-(top-s_EndCorrection.Price)*0.9; // 以修正终点为基准,向下量 0.9 倍顶至修正终点的价差 tp2=top-(top-s_StartTrend.Price)*0.9; // 以 top 为基准,向下量 0.9 倍顶至趋势起点价的价差 tp1=fmax(tp1,tp2); // 两个候选止盈取大,避免保守测算吃掉空间 break; // 跳出当前分支 参数区把 ZigZag 深度锁在 12、偏离 100、后退步数 3,最大回溯 1000 根柱线,主计算跑在 M30;形态过滤要求修正幅度介于 0.118~0.5 之间,并用 M5 做确认。 交易手数默认 0.1,外汇与贵金属杠杆品种波动剧烈,实盘前请在 MT5 策略测试器里把 i_Depth 与 d_MinCorrection 各调两档,观察信号频率变化再决定跟否。

MQL5 / C++
class="type">class="kw">double top=fmax(s_StartCorrection.Price,s_EndTrend.Price);
tp1=s_EndCorrection.Price-(top-s_EndCorrection.Price)*class="num">0.9;
tp2=top-(top-s_StartTrend.Price)*class="num">0.9;
tp1=fmax(tp1,tp2);
class="kw">break;
}
sinput   class="type">class="kw">string            s1           =  "---- ZigZag Settings ----";   class=class="str">"cmt">//---
class="kw">input    class="type">int               i_Depth      =  class="num">12;                                        class=class="str">"cmt">// 深度
class="kw">input    class="type">int               i_Deviation  =  class="num">100;                                       class=class="str">"cmt">// 偏离
class="kw">input    class="type">int               i_Backstep   =  class="num">3;                                         class=class="str">"cmt">// 后退步数
class="kw">input    class="type">int               i_MaxHistory =  class="num">1000;                                      class=class="str">"cmt">// 最大历史, 柱线
class="kw">input    ENUM_TIMEFRAMES   e_TimeFrame  =  PERIOD_M30;                                class=class="str">"cmt">// 工作时间帧
sinput   class="type">class="kw">string            s2           =  "---- Pattern Settings ----";    class=class="str">"cmt">//---
class="kw">input    class="type">class="kw">double            d_MinCorrection=  class="num">0.118;                                   class=class="str">"cmt">// 最小修正
class="kw">input    class="type">class="kw">double            d_MaxCorrection=  class="num">0.5;                                     class=class="str">"cmt">// 最大修正
class="kw">input    ENUM_TIMEFRAMES   e_ConfirmationTF= PERIOD_M5;                               class=class="str">"cmt">// 确认时间帧
sinput   class="type">class="kw">string            s3           =  "---- Trade Settings ----";        class=class="str">"cmt">//---
class="kw">input    class="type">class="kw">double            d_Lot        =  class="num">0.1;                                       class=class="str">"cmt">// 交易手数

「EA 初始化与对象生命周期的落地写法」

把形态识别做成 EA,第一道关是 OnInit 里把各类指针建稳。下面这段代码用 CArrayObj 统一托管指标与趋势搜索实例,任一 new 失败就直接返回 INIT_FAILED,避免半初始化状态在 OnTick 里爆雷。 滑点设 10 点、止损后退 350 点这两个 input 是外汇与贵金属场景下的保守默认值;实盘里 XAUUSD 在美盘流动性切换时滑点可能瞬时超过该值,建议先在策略测试器用 2023 年 1—9 月数据跑一遍再上真仓。 CTrade 这里关掉了异步模式(SetAsyncMode(false)),并用 SetDeviationInPoints 把允许偏差锁死在 l_Slippage。同步发单在高频 tick 下可能阻塞,但形态信号本身低频,换来的是成交回报确定性强。 OnDeinit 的逆序 delete 很关键:先循环释放数组内对象,再删数组本身,最后清 Trade 和 Pattern。漏掉 Pattern 那行,切换周期时就会留下悬空指针,MT5 日志会开始刷 cannot access 报错。

MQL5 / C++
class="kw">input   class="type">class="kw">ulong        l_Slippage     = class="num">10;                                            class=class="str">"cmt">// 滑点
class="kw">input   class="type">uint         i_SL           = class="num">350;                                            class=class="str">"cmt">// 止损后退,点数
CArrayObj          *ar_Objects;
CTrade             *Trade;
CPattern           *Pattern;
class="type">class="kw">datetime           start_search;
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 初始化对象数组
   ar_Objects=new CArrayObj();
   if(CheckPointer(ar_Objects)==POINTER_INVALID)
      class="kw">return INIT_FAILED;
class=class="str">"cmt">//--- 初始化之字折线指标类
   CZigZag *zig_zag=new CZigZag();
   if(CheckPointer(zig_zag)==POINTER_INVALID)
      class="kw">return INIT_FAILED;
   if(!ar_Objects.Add(zig_zag))
     {
       class="kw">delete zig_zag;
       class="kw">return INIT_FAILED;
     }
   zig_zag.Create(_Symbol,i_Depth,i_Deviation,i_Backstep,e_TimeFrame);
   zig_zag.MaxHistory(i_MaxHistory);
class=class="str">"cmt">//--- 初始化趋势走势搜索类
   CTrends *trends=new CTrends();
   if(CheckPointer(trends)==POINTER_INVALID)
      class="kw">return INIT_FAILED;
   if(!ar_Objects.Add(trends))
     {
       class="kw">delete trends;
       class="kw">return INIT_FAILED;
     }
   if(!trends.Create(zig_zag,d_MinCorrection))
      class="kw">return INIT_FAILED;
class=class="str">"cmt">//--- 初始化交易操作类
   Trade=new CTrade();
   if(CheckPointer(Trade)==POINTER_INVALID)
      class="kw">return INIT_FAILED;
   Trade.SetAsyncMode(class="kw">false);
   Trade.SetDeviationInPoints(l_Slippage);
   Trade.SetTypeFillingBySymbol(_Symbol);
class=class="str">"cmt">//--- 初始化其它变量
   start_search=class="num">0;
   CLimitTakeProfit::OnlyOneSymbol(true);
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
   }
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---
   if(CheckPointer(ar_Objects)!=POINTER_INVALID)
     {
       for(class="type">int i=ar_Objects.Total()-class="num">1;i>=class="num">0;i--)
          class="kw">delete ar_Objects.At(i);
       class="kw">delete ar_Objects;
     }
   if(CheckPointer(Trade)!=POINTER_INVALID)
      class="kw">delete Trade;
   if(CheckPointer(Pattern)!=POINTER_INVALID)
      class="kw">delete Pattern;
   }
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   class="kw">static class="type">class="kw">datetime Last_CfTF=class="num">0;

常见问题

用趋势容器承接之字转向的波峰波谷,遍历该段所有拐点取最高/最低值即为形态极值,注意只取同一趋势方向内的点。
对相邻趋势点按最小波动阈值合并,再用调整幅度占趋势比例过滤掉过小回撤,代码里设好 filter percent 参数即可。
可以,小布盯盘的 AIGC 已内置形态识别,打开对应品种页会自动标注双顶双底、颈线及破位参考止损。
止损放在右底下方加一点缓冲价差,或用确认周期收盘价破位才触发,能过滤掉小时线影线毛刺。
主要调形态高度倍数和确认周期长度,倍数默认 1 倍测距,周期拉长可降假信号但会晚进场。