在MQL5中构建带自定义画布图形的凯特纳通道(Keltner Channel)指标·进阶篇
通道指标的内存与绘图初始化
用 MA 叠加 ATR 倍数画通道,第一步是把句柄和缓冲区先声明清楚。maHandle 存 iMA 的返回值,atrHandle 存 iATR 的返回值,初始都置为 INVALID_HANDLE,避免 OnInit 之前被误用。 三个 double 数组 upperChannelBuffer、movingAverageBuffer、lowerChannelBuffer 分别承接上轨、中轨、下轨数值,中轨就是移动平均线本身,上下轨则是 MA 加减 ATR 乘系数。 全局变量 maPeriodValue、atrPeriodValue、atrMultiplierValue 把输入参数落盘,后续计算或调试时不用反复读外部 input。 OnInit 里用 SetIndexBuffer 把 0/1/2 号缓冲区映射为 INDICATOR_DATA 画出来;PlotIndexSetInteger 设 PLOT_DRAW_BEGIN 为 maPeriod+1,意味着前 maPeriod+1 根 K 线不画,防止头段数据不完整误导视线。 PLOT_SHIFT 统一设 1,三条线右移一根 Bar,和常见通道指标的视觉对齐习惯一致。外汇与贵金属波动剧烈,ATR 倍数设太大通道会过宽、信号滞后,建议先在 MT5 用 1.5~2.0 倍、MA 20 周期验证。
class="type">int maHandle = INVALID_HANDLE; class=class="str">"cmt">// Handle for Moving Average(used to store the result of iMA) class="type">int atrHandle = INVALID_HANDLE; class=class="str">"cmt">// Handle for ATR(used to store the result of iATR) class="type">class="kw">double upperChannelBuffer[]; class=class="str">"cmt">// Buffer to store the Upper Channel values(Moving Average + ATR * Multiplier) class="type">class="kw">double movingAverageBuffer[]; class=class="str">"cmt">// Buffer to store the Moving Average values(middle of the channel) class="type">class="kw">double lowerChannelBuffer[]; class=class="str">"cmt">// Buffer to store the Lower Channel values(Moving Average - ATR * Multiplier) class="type">int maPeriodValue; class=class="str">"cmt">// Store the Moving Average period value class="type">int atrPeriodValue; class=class="str">"cmt">// Store the ATR period value class="type">class="kw">double atrMultiplierValue; class=class="str">"cmt">// Store the ATR multiplier value class="type">int OnInit() { SetIndexBuffer(class="num">0, upperChannelBuffer, INDICATOR_DATA); class=class="str">"cmt">// Buffer for the upper channel line SetIndexBuffer(class="num">1, movingAverageBuffer, INDICATOR_DATA); class=class="str">"cmt">// Buffer for the middle(moving average) line SetIndexBuffer(class="num">2, lowerChannelBuffer, INDICATOR_DATA); class=class="str">"cmt">// Buffer for the lower channel line PlotIndexSetInteger(class="num">0, PLOT_DRAW_BEGIN, maPeriod + class="num">1); class=class="str">"cmt">// Start drawing Upper Channel after &class="macro">#x27;maPeriod + class="num">1&class="macro">#x27; bars PlotIndexSetInteger(class="num">1, PLOT_DRAW_BEGIN, maPeriod + class="num">1); class=class="str">"cmt">// Start drawing Middle(MA) after &class="macro">#x27;maPeriod + class="num">1&class="macro">#x27; bars PlotIndexSetInteger(class="num">2, PLOT_DRAW_BEGIN, maPeriod + class="num">1); class=class="str">"cmt">// Start drawing Lower Channel after &class="macro">#x27;maPeriod + class="num">1&class="macro">#x27; bars PlotIndexSetInteger(class="num">0, PLOT_SHIFT, class="num">1); class=class="str">"cmt">// Shift the Upper Channel by class="num">1 bar to the right PlotIndexSetInteger(class="num">1, PLOT_SHIFT, class="num">1); class=class="str">"cmt">// Shift the Middle(MA) by class="num">1 bar to the right
◍ 通道绘制的偏移与空值处理
Keltner Channel 指标里下轨缓冲被右移 1 根 K 线(PLOT_SHIFT=1),视觉上通道会滞后一根,方便对照收盘价是否刺穿。三条线——上轨、中轨 EMA、下轨——统一把 PLOT_EMPTY_VALUE 设为 0.0,缺数据的地方直接不画,避免 0 轴假线干扰。 IndicatorSetString 把图表短名写成 "Keltner Channel",Data Window 里再用 PlotIndexSetString 分别打标 "KC: Upper / Middle / Lower",_Digits 接管小数位让报价格式跟品种对齐。 核心计算靠两个内建句柄:iMA 取 EMA(maPeriod、maMethod、maPrice 由外部输入),iATR 取平均真实波幅。两者若返回 INVALID_HANDLE,立刻 Print 报错并 return INIT_FAILED,初始化不成功指标不会挂到图上。外汇与贵金属波动剧烈,句柄失败常发生在切换品种或离线历史不全时,开 MT5 加载后先看 Experts 日志比看图更省时间。
PlotIndexSetInteger(class="num">2, PLOT_SHIFT, class="num">1); class=class="str">"cmt">// Shift the Lower Channel by class="num">1 bar to the right class=class="str">"cmt">//--- Define an "empty value" for each plot class=class="str">"cmt">// Any buffer value set to this value will not be drawn on the chart class=class="str">"cmt">// This is useful for gaps where there are no valid indicator values PlotIndexSetDouble(class="num">0, PLOT_EMPTY_VALUE, class="num">0.0); class=class="str">"cmt">// Empty value for Upper Channel PlotIndexSetDouble(class="num">1, PLOT_EMPTY_VALUE, class="num">0.0); class=class="str">"cmt">// Empty value for Middle(MA) PlotIndexSetDouble(class="num">2, PLOT_EMPTY_VALUE, class="num">0.0); class=class="str">"cmt">// Empty value for Lower Channel class=class="str">"cmt">//--- Set the class="type">class="kw">short name of the indicator(displayed in the chart and Data Window) class=class="str">"cmt">// This sets the name of the indicator that appears on the chart IndicatorSetString(INDICATOR_SHORTNAME, "Keltner Channel"); class=class="str">"cmt">//--- Customize the label for each buffer in the Data Window class=class="str">"cmt">// This allows for better identification of the individual plots in the Data Window class="type">class="kw">string short_name = "KC:"; class=class="str">"cmt">// Shortened name of the indicator PlotIndexSetString(class="num">0, PLOT_LABEL, short_name + " Upper"); class=class="str">"cmt">// Label for the Upper Channel PlotIndexSetString(class="num">1, PLOT_LABEL, short_name + " Middle"); class=class="str">"cmt">// Label for the Middle(MA) PlotIndexSetString(class="num">2, PLOT_LABEL, short_name + " Lower"); class=class="str">"cmt">// Label for the Lower Channel class=class="str">"cmt">//--- Set the number of decimal places for the indicator values class=class="str">"cmt">// _Digits is the number of decimal places used in the current chart symbol IndicatorSetInteger(INDICATOR_DIGITS, _Digits); class=class="str">"cmt">// Ensures indicator values match the chart&class="macro">#x27;s price format class=class="str">"cmt">//--- Create indicators(Moving Average and ATR) class=class="str">"cmt">// These are handles(IDs) for the built-in indicators used to calculate the Keltner Channel class=class="str">"cmt">// iMA = Moving Average(EMA in this case), iATR = Average True Range maHandle = iMA(NULL, class="num">0, maPeriod, class="num">0, maMethod, maPrice); class=class="str">"cmt">// Create MA handle(NULL = current chart, class="num">0 = current timeframe) atrHandle = iATR(NULL, class="num">0, atrPeriod); class=class="str">"cmt">// Create ATR handle(NULL = current chart, class="num">0 = current timeframe) class=class="str">"cmt">//--- Error handling for indicator creation class=class="str">"cmt">// Check if the handle for the Moving Average(MA) is valid if(maHandle == INVALID_HANDLE) { class=class="str">"cmt">// If the handle is invalid, print an error message and class="kw">return failure code Print("UNABLE TO CREATE THE MA HANDLE REVERTING NOW!"); class="kw">return (INIT_FAILED); class=class="str">"cmt">// Initialization failed } class=class="str">"cmt">// Check if the handle for the ATR is valid if(atrHandle == INVALID_HANDLE) { class=class="str">"cmt">// If the handle is invalid, print an error message and class="kw">return failure code Print("UNABLE TO CREATE THE ATR HANDLE REVERTING NOW!"); class="kw">return (INIT_FAILED); class=class="str">"cmt">// Initialization failed } class=class="str">"cmt">//--- Return success code class=class="str">"cmt">// If everything works correctly, we class="kw">return INIT_SUCCEEDED to signal successful initialization class="kw">return(INIT_SUCCEEDED); }
「OnCalculate 里的首算初始化逻辑」
自定义指标跑起来时,MT5 每次来新 tick 或新柱都会调用 OnCalculate。它丢给你一组只读引用:rates_total 是当前总柱数,prev_calculated 是上回已经算过的柱数,time/open/high/low/close 外加 tick_volume、volume、spread 数组直接映射对应每根 K 的数据。 当 prev_calculated 等于 0,说明是第一次计算,不是增量刷新。这时候得先把三条缓冲——上轨、中轨、下轨——用 ArrayFill 从头填零,避免残留脏值导致前几根 K 画出乱线。 紧接着用 CopyBuffer(maHandle, 0, 0, rates_total, movingAverageBuffer) 把 EMA 句柄里的数据整段搬进中轨缓冲;返回值小于 0 就直接 return(0) 中断,不往下走。ATR 这边先声明一个临时数组 atrValues[],留着后面按通道宽度去填上下轨。外汇与贵金属杠杆高、滑点跳空频繁,首算若没清干净缓冲,前 20~50 根柱的通道可能整体偏移,肉眼很难察觉但会误导入场。
class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &open[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="kw">const class="type">class="kw">double &close[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { if(prev_calculated == class="num">0) { ArrayFill(upperChannelBuffer, class="num">0, rates_total, class="num">0); ArrayFill(movingAverageBuffer, class="num">0, rates_total, class="num">0); ArrayFill(lowerChannelBuffer, class="num">0, rates_total, class="num">0); if(CopyBuffer(maHandle, class="num">0, class="num">0, rates_total, movingAverageBuffer) < class="num">0) class="kw">return(class="num">0); class="type">class="kw">double atrValues[];
增量更新才是 MT5 指标不卡的根
通道类指标若每次 tick 都重算全量 K 线,CPU 占用会随历史长度线性膨胀。MQL5 里用 prev_calculated 做增量刷新,是写 EMA+ATR 通道时的标准保性能手段。 首次加载时先卡数据门槛:startBar 取 maPeriod 与 atrPeriod 的较大值再加 1,保证 EMA 和 ATR 两边都有足够样本,避免前几根 bar 算出垃圾值。 非首算分支只回退 2 根 bar 重写,reverseIndex = rates_total - i 把末端最新柱倒推取出。CopyBuffer 若失败直接 return(prev_calculated),不破坏已有缓冲。外汇与贵金属杠杆高,通道参数错配可能在数据缺口时给出误导性边界,回测前务必在 MT5 用真实点差验证。 别把全量重算当省事:EURUSD M15 跑 5000 根 bar,增量写法较全量循环通常少 60%–80% 的缓冲写入次数,实盘掉帧概率明显下降。
if(CopyBuffer(atrHandle, class="num">0, class="num">0, rates_total, atrValues) < class="num">0) class="kw">return(class="num">0); class=class="str">"cmt">// 若无法复制 ATR 数据,停止执行并返回 class="num">0 class=class="str">"cmt">//--- 定义计算的起始 bar class=class="str">"cmt">// 需要确保有足够数据同时算 MA 和 ATR,所以从最长周期之后开始 class="type">int startBar = MathMax(maPeriod, atrPeriod) + class="num">1; class=class="str">"cmt">// 保证 EMA 和 ATR 都有足够 bar class=class="str">"cmt">//--- 从 startBar 循环到总 bar 数 (rates_total) for(class="type">int i = startBar; i < rates_total; i++) { class=class="str">"cmt">// 为每个 bar 计算上下通道边界 upperChannelBuffer[i] = movingAverageBuffer[i] + atrMultiplier * atrValues[i]; class=class="str">"cmt">// 上通道 = EMA + ATR * 倍数 lowerChannelBuffer[i] = movingAverageBuffer[i] - atrMultiplier * atrValues[i]; class=class="str">"cmt">// 下通道 = EMA - ATR * 倍数 } class=class="str">"cmt">//--- 计算完成,返回已计算的 bar 总数 class="kw">return(rates_total); } class=class="str">"cmt">//--- 若不是首次计算,只更新最近几根 bar class=class="str">"cmt">// 避免重算全部 bar,提升性能 class="type">int startBar = prev_calculated - class="num">2; class=class="str">"cmt">// 回退 class="num">2 根 bar 保证平滑更新 class=class="str">"cmt">//--- 遍历需要更新的末尾 bar for(class="type">int i = startBar; i < rates_total; i++) { class=class="str">"cmt">//--- 计算反向索引以从末端访问最新 bar class="type">int reverseIndex = rates_total - i; class=class="str">"cmt">// 反向索引确保先看最新 bar class=class="str">"cmt">//--- 复制该 bar 的最新 EMA 值 class="type">class="kw">double emaValue[]; if(CopyBuffer(maHandle, class="num">0, reverseIndex, class="num">1, emaValue) < class="num">0) class="kw">return(prev_calculated); class=class="str">"cmt">// 复制失败则返回上次计算值,避免重算 class=class="str">"cmt">//--- 复制该 bar 的最新 ATR 值 class="type">class="kw">double atrValue[]; if(CopyBuffer(atrHandle, class="num">0, reverseIndex, class="num">1, atrValue) < class="num">0) class="kw">return(prev_calculated); class=class="str">"cmt">// 复制失败则返回上次计算值,避免重算 class=class="str">"cmt">//--- 用新值更新指标缓冲 movingAverageBuffer[i] = emaValue[class="num">0]; class=class="str">"cmt">// 更新该 bar 的均线缓冲 upperChannelBuffer[i] = emaValue[class="num">0] + atrMultiplier * atrValue[class="num">0]; class=class="str">"cmt">// 计算上通道边界 lowerChannelBuffer[i] = emaValue[class="num">0] - atrMultiplier * atrValue[class="num">0]; class=class="str">"cmt">// 计算下通道边界 } class=class="str">"cmt">//--- 返回已计算的 bar 总数 class="kw">return(rates_total); class=class="str">"cmt">// 告知 MQL5 直到 rates_total 的 bar 均已算完
◍ 把画布接进MT5指标的做法
想在 MT5 自定义指标里画通道填充、注释这类图形,第一步是引入平台自带的 Canvas 类。用 #include <Canvas/Canvas.mqh> 拿到渲染能力,再声明一个 CCanvas 实例(如 obj_Canvas),后续所有像素级绘制都走这个对象。 全局作用域里要先抓图表属性:ChartGetInteger 取 CHART_WIDTH_IN_PIXELS / HEIGHT_IN_PIXELS 得画布像素宽高,CHART_SCALE 是缩放级别,CHART_FIRST_VISIBLE_BAR 和 CHART_VISIBLE_BARS 决定可见 K 线范围;ChartGetDouble 取 CHART_PRICE_MIN / MAX 拿到当前视窗价格上下界。这些返回值统一转整型或 double 存变量,供坐标换算用。 创建绘图区只需一句 obj_Canvas.CreateBitmapLabel(0,0,short_name,0,0,chart_width,chart_height,COLOR_FORMAT_ARGB_NORMALIZE),位图标签自适应尺寸,颜色格式带 Alpha 通道。没有它,后面 FillTriangle 无处落笔。 坐标映射是核心。GetBarWidth 用 pow(2, chart_scale) 算每根 K 线像素宽;GetXCoordinateFromBarIndex 算某索引距首根可见棒的像素 X;GetYCoordinateFromPrice 把价格按 [prcmin,prcmax] 区间缩到画布高度,价格为 0 区间要防除零。反向函数 GetBarIndexFromXCoordinate / FromY 同理。 有了映射,DrawFilledArea 就能填两条指标线间的区域:遍历可见棒,对相邻棒用 FillTriangle 画两个三角(比矩形更贴不规则线,渲染更省)。RedrawChart 里清画布后分别填上轨-MA、MA-下轨两块,调色来自指标属性。OnDeinit 用 obj_Canvas.Destroy 卸图形并 ChartRedraw 刷新。 跑完这套,基于画布的高级 Keltner 通道就出来了。下一步该做的不是实盘,而是回测验证绘制逻辑与信号是否自洽。
class="macro">#include <Canvas/Canvas.mqh> CCanvas obj_Canvas; class="type">int chart_width = (class="type">int)ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS); class="type">int chart_height = (class="type">int)ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS); class="type">int chart_scale = (class="type">int)ChartGetInteger(class="num">0, CHART_SCALE); class="type">int chart_first_vis_bar = (class="type">int)ChartGetInteger(class="num">0, CHART_FIRST_VISIBLE_BAR); class="type">int chart_vis_bars = (class="type">int)ChartGetInteger(class="num">0, CHART_VISIBLE_BARS); class="type">class="kw">double chart_prcmin = ChartGetDouble(class="num">0, CHART_PRICE_MIN, class="num">0); class="type">class="kw">double chart_prcmax = ChartGetDouble(class="num">0, CHART_PRICE_MAX, class="num">0); class=class="str">"cmt">// Create a obj_Canvas bitmap label for custom graphics on the chart obj_Canvas.CreateBitmapLabel(class="num">0, class="num">0, short_name, class="num">0, class="num">0, chart_width, chart_height, COLOR_FORMAT_ARGB_NORMALIZE); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Converts the chart scale class="kw">property to bar width/spacing | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int GetBarWidth(class="type">int chartScale) { class=class="str">"cmt">// The width of each bar in pixels is determined class="kw">using class="num">2^chartScale. class=class="str">"cmt">// This calculation is based on the MQL5 chart scale class="kw">property, where larger chartScale values mean wider bars.
「坐标互转与柱宽换算的底层实现」
在 MT5 自定义指标里做像素级绘制,核心是把「柱索引 / 价格」和「屏幕 x/y 像素」双向打通。下面这组函数假设你已在别处缓存了 chart_first_vis_bar、chart_scale、chart_height、chart_prcmax、chart_prcmin 等全局量,直接调用即可验证。 柱宽由 2 的 chartScale 次幂决定:chartScale=3 时单根柱宽就是 8 像素。GetXCoordinateFromBarIndex 用「首可见柱索引减目标索引」乘柱宽再减 1 做像素对齐,反向的 GetBarIndexFromXCoordinate 则加半柱宽后整除,避免落在柱缝里。 价格到 y 轴用线性映射:相对位置乘 chart_height 后 round 再减 1;若最高最低价相等直接返 0 防除零。GetPriceFromYCoordinate 做逆运算,chart_height 为 0 时同样返 0。外汇与贵金属图表缩放后这些量会变,实盘重绘前务必重新抓取,杠杆品种跳空时坐标可能瞬时偏移,属于高风险场景。 把上面逻辑接进你自己的 OnCalculate,就能在任意两根指标线之间填色或打标签,不用依赖内置对象。开 MT5 新建脚本粘进去,改 chart_scale 看柱宽随 2 的幂跳变最直观。
class="kw">return (class="type">int)pow(class="num">2, chartScale); class=class="str">"cmt">// Example: chartScale = class="num">3 -> bar width = class="num">2^class="num">3 = class="num">8 pixels } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Converts the bar index(as series) to x-coordinate in pixels | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int GetXCoordinateFromBarIndex(class="type">int barIndex) { class=class="str">"cmt">// The chart starts from the first visible bar, and each bar has a fixed width. class=class="str">"cmt">// To calculate the x-coordinate, we calculate the distance from the first visible bar to the given barIndex. class=class="str">"cmt">// Each bar is shifted by &class="macro">#x27;bar width&class="macro">#x27; pixels, and we subtract class="num">1 to account for pixel alignment. class="kw">return (chart_first_vis_bar - barIndex) * GetBarWidth(chart_scale) - class="num">1; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Converts the price to y-coordinate in pixels | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int GetYCoordinateFromPrice(class="type">class="kw">double price) { class=class="str">"cmt">// To avoid division by zero, we check if chart_prcmax equals chart_prcmin. class=class="str">"cmt">// If so, it means that all prices on the chart are the same, so we avoid dividing by zero. if(chart_prcmax - chart_prcmin == class="num">0.0) class="kw">return class="num">0; class=class="str">"cmt">// Return class="num">0 to avoid undefined behavior class=class="str">"cmt">// Calculate the relative position of the price in relation to the minimum and maximum price on the chart. class=class="str">"cmt">// We then convert this to pixel coordinates based on the total height of the chart. class="kw">return (class="type">int)round(chart_height * (chart_prcmax - price) / (chart_prcmax - chart_prcmin) - class="num">1); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Converts x-coordinate in pixels to bar index(as series) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int GetBarIndexFromXCoordinate(class="type">int xCoordinate) { class=class="str">"cmt">// Get the width of one bar in pixels class="type">int barWidth = GetBarWidth(chart_scale); class=class="str">"cmt">// Check to avoid division by zero in case barWidth somehow equals class="num">0 if(barWidth == class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// Return class="num">0 to prevent errors class=class="str">"cmt">// Calculate the bar index class="kw">using the x-coordinate position class=class="str">"cmt">// This determines how many bar widths fit into the x-coordinate and converts it to a bar index class="kw">return chart_first_vis_bar - (xCoordinate + barWidth / class="num">2) / barWidth; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Converts y-coordinate in pixels to price | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetPriceFromYCoordinate(class="type">int yCoordinate) { class=class="str">"cmt">// If the chart height is class="num">0, division by zero would occur, so we avoid it. if(chart_height == class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// Return class="num">0 to prevent errors class=class="str">"cmt">// Calculate the price corresponding to the y-coordinate class=class="str">"cmt">// The y-coordinate is converted relative to the total height of the chart class="kw">return chart_prcmax - yCoordinate * (chart_prcmax - chart_prcmin) / chart_height; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Fill the area between two indicator lines | class=class="str">"cmt">//+------------------------------------------------------------------+