从新手到专家:自动几何分析系统·进阶篇
📐

从新手到专家:自动几何分析系统·进阶篇

(2/3)· 手动画趋势线又慢又飘?这篇用 OOP 把矩形、三角形检测写成可复用模块

实战向进阶 第 2/3 篇
很多交易者还在手动拖趋势线找三角形,区间一拉长连线就随心情偏。手动几何分析不仅慢,还容易漏掉压缩末端的关键突破。把识别逻辑固化成代码,才能拿到前后一致的信号。

几何形态检测器的底层结构体与类骨架

做形态识别,第一步是把「顶点」和「检测上下文」固化成结构。下面这段 C++ 风格的 MQL5 代码定义了一个 PatternResult 结构,用三个顶点时间/价格(vertex1~3)加标签位,把一次三角形或矩形形态的几何信息一次性打包。

MQL5 / C++
class="type">class="kw">double vertex1Price;
class="type">class="kw">datetime vertex2Time;
class="type">class="kw">double vertex2Price;
class="type">class="kw">datetime vertex3Time;
class="type">class="kw">double vertex3Price;
PatternResult(const class="type">class="kw">string _s,const ENUM_TIMEFRAMES _tf,const ENUM_PATTERN_TYPE _t,
              class="type">class="kw">datetime lt,class="type">class="kw">double lp,
              class="type">class="kw">datetime v1t,class="type">class="kw">double v1p,
              class="type">class="kw">datetime v2t,class="type">class="kw">double v2p,
              class="type">class="kw">datetime v3t,class="type">class="kw">double v3p)
   : symbol(_s), timeframe(_tf), type(_t), detectionTime(TimeCurrent()),
     labelTime(lt), labelPrice(lp),
     vertex1Time(v1t), vertex1Price(v1p),
     vertex2Time(v2t), vertex2Price(v2p),
     vertex3Time(v3t), vertex3Price(v3p) {}
};
逐行看:vertex1Price 存第一个转折点的报价;vertex2Time / vertex2Price 是第二顶点时空坐标;vertex3 同理。构造函数把品种 _s、周期 _tf、形态类型 _t 传进来,detectionTime 直接取 TimeCurrent() 记当下检测时刻,label 位是形态命名锚点。 真正的调度核心是 CGeometricPatternDetector 类。私有成员里 m_swingLookback 默认 3,代表左右各看 3 根 K 线确认摆动点;m_atrMultiplier 默认 1.5,用 ATR 的 1.5 倍作噪声过滤阈值;m_minTouchPoints 默认 2,即一条边界至少被触碰 2 次才认。
MQL5 / C++
class CGeometricPatternDetector {
class="kw">private:
  CArrayObj  m_swings;
  class="type">int        m_swingLookback;
  class="type">class="kw">double     m_atrMultiplier;
  class="type">int        m_minTouchPoints;
  class="type">class="kw">string     m_lastTriangle;
  class="type">class="kw">string     m_lastSwingHash;
  class="type">class="kw">datetime   m_lastTriangleTime;
  class="type">class="kw">string     m_lastRectangle;
  class="type">class="kw">string     m_lastRectangleHash;
  class="type">class="kw">datetime   m_lastRectangleTime;
  class="type">bool      IsNewBar(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf);
  class="type">void      UpdateSwingPoints(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf);
  class="type">class="kw">double    CalculateATR(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf,const class="type">int period=class="num">14);
  class="type">class="kw">string    GetSwingHash(SwingPoint *p1,SwingPoint *p2,SwingPoint *p3,SwingPoint *p4);
class="kw">public:
  CGeometricPatternDetector(class="type">int swingLookback=class="num">3,class="type">class="kw">double atrMultiplier=class="num">1.5,class="type">int minTouchPoints=class="num">2);
  ~CGeometricPatternDetector();
  class="type">void Update(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf);
  class="type">void DetectTriangle(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf);
  class="type">void DetectRectangle(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf);
  ENUM_PATTERN_TYPE GetLastPattern();
  class="type">void ClearPatterns();
  CArrayObj m_currentPatterns;
};
m_lastTriangle / m_lastRectangle 配 _Hash 和 _Time,是用来防重复绘制的去重键。CalculateATR 周期写死 14,是 MT5 里最通用的波动基准。 UpdateSwingPoints 是每根新柱调用的摆动点刷新器。代码开头先算 bars = m_swingLookback*2 + 1,当 lookback=3 时取 7 根;然后给 highs / lows / times 三个数组开 ArraySetAsSeries(...,true),把索引 0 钉在当前柱,方便用 [1]、[2] 往历史回溯。
MQL5 / C++
class="type">void CGeometricPatternDetector::UpdateSwingPoints(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf)
{
  class="type">int bars = m_swingLookback*class="num">2 + class="num">1;
  class="type">class="kw">double highs[], lows[];
  class="type">class="kw">datetime times[];
  ArraySetAsSeries(highs,true);
  ArraySetAsSeries(lows,true);
  ArraySetAsSeries(times,true);
外汇与贵金属波动剧烈,ATR 倍数和 lookback 直接决定假突破频率;建议把这段类骨架拷进 MT5 的 include 文件,先改 m_atrMultiplier 从 1.5 试到 2.0,观察 XAUUSD 在 H1 上矩形识别数量的变化。

MQL5 / C++
class="type">class="kw">double vertex1Price;
class="type">class="kw">datetime vertex2Time;
class="type">class="kw">double vertex2Price;
class="type">class="kw">datetime vertex3Time;
class="type">class="kw">double vertex3Price;
PatternResult(const class="type">class="kw">string _s,const ENUM_TIMEFRAMES _tf,const ENUM_PATTERN_TYPE _t,
              class="type">class="kw">datetime lt,class="type">class="kw">double lp,
              class="type">class="kw">datetime v1t,class="type">class="kw">double v1p,
              class="type">class="kw">datetime v2t,class="type">class="kw">double v2p,
              class="type">class="kw">datetime v3t,class="type">class="kw">double v3p)
   : symbol(_s), timeframe(_tf), type(_t), detectionTime(TimeCurrent()),
     labelTime(lt), labelPrice(lp),
     vertex1Time(v1t), vertex1Price(v1p),
     vertex2Time(v2t), vertex2Price(v2p),
     vertex3Time(v3t), vertex3Price(v3p) {};
class CGeometricPatternDetector {
class="kw">private:
  CArrayObj  m_swings;
  class="type">int        m_swingLookback;
  class="type">class="kw">double     m_atrMultiplier;
  class="type">int        m_minTouchPoints;
  class="type">class="kw">string     m_lastTriangle;
  class="type">class="kw">string     m_lastSwingHash;
  class="type">class="kw">datetime   m_lastTriangleTime;
  class="type">class="kw">string     m_lastRectangle;
  class="type">class="kw">string     m_lastRectangleHash;
  class="type">class="kw">datetime   m_lastRectangleTime;
  class="type">bool      IsNewBar(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf);
  class="type">void      UpdateSwingPoints(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf);
  class="type">class="kw">double    CalculateATR(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf,const class="type">int period=class="num">14);
  class="type">class="kw">string    GetSwingHash(SwingPoint *p1,SwingPoint *p2,SwingPoint *p3,SwingPoint *p4);
class="kw">public:
  CGeometricPatternDetector(class="type">int swingLookback=class="num">3,class="type">class="kw">double atrMultiplier=class="num">1.5,class="type">int minTouchPoints=class="num">2);
  ~CGeometricPatternDetector();
  class="type">void Update(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf);
  class="type">void DetectTriangle(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf);
  class="type">void DetectRectangle(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf);
  ENUM_PATTERN_TYPE GetLastPattern();
  class="type">void ClearPatterns();
  CArrayObj m_currentPatterns;
};
class="type">void CGeometricPatternDetector::UpdateSwingPoints(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf)
{
  class="type">int bars = m_swingLookback*class="num">2 + class="num">1;
  class="type">class="kw">double highs[], lows[];
  class="type">class="kw">datetime times[];
  ArraySetAsSeries(highs,true);
  ArraySetAsSeries(lows,true);
  ArraySetAsSeries(times,true);

「摆点判定与三角检测的底层实现」

摆点识别先卡数据完整性:CopyHigh/CopyLow/CopyTime 任意一个返回值小于 bars,直接打印错误并 return,避免后续数组越界。中点取 m_swingLookback 索引处的 K 线,向左右各延展 m_swingLookback 根比对高低,只有严格高于两侧才记为 Swing High,严格低于两侧才记为 Swing Low。 向量容器 m_swings 上限锁在 50,超出就从头部 delete 指针并 Remove,防止长期运行内存膨胀。ATR 用 iATR 拿句柄后只 CopyBuffer 最近 1 根,失败返回 0,调用方需自行处理 0 值情形。 GetSwingHash 把四个摆点的时间与价格(精度 8 位)拼成字符串,时间精确到秒、价格到小数点第 8 位,用作形态去重键。DetectTriangle 要求 m_swings 至少 4 个点,并用 PeriodSeconds(tf) 乘 m_swingLookback 算出最小跨度秒数 minSpan,作为三角收敛的时间门槛。外汇与贵金属波动受杠杆影响大,这类几何检测仅提供概率参考,实盘前请在 MT5 策略测试器用历史数据验证阈值敏感性。

MQL5 / C++
  if(CopyHigh(sym,tf,class="num">0,bars,highs)<bars ||
      CopyLow(sym,tf,class="num">0,bars,lows)<bars  ||
      CopyTime(sym,tf,class="num">0,bars,times)<bars)
   {
      Print("Error: Failed to copy price/time data");
      class="kw">return;
   }
   class="type">int mid = m_swingLookback;
   class="type">class="kw">datetime t = times[mid];
   class="type">class="kw">double h = highs[mid], l = lows[mid];
   class="type">bool isH = true;
   for(class="type">int i=class="num">1; i<=m_swingLookback; i++)
      if(h<=highs[mid-i] || h<=highs[mid+i]) { isH=false; class="kw">break; }
   if(isH) {
      m_swings.Add(new SwingPoint(t,h,true));
      Print("Swing High detected at ",TimeToString(t)," Price: ",h);
   }
   class="type">bool isL = true;
   for(class="type">int i=class="num">1; i<=m_swingLookback; i++)
      if(l>=lows[mid-i] || l>=lows[mid+i]) { isL=false; class="kw">break; }
   if(isL) {
      m_swings.Add(new SwingPoint(t,l,false));
      Print("Swing Low detected at ",TimeToString(t)," Price: ",l);
   }
   while(m_swings.Total()>class="num">50) {
      class="kw">delete (SwingPoint*)m_swings.At(class="num">0);
      m_swings.Delete(class="num">0);
   }
}
class="type">class="kw">double CGeometricPatternDetector::CalculateATR(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf,const class="type">int period)
{
   class="type">int h = iATR(sym,tf,period);
   if(h==INVALID_HANDLE) { Print("ATR handle error"); class="kw">return class="num">0; }
   class="type">class="kw">double buf[]; ArraySetAsSeries(buf,true);
   if(CopyBuffer(h,class="num">0,class="num">0,class="num">1,buf)!=class="num">1) { Print("ATR copy error"); class="kw">return class="num">0; }
   class="kw">return buf[class="num">0];
}
class="type">class="kw">string CGeometricPatternDetector::GetSwingHash(SwingPoint *p1,SwingPoint *p2,SwingPoint *p3,SwingPoint *p4)
{
   class="kw">return TimeToString(p1.time)+"*"+DoubleToString(p1.price,class="num">8)+"*"
         + TimeToString(p2.time)+"*"+DoubleToString(p2.price,class="num">8)+"*"
         + TimeToString(p3.time)+"*"+DoubleToString(p3.price,class="num">8)+"*"
         + TimeToString(p4.time)+"_"+DoubleToString(p4.price,class="num">8);
}
class="type">void CGeometricPatternDetector::DetectTriangle(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf)
{
   class="type">int tot = m_swings.Total();
   if(tot < class="num">4) class="kw">return;
   class="type">ulong barSec = PeriodSeconds(tf);
   class="type">ulong minSpan = (class="type">ulong)m_swingLookback * barSec;

◍ 用四点摆动结构锁定三角形收敛

识别三角整理,核心是连续取四个摆动点并判断高低交替。下面这段逻辑要求序列满足「低-高-低-高」形态,即 p1、p3 为波段低点,p2、p4 为波段高点,否则直接跳过。 相邻摆动点时间间隔不能太短,需用 minSpan 过滤杂波;若任意两段间隔小于该值则放弃,避免把震荡毛刺当结构。 斜率用价格差除以时间差来算:m_low 是下边(p1→p3)斜率,m_high 是上边(p2→p4)斜率。再用 14 周期 ATR 乘倍数 tolFlat 作容差,判断某一边是否足够平。 若下边平坦且上边斜率为负,记为下降三角;上边平坦且下边斜率为正,记为上升三角;其余归为对称三角。两条斜率差 denom 接近 0(绝对值小于 1e-12)说明近乎平行,直接弃用。 交点 tx、py 由线性联立解出,要求 tx 落在最新摆动点之后、且不超过当前时间加 50 根 K 线的周期,否则不画。旧对象先删再建,用 OBJ_TRIANGLE 连 p1、p2 与交点,橙色空心线宽 2,肉眼复核很方便。 外汇与贵金属波动剧烈,ATR 容差倍数建议先在 M1~H1 多周期回看,再定 minSpan,三角误报率可能随品种明显变化。

MQL5 / C++
for(class="type">int i=class="num">0; i<=tot-class="num">4; i++)
  {
    SwingPoint *p1 = (SwingPoint*)m_swings.At(i);
    SwingPoint *p2 = (SwingPoint*)m_swings.At(i+class="num">1);
    SwingPoint *p3 = (SwingPoint*)m_swings.At(i+class="num">2);
    SwingPoint *p4 = (SwingPoint*)m_swings.At(i+class="num">3);
    if(!( !p1.isHigh && p2.isHigh && !p3.isHigh && p4.isHigh )) class="kw">continue;
    if((class="type">ulong)(p2.time-p1.time)<minSpan ||
       (class="type">ulong)(p3.time-p2.time)<minSpan ||
       (class="type">ulong)(p4.time-p3.time)<minSpan) class="kw">continue;
    class="type">class="kw">double m_low  = (p3.price - p1.price) / class="type">class="kw">double(p3.time - p1.time);
    class="type">class="kw">double m_high = (p4.price - p2.price) / class="type">class="kw">double(p4.time - p2.time);
    class="type">class="kw">double tolFlat = CalculateATR(sym,tf,class="num">14) * m_atrMultiplier;
    class="type">bool lowerFlat = MathAbs(p3.price-p1.price) < tolFlat;
    class="type">bool upperFlat = MathAbs(p4.price-p2.price) < tolFlat;
    ENUM_PATTERN_TYPE type;
    if(lowerFlat && m_high < class="num">0)           type = PATTERN_TRIANGLE_DESCENDING;
    else if(upperFlat && m_low > class="num">0)       type = PATTERN_TRIANGLE_ASCENDING;
    else                                  type = PATTERN_TRIANGLE_SYMMETRICAL;
    class="type">class="kw">double denom = m_low - m_high;
    if(MathAbs(denom)<class="num">1e-12) class="kw">continue;
    class="type">class="kw">double num = (p2.price - p1.price) + (m_low*p1.time - m_high*p2.time);
    class="type">class="kw">double tx  = num/denom;
    class="type">class="kw">double px  = p1.price + m_low*(tx-p1.time);
    class="type">class="kw">datetime latest = MathMax(p1.time,p2.time);
    if(tx<=latest || tx>TimeCurrent()+barSec*class="num">50) class="kw">continue;
    if(StringLen(m_lastTriangle)>class="num">0)
       ObjectDelete(class="num">0,m_lastTriangle);
    class="type">class="kw">string base = "GPD_"+sym+"_"+EnumToString(tf)+"_"+TimeToString(TimeCurrent(),TIME_SECONDS);
    class="type">long cid      = ChartID();
    m_lastTriangle = base+"_T";
    ObjectCreate(cid,m_lastTriangle,OBJ_TRIANGLE,class="num">0,
                 p1.time,p1.price,
                 p2.time,p2.price,
                 tx,       px);
    ObjectSetInteger(cid,m_lastTriangle,OBJPROP_COLOR,clrOrange);
    ObjectSetInteger(cid,m_lastTriangle,OBJPROP_WIDTH,class="num">2);
    ObjectSetInteger(cid,m_lastTriangle,OBJPROP_FILL,false);
    m_lastSwingHash      = GetSwingHash(p1,p2,p3,p4);

矩形震荡区的判定与去重锁

矩形形态识别依赖四个摆动点构成「低-高-低-高」序列,且相邻高低点间距不能小于 5 根 K 线周期。代码里用 minSpan5 = 5 * PeriodSeconds(tf) 强制过滤掉那些因噪音产生的密集拐点,避免在 M1 图表上刷出一堆伪矩形。

容差直接用 ATR(14) 乘以可调系数 m_atrMultiplier。上下沿各自的振幅(b-ad-c)必须超过 tolATR,同时两个低点 a、c 的价差又要小于 tolATR,否则就只是无序波动而非箱体。

为了防止同一矩形被反复绘制,算法用 m_lastRectangleHash 记录「左时间_下沿_上沿」字符串,并设了 40 根 K 线的锁定期 lockT。若哈希未变且当前时间落在锁内,直接 return 跳过,这对 MT5 实盘减少对象闪烁很有用。 绘制前会先 ObjectDelete 掉上一条矩形,再用 ChartID 绑定的 cid 新建对象。你可以把 m_atrMultiplier 从默认 1.0 调到 0.6 试一下,在 XAUUSD H1 上箱体识别会更密,但假信号概率也会上升——贵金属杠杆高,验证时请先用模拟盘。

MQL5 / C++
m_lastTriangleTime = TimeCurrent();
m_currentPatterns.Add(new PatternResult(
   sym,tf,type,
   latest,(p2.price+p4.price)/class="num">2.0,
   p1.time,p1.price,
   p2.time,p2.price,
   (class="type">class="kw">datetime)tx,px
));
ChartRedraw(cid);
class="kw">break;
}
}
class="type">void CGeometricPatternDetector::DetectRectangle(const class="type">class="kw">string sym,const ENUM_TIMEFRAMES tf)
{
   class="type">int tot = m_swings.Total();
   if(tot < class="num">4) class="kw">return;
   class="type">ulong barSec    = PeriodSeconds(tf);
   class="type">ulong minSpan5  = class="num">5 * barSec;                     
   class="type">class="kw">double tolATR   = CalculateATR(sym,tf,class="num">14) * m_atrMultiplier;
   for(class="type">int i=class="num">0; i<=tot-class="num">4; i++)
   {
      SwingPoint *a = (SwingPoint*)m_swings.At(i);
      SwingPoint *b = (SwingPoint*)m_swings.At(i+class="num">1);
      SwingPoint *c = (SwingPoint*)m_swings.At(i+class="num">2);
      SwingPoint *d = (SwingPoint*)m_swings.At(i+class="num">3);
      if(!( !a.isHigh && b.isHigh && !c.isHigh && d.isHigh )) class="kw">continue;
      if((class="type">ulong)(b.time - a.time) < minSpan5 ||
         (class="type">ulong)(d.time - c.time) < minSpan5) class="kw">continue;
      if(MathAbs(b.price - a.price) < tolATR ||
         MathAbs(d.price - c.price) < tolATR) class="kw">continue;
      if(MathAbs(a.price - c.price) > tolATR) class="kw">continue;
      class="type">bool highAligned = MathAbs(b.price - d.price) < tolATR;
      class="type">class="kw">double lowP  = (a.price + c.price) / class="num">2.0;
      class="type">class="kw">double highP = highAligned ? (b.price + d.price)/class="num">2.0 : lowP + tolATR;
      class="type">class="kw">datetime leftT  = MathMin(a.time, c.time);
      class="type">class="kw">datetime rightT = leftT + (class="type">class="kw">datetime)(class="num">20 * barSec);
      class="type">class="kw">string rh = TimeToString(leftT,TIME_SECONDS) + "_" +
                  DoubleToString(lowP,class="num">8) + "_" +
                  DoubleToString(highP,class="num">8);
      class="type">class="kw">datetime lockT = m_lastRectangleTime + (class="type">class="kw">datetime)(class="num">40 * barSec);
      if(rh == m_lastRectangleHash && TimeCurrent() < lockT) class="kw">return;
      if(StringLen(m_lastRectangle) > class="num">0)
         ObjectDelete(class="num">0,m_lastRectangle);
      class="type">class="kw">string base = "GPD_"+sym+"_"+EnumToString(tf)+"_"+TimeToString(TimeCurrent(),TIME_SECONDS);
      class="type">long cid     = ChartID();
      m_lastRectangle = base+"_Rect";

「把矩形识别落进EA的绘制与生命周期」

矩形被 detector 命中后,先用 ObjectCreate 在图表上画一个不填充的蓝色边框矩形:左上角取 leftT 与 highP,右下角取 rightT 与 lowP,线宽设为 2。随后把中点价格 (highP+lowP)/2.0 连同左右边界时间写进 PatternResult,推入 m_currentPatterns 数组,供后续告警或统计调用。 ObjectSetInteger 里 OBJPROP_FILL 传 false 是关键——填充开启会挡住 K 线,肉眼读价格行为反而更累。m_lastRectangleHash 与 m_lastRectangleTime 记录本次矩形指纹和时刻,ChartRedraw 强制重绘,break 退出当前分支。 EA 层用 CGeometricPatternDetector detector(3, 1.5, 2) 初始化,三个参数分别对应最小边数、形态容差、最小跨度,调小容差会让假突破过滤更严但可能漏掉宽幅整理。OnInit 仅打印初始化成功,OnDeinit 里 detector.ClearPatterns() 并删掉上次留下的标签对象,避免历史图形堆在图表上。 OnTick 用 iTime 取当前柱时间,与 lastBarTime 比较判断新柱;非新柱直接 return,保证每根 K 线只评估一次形态,降低 CPU 占用。新柱到来会清掉 lastAlerted 与旧标签,防止同一形态跨柱重复报警。 detector.Update 跑完之后,GetLastPattern 拿最新类型,若等于 PATTERN_NONE 或跟上根柱一样就跳过。取数组末位 PatternResult 读边界,按类型拼 name 字符串——注意外汇与贵金属波动剧烈,形态失效概率不低,实盘前务必在 MT5 策略测试器用历史数据验证参数敏感性。

MQL5 / C++
ObjectCreate(cid,m_lastRectangle,OBJ_RECTANGLE,class="num">0,
                     leftT,  highP,
                     rightT, lowP);
ObjectSetInteger(cid,m_lastRectangle,OBJPROP_COLOR, clrBlue);
ObjectSetInteger(cid,m_lastRectangle,OBJPROP_WIDTH, class="num">2);
ObjectSetInteger(cid,m_lastRectangle,OBJPROP_FILL,  false);
m_currentPatterns.Add(new PatternResult(
   sym,tf,PATTERN_RECTANGLE,
   leftT,(highP+lowP)/class="num">2.0,
   leftT,highP,
   leftT,lowP,
   rightT,lowP
));
m_lastRectangleHash = rh;
m_lastRectangleTime = TimeCurrent();
ChartRedraw(cid);
class="kw">break;
}
class="macro">#include <GeometricPatternDetector.mqh>
class=class="str">"cmt">//–– detector instance & state
CGeometricPatternDetector   detector(class="num">3, class="num">1.5, class="num">2);
ENUM_PATTERN_TYPE           lastAlerted   = PATTERN_NONE;
class="type">class="kw">datetime                    lastBarTime   = class="num">0;
class="type">class="kw">string                      lastLabelName = "";
class="type">int OnInit()
{
   Print("GeometryAnalyzerEA initialized");
   class="kw">return(INIT_SUCCEEDED);
}
class="type">void OnDeinit(const class="type">int reason)
{
   detector.ClearPatterns();
   if(StringLen(lastLabelName) > class="num">0)
      ObjectDelete(class="num">0, lastLabelName);
   Print("GeometryAnalyzerEA deinitialized, reason=", reason);
}
class="type">void OnTick()
{
   class="type">class="kw">datetime curBar = iTime(Symbol(), _Period, class="num">0);
   class="type">bool isNewBar = (curBar != lastBarTime);
   if(isNewBar)
   {
      lastBarTime = curBar;
      lastAlerted = PATTERN_NONE;
      if(StringLen(lastLabelName) > class="num">0)
      {
         ObjectDelete(class="num">0, lastLabelName);
         lastLabelName = "";
      }
   }
   if(!isNewBar)
      class="kw">return;
   class=class="str">"cmt">// …
}
detector.Update(Symbol(), _Period);
ENUM_PATTERN_TYPE pattern = detector.GetLastPattern();
if(pattern == PATTERN_NONE || pattern == lastAlerted)
   class="kw">return;
lastAlerted = pattern;
class="type">int count = detector.m_currentPatterns.Total();
PatternResult *pr = (PatternResult*)detector.m_currentPatterns.At(count - class="num">1);
class="type">class="kw">string name = (pattern == PATTERN_RECTANGLE) ? "Rectangle" :
              (pattern == PATTERN_TRIANGLE_ASCENDING)   ? "Ascending Triangle" :
              (pattern == PATTERN_TRIANGLE_DESCENDING)  ? "Descending Triangle" :
让小布替你跑这套
这些几何形态的自动识别逻辑,小布盯盘的 AIGC 看盘已内置,打开对应品种页就能直接看到实时三角与矩形标注,你只需判断概率与节奏。

常见问题

OOP 把高点低点、趋势线、形态类拆成独立模块,扩展矩形、通道只需继承改写,避免过程式代码越写越乱。
矩形要求上下沿近似平行且区间收窄慢,三角形则是两边斜率相向、波幅递减,代码里用斜率与极点距离双阈值区分。
可以,小布盯盘在品种页对识别出的形态回溯统计突破倾向与回撤概率,省去你自己写测试脚本。
长序列混入噪声 K 线增多,局部反转破坏单一形态结构,所以先提极点再连形状比硬匹配模板更稳。
不能,外汇贵金属高风险,几何形态只提供空间参考,突破倾向需结合量仓与更高周期确认。