逆转形态:测试头肩形态·进阶篇
(2/3)· 为什么你画对头肩却总被假突破洗出场?本文用智能交易系统拆解形态胜率真相
头肩形态过滤与信号确认的实现细节
这段逻辑先算头肩结构中几个关键距离:头部高度 header 是头部极值与修正终点价差绝对值,反转幅度 revers 是头部极值与右肩起点价差绝对值,右肩长度 r_shoulder 是趋势终点与右肩起点价差绝对值。若修正占趋势比小于 d_MinCorrection,或头部大于趋势减修正,或头反转不对称度(1 - 较小/较大)超 d_MaxCorrection,或右肩相对反转、修正相对头部回撤不足 d_MinCorrection,直接 continue 跳过,说明形态概率上不达标。 确认信号时,先取确认周期下从趋势终点到当前的 K 线,用 iBarShift 定位。若品种未初始化、shift 无效或 CopyRates 条数不足,返回 false,避免外汇贵金属这种高波动品种上误触发。 对于上升头肩(up_trend 为真),若价格刺穿头部极值或 tp1 已设且低点破 tp1,直接否掉。颈线用两点斜率 koef 推算:neck = koef*(shift2-shift-i)+右肩起点价。收盘价未破颈线就 continue;一破就将 break_neck 置真,并算 tp1 = 颈线 - (头-颈线)*0.9、tp2 = 颈线 - (颈线-趋势起点)*0.9,取两者较大值为 tp1。 开 MT5 把 d_MinCorrection、d_MaxCorrection 调成 0.1~0.3 区间跑一遍,能看出不对称过滤对假突破的剔除效果;外汇贵金属杠杆高,形态失效也常见,任何信号都只是概率倾向。
class="type">class="kw">double header=MathAbs(s_HeadExtremum.Price-s_EndCorrection.Price); class="type">class="kw">double revers=MathAbs(s_HeadExtremum.Price-s_StartRShoulder.Price); class="type">class="kw">double r_shoulder=MathAbs(s_EndTrend.Price-s_StartRShoulder.Price); if((correction/trend)<d_MinCorrection || header>(trend-correction) || (class="num">1-fmin(header,revers)/fmax(header,revers))>=d_MaxCorrection || (class="num">1-r_shoulder/revers)<d_MinCorrection || (class="num">1-correction/header)<d_MinCorrection) class="kw">continue; b_found= true; class=class="str">"cmt">//--- class="kw">break; } class=class="str">"cmt">//--- class="kw">return b_found; } class="type">bool CHS_Pattern::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; class=class="str">"cmt">//--- signal=class="num">0; sl=tp1=tp2=-class="num">1; class="type">bool up_trend=C_Trends.IsHigh(s_EndTrend); class="type">int shift1=iBarShift(symbol,e_ConfirmationTF,s_EndCorrection.TimeStartBar,true); class="type">int shift2=iBarShift(symbol,e_ConfirmationTF,s_StartRShoulder.TimeStartBar,true); if(shift1<=class="num">0 || shift2<=class="num">0) class="kw">return class="kw">false; class="type">class="kw">double koef=(s_StartRShoulder.Price-s_EndCorrection.Price)/(shift1-shift2); class="type">bool break_neck=class="kw">false; for(class="type">int i=class="num">0;i<total;i++) { if(up_trend) { if((tp1>class="num">0 && rates[i].low<=tp1) || rates[i].high>s_HeadExtremum.Price) class="kw">return class="kw">false; class="type">class="kw">double neck=koef*(shift2-shift-i)+s_StartRShoulder.Price; if(!break_neck) { if(rates[i].close>neck) class="kw">continue; break_neck=true; tp1=neck-(s_HeadExtremum.Price-neck)*class="num">0.9; tp2=neck-(neck-s_StartTrend.Price)*class="num">0.9; tp1=fmax(tp1,tp2); class="kw">continue; }
「颈线突破后的止损与信号锁定」
头肩形态判定里,颈线(neck)不是画完就完事。价格收破颈线之前,算法会持续跟踪极值来动态垫高或压低止损位:向上破位时用 fmax 保留最高价作 sl,向下破位时用 fmin 保留最低价作 sl,避免止损被毛刺洗掉。 一旦收盘价站上颈线且 sl 已初始化,循环 continue 继续向后看;但如果剩余未处理 K 线数 (total-i) 大于 2 根仍未确认,直接 return false 丢弃该形态——这意味着至少留 2 根缓冲来确认有效性。 向下破位分支中,tp1 先按 (颈线-头部极值)*0.9 估算,再与 (颈线-趋势起点)*0.9 取较小值,fmin 保证目标不过分乐观。若某根 high 摸到 tp1 或 low 跌破头部极值,形态直接判废 return false。 颈线首次被 close 跌破才置 break_neck=true 并算 tp,之后若 low 回插颈线下方则继续刷新 sl。最终 signal 赋 1 或 -1 并 break,函数返回 true 表示形态可用。外汇与贵金属波动剧烈,这类形态失效概率不低,实盘前务必在 MT5 用历史数据跑通该逻辑。
if(rates[i].high>neck) { if(sl==-class="num">1) sl=rates[i].high; else sl=fmax(sl,rates[i].high); } if(rates[i].close>neck || sl==-class="num">1) class="kw">continue; if((total-i)>class="num">2) class="kw">return class="kw">false; class=class="str">"cmt">//--- signal=-class="num">1; class="kw">break; } else { if((tp1>class="num">0 && rates[i].high>=tp1) || rates[i].low<s_HeadExtremum.Price) class="kw">return class="kw">false; class="type">class="kw">double neck=koef*(shift2-shift-i)+s_StartRShoulder.Price; if(!break_neck) { if(rates[i].close<neck) class="kw">continue; break_neck=true; tp1=neck+(neck-s_HeadExtremum.Price)*class="num">0.9; tp2=neck+(s_StartTrend.Price-neck)*class="num">0.9; tp1=fmin(tp1,tp2); class="kw">continue; } if(rates[i].low<neck) { if(sl==-class="num">1) sl=rates[i].low; else sl=fmin(sl,rates[i].low); } if(rates[i].close<neck || sl==-class="num">1) class="kw">continue; if((total-i)>class="num">2) class="kw">return class="kw">false; class=class="str">"cmt">//--- signal=class="num">1; class="kw">break; } } class=class="str">"cmt">//--- class="kw">return true; }
◍ 把两套逆转形态塞进一个EA
两个独立的逆转形态EA若各跑各的,资金占用和切换成本都不划算。既然搜索类本来就是继承关系,合并成单一程序的工作量很小:给基类加Type虚方法,CPattern返回101、CHS_Pattern返回102,靠数值区分形态类型;形态多了再换枚举,可读性更好。 类比较用Compare方法按Type和三段趋势/修正的起始时间逐级比对,返回-1/0/1。EA侧只需补一个NewClass函数,用switch按type生成对应实例,OnTick里循环pat从0到1依次调NewClass并跑历史搜索,入场模块因继承结构无需改动。 合并后用相同参数回测,128笔交易里约60%盈利,盈利因子1.94、恢复因子3.85,说明两策略在样本期内倾向互补而非互斥。外汇与贵金属杠杆高,这类历史表现只代表过去概率,实盘前务必在MT5用TwoPatterns.mq5自行验证。 别把数值常量当长久方案 形态只有两种时用101/102硬编码没问题,一旦要加第三、第四种,switch和Compare里的魔法数字会迅速失控,早点改枚举省得后期重构。
class="type">int CPattern::Compare(class="kw">const CPattern *node,class="kw">const class="type">int mode=class="num">0) class="kw">const { if(Type()>node.Type()) class="kw">return -class="num">1; else if(Type()<node.Type()) class="kw">return class="num">1; class=class="str">"cmt">//--- if(s_StartTrend.TimeStartBar>node.StartTrend().TimeStartBar) class="kw">return -class="num">1; else if(s_StartTrend.TimeStartBar<node.StartTrend().TimeStartBar) class="kw">return class="num">1; class=class="str">"cmt">//--- if(s_StartCorrection.TimeStartBar>node.StartCorrection().TimeStartBar) class="kw">return -class="num">1; else if(s_StartCorrection.TimeStartBar<node.StartCorrection().TimeStartBar) class="kw">return class="num">1; class=class="str">"cmt">//--- if(s_EndCorrection.TimeStartBar>node.EndCorrection().TimeStartBar) class="kw">return -class="num">1; else if(s_EndCorrection.TimeStartBar<node.EndCorrection().TimeStartBar) class="kw">return class="num">1; class=class="str">"cmt">//--- class="kw">return class="num">0; } CPattern *NewClass(class="type">int type) { class="kw">switch(type) { case class="num">0: class="kw">return new CPattern(); class="kw">break; case class="num">1: class="kw">return new CHS_Pattern(); class="kw">break; } class=class="str">"cmt">//--- class="kw">return NULL; } class="type">void OnTick() { class=class="str">"cmt">//--- ......................... ......................... ......................... class=class="str">"cmt">//--- for(class="type">int pat=class="num">0;pat<class="num">2;pat++) { Pattern=NewClass(pat); if(CheckPointer(Pattern)==POINTER_INVALID) class="kw">return; if(!Pattern.Create(ar_Objects.At(class="num">1),d_MinCorrection,d_MaxCorrection)) { class="kw">delete Pattern; class="kw">continue; } class=class="str">"cmt">//--- class="type">class="kw">datetime ss=start_search; class="kw">while(!IsStopped() && Pattern.Search(ss)) { ss=fmax(ss,Pattern.EndTrendTime()+PeriodSeconds(e_TimeFrame)); class="type">bool found=class="kw">false;
图形对象数组的去重与构建循环
这段循环是形态识别里最容易被忽略的脏活:它从索引 2 开始遍历已识别对象数组,跳过前两个基准元素,逐个用 Compare 方法比对当前 Pattern 是否与已有对象重复。 for(int i=2;i<ar_Objects.Total();i++) { CPattern *temp=ar_Objects.At(i); if(Pattern.Compare(temp,0)==0) { found=true; break; } } 若 found 为 true 直接 continue,避免把相同形态重复塞进数组;随后连续用 CheckPattern 验证形态有效性、用 ar_Objects.Add 尝试加入,任何一步失败都跳过本次迭代。 Pattern=NewClass(pat); if(CheckPointer(Pattern)==POINTER_INVALID) break; 新建形态实例后立刻用 CheckPointer 判空,指针无效就 break 退出,防止后续对野指针调用 Create。Create 失败时 delete 并 break,函数末尾再对残留 Pattern 做一次判空释放。 在 MT5 里跑这类逻辑时,建议把 ar_Objects.Total() 在循环外缓存成变量,数组频繁增减时能少几次方法调用开销。外汇与贵金属市场高杠杆、价格跳空频繁,这类对象管理代码若漏掉指针释放,实盘可能跑几小时就内存泄漏崩 EA。
for(class="type">int i=class="num">2;i<ar_Objects.Total();i++) { CPattern *temp=ar_Objects.At(i); if(Pattern.Compare(temp,class="num">0)==class="num">0) { found=true; class="kw">break; } } if(found) class="kw">continue; if(!CheckPattern(Pattern)) class="kw">continue; if(!ar_Objects.Add(Pattern)) class="kw">continue; Pattern=NewClass(pat); if(CheckPointer(Pattern)==POINTER_INVALID) class="kw">break; if(!Pattern.Create(ar_Objects.At(class="num">1),d_MinCorrection,d_MaxCorrection)) { class="kw">delete Pattern; class="kw">break; } } if(CheckPointer(Pattern)!=POINTER_INVALID) class="kw">delete Pattern; } class=class="str">"cmt">//--- class="kw">return; }
「同一套参数在不同品种上裂成两半」
把 EA 原样跑在 6 个主要货币对上、测试参数一字未改,结果直接劈成两半:EURUSD 盈利 743.57、盈利因子 1.94,EURJPY 盈利 125.13、因子 1.47,GBPJPY 微利 33.93、因子 1.04;而 EURGBP 亏 191.7、因子 0.82,GBPUSD 亏 371.05、因子 0.60,USDJPY 亏 657.38、因子仅 0.31。一半赚一半亏,说明价格行为在不同品种上根本不是同一套分布。 外汇和贵金属都是高杠杆高风险品种,跨品种直接套用同一 EA 参数大概率会吃掉你一半的账户。上面这张表本身就是证据:不优化就上实盘,亏损那边的因子已经掉到 0.3 附近。 只对止损回退做针对性优化,局面就变了。EURUSD 利润抬到 1020.28(回退 350),EURJPY 到 532.54(400),GBPJPY 到 208.69(300),EURGBP 翻正到 91.45(450),GBPUSD 和 USDJPY 亏损也收窄到 -315.87 和 -453.08(均 100)。整体篮子的盈利能力倾向改善,但 USDJPY 因子仍只有 0.33,远没到安全区。 别把正态当圣经 品种间相关性会骗人,GBPUSD 与 EURUSD 平时同涨同跌,可同一 EA 一个因子 1.94、一个 0.60。开 MT5 把这两对拉进同一次测试,逐项改止损回退,你会看到参数敏感度完全不对称。