从新手到专家:自动几何分析系统·进阶篇
(2/3)· 手动画趋势线又慢又飘?这篇用 OOP 把矩形、三角形检测写成可复用模块
几何形态检测器的底层结构体与类骨架
做形态识别,第一步是把「顶点」和「检测上下文」固化成结构。下面这段 C++ 风格的 MQL5 代码定义了一个 PatternResult 结构,用三个顶点时间/价格(vertex1~3)加标签位,把一次三角形或矩形形态的几何信息一次性打包。
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);
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 策略测试器用历史数据验证阈值敏感性。
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,三角误报率可能随品种明显变化。
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-a | 、 | d-c | )必须超过 tolATR,同时两个低点 a、c 的价差又要小于 tolATR,否则就只是无序波动而非箱体。 |
|---|
为了防止同一矩形被反复绘制,算法用 m_lastRectangleHash 记录「左时间_下沿_上沿」字符串,并设了 40 根 K 线的锁定期 lockT。若哈希未变且当前时间落在锁内,直接 return 跳过,这对 MT5 实盘减少对象闪烁很有用。 绘制前会先 ObjectDelete 掉上一条矩形,再用 ChartID 绑定的 cid 新建对象。你可以把 m_atrMultiplier 从默认 1.0 调到 0.6 试一下,在 XAUUSD H1 上箱体识别会更密,但假信号概率也会上升——贵金属杠杆高,验证时请先用模拟盘。
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 策略测试器用历史数据验证参数敏感性。
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" :