构建和测试肯特纳通道交易系统·进阶篇
(2/3)· 接上篇基础概念,本文拆解波动率度量与肯特纳通道算法,并给出可复用的 MQL5 系统骨架
自写肯特纳通道的缓冲与句柄初始化
想在 MT5 里手搓一个肯特纳通道,先得把三条线的缓冲区和中轨算法钉死。上面这段声明里,indicator_color2 设成 clrBlue 对应的是中轨标签 "Keltner Middle Line",indicator_color3 是 clrGreen 对应下轨,上轨走的是 2 号缓冲之前的 0 号通道,宽度统一给到 2 像素,肉眼分辨不费劲。 输入参数里 maPeriod 默认 10、multiInp 默认 2.0、isAtr 默认 false,意味着默认用典型价(PRICE_TYPICAL)上的 10 期 EMA 做中轨,通道宽度 = 中轨 ± 非 ATR 均值 × 2.0。把 isAtr 切到 true,才会去拿 iATR(NULL,0,maPeriod) 的句柄,拿不到就 INIT_FAILED 直接崩初始化。 minBars 被定为 maPeriod+1 即至少 11 根 bar 才出数,避免前几根缓冲区空洞。SetIndexBuffer 把 upper/middle/lower 分别绑到 0/1/2 号缓冲,再 ArraySetAsSeries(...,true) 让索引 0 对应最新 bar——这套顺序错了,画线就会左右颠倒。 indValue 里先 CopyBuffer 取中轨那一根,失败就直接 return 不写数组;随后 AVG(h,l,shift) 算通道半宽基准,upper = middle + average*multiInp、lower = middle - average*multiInp。你开 MT5 新建指标把这段贴进 OnInit/OnCalculate 骨架,改 maPeriod=20 就能直观比对默认版和慢速版的收口节奏,外汇与贵金属杠杆高,参数乱调易放大回撤风险。
class="macro">#class="kw">property indicator_width2 class="num">1 class="macro">#class="kw">property indicator_color2 clrBlue class="macro">#class="kw">property indicator_label2 "Keltner Middle Line" class="macro">#class="kw">property indicator_type3 DRAW_LINE class="macro">#class="kw">property indicator_style3 STYLE_SOLID class="macro">#class="kw">property indicator_width3 class="num">2 class="macro">#class="kw">property indicator_color3 clrGreen class="macro">#class="kw">property indicator_label3 "Keltner Lower Band" class="kw">input class="type">int maPeriod = class="num">10; class=class="str">"cmt">// Moving Average Period class="kw">input class="type">class="kw">double multiInp = class="num">2.0; class=class="str">"cmt">// Channel multiplier class="kw">input class="type">bool isAtr = false; class=class="str">"cmt">// ATR class="kw">input ENUM_MA_METHOD maMode = MODE_EMA; class=class="str">"cmt">// Moving Average Mode class="kw">input ENUM_APPLIED_PRICE priceType = PRICE_TYPICAL;class=class="str">"cmt">// Price Type class="type">class="kw">double upper[], middle[], lower[]; class="type">int maHandle, atrHandle; class="kw">static class="type">int minBars = maPeriod + class="num">1; SetIndexBuffer(class="num">0,upper, INDICATOR_DATA); SetIndexBuffer(class="num">1,middle, INDICATOR_DATA); SetIndexBuffer(class="num">2,lower, INDICATOR_DATA); ArraySetAsSeries(upper, true); ArraySetAsSeries(middle, true); ArraySetAsSeries(lower, true); IndicatorSetString(INDICATOR_SHORTNAME,"Custom Keltner Channel " + IntegerToString(maPeriod)); IndicatorSetInteger(INDICATOR_DIGITS,_Digits); maHandle = iMA(NULL, class="num">0, maPeriod, class="num">0, maMode, priceType); if(isAtr) { atrHandle = iATR(NULL, class="num">0, maPeriod); if(atrHandle == INVALID_HANDLE) { Print("Handle Error"); class="kw">return(INIT_FAILED); } } else atrHandle = INVALID_HANDLE; class="type">void indValue(const class="type">class="kw">double& h[], const class="type">class="kw">double& l[], class="type">int shift) { class="type">class="kw">double ma[class="num">1]; if(CopyBuffer(maHandle, class="num">0, shift, class="num">1, ma) <= class="num">0) class="kw">return; middle[shift] = ma[class="num">0]; class="type">class="kw">double average = AVG(h, l, shift); upper[shift] = middle[shift] + average * multiInp; lower[shift] = middle[shift] - average * multiInp; } class="type">class="kw">double AVG(const class="type">class="kw">double& High[],const class="type">class="kw">double& Low[], class="type">int shift) {
「Keltner 通道的 ATR 回退与逐根重算逻辑」
当 atrHandle 无效时,代码直接退化为用 High[i]-Low[i] 的 maPeriod 根 K 线真实波幅求和再平均,相当于手工算 ATR。若 ATR 句柄可用,则 CopyBuffer 取 maPeriod 长度序列,任一拷贝失败立即 return sum(此时 sum 还是 0.0),避免脏数据写入缓冲区。 主计算里 rates_total<=minBars 直接返 0,保证前 minBars 根不画指标。ArraySetAsSeries 把 close/high/low 倒序,使 shift=0 对应最新 Bar,和后续 indValue(high, low, i) 的索引口径一致。 limit 分支值得注意:limit==1 只算最新一根的 indValue 并返回;limit>1 则先把 middle/upper/lower 三个缓冲区填 EMPTY_VALUE,再从 rates_total-minBars 往前循环到 i>=1 全量重算,最后补一根 i=0。这套写法在外汇与贵金属 5 分钟图上重载历史时可能触发多次全量循环,CPU 占用倾向陡增。 指标属性段声明了 3 个 buffer、3 个 plot,上轨为红实线宽 2,标签写死 Keltner Upper Band;中下轨样式在同文件后续 #property 段定义。实盘加载前建议把 maPeriod 与 minBars 打印出来核对,贵金属跳空时 ATR 回退分支的概率会明显上升。
class="type">class="kw">double sum = class="num">0.0; if(atrHandle == INVALID_HANDLE) { for(class="type">int i = shift; i < shift + maPeriod; i++) sum += High[i] - Low[i]; } else { class="type">class="kw">double t[]; ArrayResize(t, maPeriod); ArrayInitialize(t, class="num">0); if(CopyBuffer(atrHandle, class="num">0, shift, maPeriod, t) <= class="num">0) class="kw">return sum; for(class="type">int i = class="num">0; i < maPeriod; i++) sum += t[i]; } class="kw">return sum / maPeriod; } if(rates_total <= minBars) class="kw">return class="num">0; ArraySetAsSeries(close,true); ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); class="type">int limit = rates_total - prev_calculated; if(limit == class="num">0) { } else if(limit == class="num">1) { indValue(high, low, class="num">1); class="kw">return(rates_total); } else if(limit > class="num">1) { ArrayInitialize(middle, EMPTY_VALUE); ArrayInitialize(upper, EMPTY_VALUE); ArrayInitialize(lower, EMPTY_VALUE); limit = rates_total - minBars; for(class="type">int i = limit; i >= class="num">1 && !IsStopped(); i--) indValue(high, low, i); class="kw">return(rates_total); } indValue(high, low, class="num">0); class="kw">return(rates_total); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom_Keltner_Channel.mq5 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_buffers class="num">3 class="macro">#class="kw">property indicator_plots class="num">3 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_style1 STYLE_SOLID class="macro">#class="kw">property indicator_width1 class="num">2 class="macro">#class="kw">property indicator_color1 clrRed class="macro">#class="kw">property indicator_label1 "Keltner Upper Band" class="macro">#class="kw">property indicator_type2 DRAW_LINE class="macro">#class="kw">property indicator_style2 STYLE_SOLID
◍ Keltner 通道的缓冲与句柄初始化
自定义肯特纳通道要画三条线,得先在指标属性里把第 2、3 号绘图的样式钉死:中轨用蓝色细线标 "Keltner Middle Line",下轨用绿色 2 像素实线标 "Keltner Lower Band",上轨属性在邻近代码段定义。这样 MT5 导航树里能直接区分三条带,不会和默认指标撞色。 输入参数给了四个可调点:maPeriod 默认 10 根 K 线、multiInp 通道倍数 2.0、isAtr 开关默认关、maMode 用指数平滑 MODE_EMA、priceType 取典型价。把 multiInp 从 2.0 调到 3.0,通道会变宽,假突破触发概率可能下降但滞后更明显,外汇与贵金属波动大时这种参数敏感性要现场验证。 OnInit 里先 SetIndexBuffer 把 upper/middle/lower 三个数组绑到 0/1/2 号缓冲区,再用 ArraySetAsSeries 设成时间序列(最新柱在 [0])。maHandle 通过 iMA 拿到,isAtr 为真才建 iATR 句柄,建失败直接 INIT_FAILED——这是避免后面 CopyBuffer 读空的典型防呆。 indValue 是每根 K 线算带宽的核心:从 maHandle 拷 1 根到 ma 数组填中轨,再用 AVG(h,l,shift) 算真实波幅类均值,上下轨 = 中轨 ± 均值 × multiInp。minBars 静态设为 maPeriod+1,意味着前 11 根栏(默认参数)不出值,回测时别拿前面几根当信号。
class="macro">#class="kw">property indicator_width2 class="num">1 class="macro">#class="kw">property indicator_color2 clrBlue class="macro">#class="kw">property indicator_label2 "Keltner Middle Line" class="macro">#class="kw">property indicator_type3 DRAW_LINE class="macro">#class="kw">property indicator_style3 STYLE_SOLID class="macro">#class="kw">property indicator_width3 class="num">2 class="macro">#class="kw">property indicator_color3 clrGreen class="macro">#class="kw">property indicator_label3 "Keltner Lower Band" class="kw">input class="type">int maPeriod = class="num">10; class=class="str">"cmt">// Moving Average Period class="kw">input class="type">class="kw">double multiInp = class="num">2.0; class=class="str">"cmt">// Channel multiplier class="kw">input class="type">bool isAtr = false; class=class="str">"cmt">// ATR class="kw">input ENUM_MA_METHOD maMode = MODE_EMA; class=class="str">"cmt">// Moving Average Mode class="kw">input ENUM_APPLIED_PRICE priceType = PRICE_TYPICAL;class=class="str">"cmt">// Price Type class="type">class="kw">double upper[], middle[], lower[]; class="type">int maHandle, atrHandle; class="kw">static class="type">int minBars = maPeriod + class="num">1; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { SetIndexBuffer(class="num">0,upper, INDICATOR_DATA); SetIndexBuffer(class="num">1,middle, INDICATOR_DATA); SetIndexBuffer(class="num">2,lower, INDICATOR_DATA); ArraySetAsSeries(upper, true); ArraySetAsSeries(middle, true); ArraySetAsSeries(lower, true); IndicatorSetString(INDICATOR_SHORTNAME,"Custom Keltner Channel " + IntegerToString(maPeriod)); IndicatorSetInteger(INDICATOR_DIGITS,_Digits); maHandle = iMA(NULL, class="num">0, maPeriod, class="num">0, maMode, priceType); if(isAtr) { atrHandle = iATR(NULL, class="num">0, maPeriod); if(atrHandle == INVALID_HANDLE) { Print("Handle Error"); class="kw">return(INIT_FAILED); } } else atrHandle = INVALID_HANDLE; class="kw">return INIT_SUCCEEDED; } class="type">void indValue(const class="type">class="kw">double& h[], const class="type">class="kw">double& l[], class="type">int shift) { class="type">class="kw">double ma[class="num">1]; if(CopyBuffer(maHandle, class="num">0, shift, class="num">1, ma) <= class="num">0) class="kw">return; middle[shift] = ma[class="num">0]; class="type">class="kw">double average = AVG(h, l, shift); upper[shift] = middle[shift] + average * multiInp; lower[shift] = middle[shift] - average * multiInp; }
均值通道的取数分支与重算边界
自定义指标里计算通道中轨,核心在 AVG 函数:当 atrHandle 无效时,退化为用 High-Low 的 maPeriod 根 K 线振幅求和再除周期;若 ATR 句柄可用,则直接 CopyBuffer 拉取 ATR 序列做平均。两种路径都返回 sum/maPeriod,意味着中轨本质是「近期平均波动幅度」而非收盘价均线,对外汇和贵金属的高波动品种更贴合真实波幅。 OnCalculate 里先用 ArraySetAsSeries 把 close/high/low 倒序,保证 shift=0 是最新柱。limit 的计算区分了三种情况:首跑 limit>1 会清空 middle/upper/lower 三个缓冲并只从 rates_total-minBars 起算;增量 tick 下 limit==1 只补最新一根,避免整段重绘。 实盘验证时把 minBars 设到 50 以上,EURUSD 的 M15 上通道对假突破的过滤概率会明显优于裸 K 进场;但贵金属 XAUUSD 在美盘跳空时仍可能瞬时刺穿上下轨,属高风险场景,需配合止损。 别把 INVALID_HANDLE 当异常 不少人在 atrHandle 未初始化好时直接走 ATR 分支,结果 CopyBuffer 返回 0 让函数 early return 0,中轨整段空白。上面代码把无效句柄当成「用振幅兜底」的前置判断,才是 MT5 里指标不闪退的写法。
class="type">class="kw">double AVG(const class="type">class="kw">double& High[],const class="type">class="kw">double& Low[], class="type">int shift) { class="type">class="kw">double sum = class="num">0.0; if(atrHandle == INVALID_HANDLE) { for(class="type">int i = shift; i < shift + maPeriod; i++) sum += High[i] - Low[i]; } else { class="type">class="kw">double t[]; ArrayResize(t, maPeriod); ArrayInitialize(t, class="num">0); if(CopyBuffer(atrHandle, class="num">0, shift, maPeriod, t) <= class="num">0) class="kw">return sum; for(class="type">int i = class="num">0; i < maPeriod; i++) sum += t[i]; } class="kw">return sum / maPeriod; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { if(rates_total <= minBars) class="kw">return class="num">0; ArraySetAsSeries(close,true); ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); class="type">int limit = rates_total - prev_calculated; if(limit == class="num">0) { } else if(limit == class="num">1) { indValue(high, low, class="num">1); class="kw">return(rates_total); } else if(limit > class="num">1) { ArrayInitialize(middle, EMPTY_VALUE); ArrayInitialize(upper, EMPTY_VALUE); ArrayInitialize(lower, EMPTY_VALUE); limit = rates_total - minBars; for(class="type">int i = limit; i >= class="num">1 && !IsStopped(); i--)
「把肯特纳通道接进EA的实跑参数」
这段代码片段展示了一个EA如何把自定义肯特纳通道指标挂进当前图表并取数。输入参数里 maPeriod=10、multiInp=2.0、maMode=MODE_EMA、priceType=PRICE_TYPICAL,代表通道中轨用10期典型价的EMA,上下轨在中轨基础上乘2倍偏移;slPips=150、tpPips=300 则给出每单硬编码的止损止盈距离(点数)。 通过 iCustom 调用名为 "Custom_Keltner_Channel" 的指示器,再用 CopyBuffer 把0/1/2号缓冲分别拷进 upper/middle/lower 数组,并 ArraySetAsSeries 置为时间倒序,这样 upper[1] 就是上一根K的通道上轨。 入场判断只看两根K:若前一根收盘低于前下轨(prevLastClose<prevLowerValue)且上一根收盘站回下轨上方(lastClose>lowerValue),就触发多头倾向信号。外汇与贵金属杠杆高,这种穿越下轨反抽的胜率随品种波动结构变化,实盘前请在MT5策略测试器用真实点差回测。 顺手把 lotSize=1 改成按净值比例计算,别拿固定手数硬怼黄金,否则一次反向跳空就可能吞掉周盈利。
indValue(high, low, i); class="kw">return(rates_total); } indValue(high, low, class="num">0); class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <trade/trade.mqh> class="kw">input class="type">int maPeriod = class="num">10; class=class="str">"cmt">// Moving Average Period class="kw">input class="type">class="kw">double multiInp = class="num">2.0; class=class="str">"cmt">// Channel multiplier class="kw">input class="type">bool isAtr = false; class=class="str">"cmt">// ATR class="kw">input ENUM_MA_METHOD maMode = MODE_EMA; class=class="str">"cmt">// Moving Average Mode class="kw">input ENUM_APPLIED_PRICE priceType = PRICE_TYPICAL;class=class="str">"cmt">// Price Type class="kw">input class="type">class="kw">double lotSize=class="num">1; class="kw">input class="type">class="kw">double slPips = class="num">150; class="kw">input class="type">class="kw">double tpPips = class="num">300; class="type">int keltner; class="type">int barsTotal; CTrade trade; barsTotal=iBars(_Symbol,PERIOD_CURRENT); keltner = iCustom(_Symbol,PERIOD_CURRENT,"Custom_Keltner_Channel",maPeriod,multiInp, isAtr, maMode, priceType); Print("EA is removed"); class="type">int bars=iBars(_Symbol,PERIOD_CURRENT); if(barsTotal < bars) barsTotal=bars; class="type">class="kw">double upper[], middle[], lower[]; CopyBuffer(keltner,class="num">0,class="num">0,class="num">3,upper); CopyBuffer(keltner,class="num">1,class="num">0,class="num">3,middle); CopyBuffer(keltner,class="num">2,class="num">0,class="num">3,lower); ArraySetAsSeries(upper,true); ArraySetAsSeries(middle,true); ArraySetAsSeries(lower,true); class="type">class="kw">double prevUpperValue = NormalizeDouble(upper[class="num">2], class="num">5); class="type">class="kw">double prevMiddleValue = NormalizeDouble(middle[class="num">2], class="num">5); class="type">class="kw">double prevLowerValue = NormalizeDouble(lower[class="num">2], class="num">5); class="type">class="kw">double upperValue = NormalizeDouble(upper[class="num">1], class="num">5); class="type">class="kw">double middleValue = NormalizeDouble(middle[class="num">1], class="num">5); class="type">class="kw">double lowerValue = NormalizeDouble(lower[class="num">1], class="num">5); class="type">class="kw">double lastClose=iClose(_Symbol,PERIOD_CURRENT,class="num">1); class="type">class="kw">double prevLastClose=iClose(_Symbol,PERIOD_CURRENT,class="num">2); if(prevLastClose<prevLowerValue && lastClose>lowerValue) {
◍ 凯尔特纳通道突破的下单与参数骨架
当上一根收盘在通道下轨下方、当前收盘回抽跌破下轨时,脚本会取卖价并在下轨减 slPips 点挂买止损、中轨加 tpPips 点挂止盈,调 Buy 进场;若上一根收在上轨外、当前回破上轨,则反手用卖价、上轨加 slPips 点作止损、中轨减 tpPips 点作止盈去 Sell。 下方头部集中暴露了可调变量:均线周期 10、通道倍数 2.0、ATR 开关 false、均线类型 EMA、价格类型 Typical,仓位 1 手,初始止损 150 点、止盈 300 点。外汇与贵金属杠杆高,点值随品种跳动,150/300 点若直接套 XAUUSD 可能吃掉远超预期的保证金,上机前先按品种重算。 OnInit 里用 iCustom 挂了名为 Custom_Keltner_Channel 的指标句柄,OnTick 只在新柱出现时重读近 3 根 upper/middle/lower 缓冲——这意味着逻辑按根 bar 触发一次,不是逐 tick 重算,回测时滑点偏差会比肉眼想的小。
class="type">class="kw">double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); class="type">class="kw">double slVal = lowerValue - slPips*_Point; class="type">class="kw">double tpVal = middleValue + tpPips*_Point; trade.Buy(lotSize,_Symbol,ask,slVal,tpVal); } if(prevLastClose>prevUpperValue && lastClose<upperValue) { class="type">class="kw">double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); class="type">class="kw">double slVal = upperValue + slPips*_Point; class="type">class="kw">double tpVal = middleValue - tpPips*_Point; trade.Sell(lotSize,_Symbol,bid,upperValue,tpVal); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Keltner_Trading_System.mq5 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <trade/trade.mqh> class="kw">input class="type">int maPeriod = class="num">10; class=class="str">"cmt">// Moving Average Period class="kw">input class="type">class="kw">double multiInp = class="num">2.0; class=class="str">"cmt">// Channel multiplier class="kw">input class="type">bool isAtr = false; class=class="str">"cmt">// ATR class="kw">input ENUM_MA_METHOD maMode = MODE_EMA; class=class="str">"cmt">// Moving Average Mode class="kw">input ENUM_APPLIED_PRICE priceType = PRICE_TYPICAL;class=class="str">"cmt">// Price Type class="kw">input class="type">class="kw">double lotSize=class="num">1; class="kw">input class="type">class="kw">double slPips = class="num">150; class="kw">input class="type">class="kw">double tpPips = class="num">300; class="type">int keltner; class="type">int barsTotal; CTrade trade; class="type">int OnInit() { barsTotal=iBars(_Symbol,PERIOD_CURRENT); keltner = iCustom(_Symbol,PERIOD_CURRENT,"Custom_Keltner_Channel",maPeriod,multiInp, isAtr, maMode, priceType); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { Print("EA is removed"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class="type">int bars=iBars(_Symbol,PERIOD_CURRENT); if(barsTotal < bars) { barsTotal=bars; class="type">class="kw">double upper[], middle[], lower[]; CopyBuffer(keltner,class="num">0,class="num">0,class="num">3,upper); CopyBuffer(keltner,class="num">1,class="num">0,class="num">3,middle);