逆转形态:测试双顶/双底形态·进阶篇
趋势容器怎么从之字转向里捞极值
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 里打点验证。
~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 倍,外汇与贵金属杠杆品种请先意识到重画风险。
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 以上周期验证极值抓取是否跟手。
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 表示空头确认,外汇与贵金属杠杆高,这套判定仅过滤形态,实盘仍可能假突破。
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 各调两档,观察信号频率变化再决定跟否。
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 报错。
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;