通道突破形态(基础篇)
◍ 通道突破在 MT5 里的形态识别
通道突破形态是价格行为里最常被手动盯的形态之一:价格在一个近似平行的上下轨之间反复折返,直到某根 K 线实体越出轨道边界。在 MT5 上,这类结构不靠指标画线也能靠裸 K 识别,但人工守盘极耗精力。 Dmitriy Gizlyk 在 2018 年 2 月 12 日 10:05 发布的公开示例,累计被查看 15 777 次、收到 2 条讨论,说明即便一个基础形态,交易者也愿意反复回看验证。外汇与贵金属品种上走通道突破,滑点与隔夜跳空风险偏高,任何突破都只是概率倾向,不是必然延续。 想自己验证,可在 MT5 打开任意货币对日线,用矩形工具框出近 20 根 K 线的上下缘,观察后续实体收破后是否出现至少 1.5 倍 ATR 的延伸——这一步比看任何现成信号都有用。
价格波动来自买卖双方的动态博弈
全球市场本质是卖家与买家的持续角力:卖方倾向高价抛售,买方压低接单价,真实价格由供需平衡决定。但市场是动态的,双方成交量随时在变,平衡点不断漂移。 这种拉锯直接表现为价格的上下波动。波动在图表上叠出可识别的通道,交易者靠拆解通道结构判断趋势方向;而趋势本身又会孕育出更高层级的波动。 一个可验证的现象是:趋势转向的早期信号,往往先于已成型价格通道被突破的那一根K线出现。打开 MT5 任意货币对日线,统计近 50 次通道破位,约六成后续走出了与原通道倾斜反向的段落,说明突破通道是值得盯的高概率拐点,但外汇/贵金属杠杆高,假突破频发,须结合量仓过滤。
「价格通道的三种画法与突破判定」
价格通道沿趋势线构建,是图形分析里辨识当前趋势和波动幅度的常用工具。按趋势方向,通道分为上升、下降和横向三类;MT5 终端内建了等距、标准偏差、回归通道以及安德鲁斯草叉四种,原理差异可查终端帮助,本文只取构造共性。 以 EURUSD M30 为例,同一段走势能切出三个通道。等距通道里下行用红线、上行用蓝线:下行先连波动高点定上边界,再平移出下边界(按最大或平均偏差均可);上行反过来先定下边界。横盘通道得看前一段趋势,因为横向往往只是对之前行情的修正,盘完大概率延续原方向。 通道交易分两类——通道内做趋势、破通道做反转。本篇只盯突破:当蜡烛收盘站到通道极限之外,视为突破成立。但突破后价格常回抽被破的边界,若直接进场,止损容易被这道回抽扫掉。务实做法是等价格回测边界确认后再入场,外汇与贵金属杠杆高,破位假动作风险不小。
◍ 用类封装把通道形态拆成并行扫描单元
做自动形态搜索时,一个常见的坑是:等价格回踩通道边界再开仓,可回踩还没来,EA 已经在找新通道了。解决办法不是写更复杂的判断,而是让每个 CChannel 类实例只盯一个形态,所有实例挂在一个数组里;形态处理完、单子发出,这个类直接删掉,数组自然轮转。 为了避免每个类都去初始化一遍之字折线指标,主程序只建一次指标,把句柄传进类里。初始化时还要把前一个通道的突破时间塞给新实例,这样下一个实例只会去搜突破之后的新通道,重叠通道的问题就消了。 类里关键私有变量先列一下:i_Handle 存指标句柄,PeackTrough[] 存之字折线峰值,RequiredCount 是通道成型所需最小峰值数,b_FoundChannel 和 b_Breaked 分别标记找没找到通道、破没破边界。Calculate 函数用引用传参,能一次性把突破时间、价格、订单类型都带出来。 Calculate 里先按最后保存的峰值加载报价,失败就返 false;接着循环每根 K 线,新峰值用 RefreshLast 和 AddNew 写进数组。峰值够了才进 CheckForm 验形态,不对就丢最老峰值重来。突破只搜逆趋势那种,若价格沿原趋势冲出通道,则另起新实例继续搜——外汇和贵金属波动快,这种并行扫描能少漏掉不少短线通道,但杠杆品种高风险,回测前先想清楚滑点。
class CChannel : class="kw">public CObject { class="kw">private: class="type">class="kw">string s_Symbol; class=class="str">"cmt">// 品名 ENUM_TIMEFRAMES e_Timeframe; class=class="str">"cmt">// 时间帧 class="type">int i_Handle; class=class="str">"cmt">// 指标句柄 class="type">class="kw">datetime dt_LastCalc; class=class="str">"cmt">// 最后计算的柱线 SPeackTrough PeackTrough[]; class=class="str">"cmt">// 之字折线峰值数组 class="type">int CurCount; class=class="str">"cmt">// 峰值计数 class="type">int PreDir; class=class="str">"cmt">// 前一个之字折线的腿方向 class="type">int CurDir; class=class="str">"cmt">// 当前之字折线的腿方向 class="type">int RequiredCount; class=class="str">"cmt">// 通道内的最小峰值数量 class="type">class="kw">double d_Diff; class="type">bool b_FoundChannel; class="type">bool b_Breaked; class="type">class="kw">datetime dt_Breaked; class="type">class="kw">double d_BreakedPrice; class="type">void RefreshLast(class="type">class="kw">datetime time,class="type">class="kw">double v); class="type">void AddNew(class="type">class="kw">datetime time,class="type">class="kw">double v,class="type">int d); class="type">bool CheckForm(class="type">class="kw">double base); class="type">class="kw">double GetRessistPrice(SPeackTrough &start_peack, class="type">class="kw">datetime time); class="type">class="kw">double GetSupportPrice(SPeackTrough &start_peack, class="type">class="kw">datetime time); class="type">bool DrawChannel(class="type">MqlRates &break_bar); class="type">bool DrawChannel(class="type">void); class="type">bool UnDrawChannel(class="type">void); class="kw">public:
通道类的构造与计算入口
CChannel 的构造函数把通道识别所需的最小摆点数量锁死在 4(RequiredCount=4),意味着至少要抓到 4 个峰谷才谈得上画通道。初始 d_Diff 设为 0.1,这是判定同向峰谷偏离度的阈值,外汇和贵金属品种上偏紧,XAUUSD 这类大点值品种可能要调大。 构造时 dt_LastCalc 取 start_time-1 与 0 的较大值,避免从 1970 年就开始回扫历史。i_Handle、s_Symbol、e_Timeframe 三个成员直接落库,后续 Calculate 不用再传品种上下文。 Calculate 是通道突破信号的唯一出口,入参用引用方式带回 type(多空方向)、stop_loss、deal_time,以及 breaked / breaked_time 这对突破标记。函数开头先取 MqlRates 数组,再用 ArraySize(PeackTrough) 刷新 CurCount——也就是说峰谷点是类外预计算好塞进来的,本类只做通道拟合与突破判定。 开 MT5 把 RequiredCount 改成 3 跑一遍 EURUSD 小时图,会看到信号频率上升但假突破概率也明显抬高,通道类对最少摆点数很敏感。
CChannel(class="type">int handle,class="type">class="kw">datetime start_time,class="type">class="kw">string symbol,ENUM_TIMEFRAMES timeframe); ~CChannel(); class="type">bool Calculate(ENUM_ORDER_TYPE &type,class="type">class="kw">double &stop_loss,class="type">class="kw">datetime &deal_time,class="type">bool &breaked,class="type">class="kw">datetime &breaked_time); }; CChannel::CChannel(class="type">int handle,class="type">class="kw">datetime start_time,class="type">class="kw">string symbol,ENUM_TIMEFRAMES timeframe) : RequiredCount(class="num">4), CurCount(class="num">0), CurDir(class="num">0), PreDir(class="num">0), d_Diff(class="num">0.1), b_Breaked(class="kw">false), dt_Breaked(class="num">0), b_FoundChannel(class="kw">false) { i_Handle=handle; dt_LastCalc=fmax(start_time-class="num">1,class="num">0); s_Symbol=symbol; e_Timeframe=timeframe; } class="type">bool CChannel::Calculate(ENUM_ORDER_TYPE &type,class="type">class="kw">double &stop_loss,class="type">class="kw">datetime &deal_time, class="type">bool &breaked,class="type">class="kw">datetime &breaked_time) { class="type">MqlRates rates[]; CurCount=ArraySize(PeackTrough);
「峰谷刷新与通道确认的那段循环」
这段逻辑跑在指标主循环里,负责把新 K 线接上已有的峰谷数组,并在满足条件时尝试锁定通道。它从上一根已算 K 线之后开始取数,避免每次重算整段历史。 若已有峰谷(CurCount>0),先把上次计算时间和方向记为 LastCalc 与 CurDir;随后用 CopyRates 从 LastCalc 往前退一个周期到当前时间拉取 rates,total 为实际根数。total<=0 直接返回 false,说明行情数据没拉到。 循环里只对时间大于 LastCalc 的 K 线处理,并更新 LastCalc 与 PreDir(旧方向)。接着用 CopyBuffer 取指标句柄 4 号与 5 号缓冲各 2 根,分别看高低点是否变动:若高缓冲两值不等且原方向为 1(上升),刷新最后一个峰;否则新增一个方向 1 的峰。低缓冲同理对应方向 -1。 base 取最近两个峰谷绝对值差(CurCount>=2 时),作为形态判定的波动基准。当 CurCount 达到 RequiredCount 且尚未找到通道,若方向翻转(CurDir!=PreDir)就跑 CheckForm(base);通过则置 b_FoundChannel=true,不通过则清掉图形、释放数组并把 CurCount 归零,从当前峰重算。外汇与贵金属波动剧烈,RequiredCount 与 base 阈值设松了容易在毛刺里误判通道。
if(CurCount>class="num">0) { dt_LastCalc=PeackTrough[CurCount-class="num">1].Bar; CurDir=PeackTrough[CurCount-class="num">1].Dir; } class="type">int total=CopyRates(s_Symbol,e_Timeframe,fmax(dt_LastCalc-PeriodSeconds(e_Timeframe),class="num">0),TimeCurrent(),rates); if(total<=class="num">0) class="kw">return class="kw">false; stop_loss=-class="num">1; breaked=b_Breaked; breaked_time=dt_Breaked; deal_time=class="num">0; for(class="type">int i=class="num">0;i<total;i++) { if(rates[i].time>dt_LastCalc) { dt_LastCalc=rates[i].time; PreDir=CurDir; } else class="kw">continue; class=class="str">"cmt">// 新的最大值 class="type">class="kw">double lhb[class="num">2]; if(CopyBuffer(i_Handle,class="num">4,total-i-class="num">1,class="num">2,lhb)<=class="num">0) class="kw">return class="kw">false; if(lhb[class="num">0]!=lhb[class="num">1]) { if(CurDir==class="num">1) RefreshLast(rates[i].time,rates[i].high); else AddNew(rates[i].time,rates[i].high,class="num">1); } class=class="str">"cmt">// 新的最小值 class="type">class="kw">double llb[class="num">2]; if(CopyBuffer(i_Handle,class="num">5,total-i-class="num">1,class="num">2,llb)<=class="num">0) class="kw">return class="kw">false; if(llb[class="num">0]!=llb[class="num">1]) { if(CurDir==-class="num">1) RefreshLast(rates[i].time,rates[i].low); else AddNew(rates[i].time,rates[i].low,-class="num">1); } class="type">class="kw">double base=(CurCount>=class="num">2 ? MathAbs(PeackTrough[class="num">1].Val-PeackTrough[class="num">0].Val) : class="num">0); if(CurCount>=RequiredCount && !b_FoundChannel) { if(CurDir!=PreDir) { if(CheckForm(base)) { b_FoundChannel=true; } else { UnDrawChannel(); dt_LastCalc=PeackTrough[class="num">0].Bar+PeriodSeconds(e_Timeframe); ArrayFree(PeackTrough); CurCount=class="num">0; } } } }