Keltner Channel MQL5 Indicator: ATR Bands and Completed-Bar Alerts
Plot a configurable moving average with upper and lower bands at a selected ATR multiple. Defaults are EMA 20, ATR 10 and 1.5 times ATR. The supplied custom indicator draws three price-chart lines, exposes normalized width in the Data Window and provides completed-bar breakout/return alerts. It contains no companion trading EA or verified performance record.
How the indicator signal works
Calculation and signal conditions
Middle is the selected moving average of the selected price; upper and lower are middle plus/minus the ATR multiplier. Buffer 3 reports (upper minus lower) divided by the absolute middle value times 100, unless the denominator is zero. Breakout alerts compare two completed closes with their own contemporaneous bands. There is no ATR-expansion, spread or account-equity filter; an alert is not an entry order.
Interpretation and limitations
The optional return alert identifies a completed close back inside the channel after the prior close was outside. It can be enabled independently from breakout alerts. The indicator has no exits, stop-loss, trailing, drawdown guard or trade execution. ATR and standard deviation measure different features of prices; neither establishes fewer false breaks without a defined comparison. Forming-bar bands can change, and parameter values do not guarantee a fixed price-coverage percentage.
MQL5 Custom Indicator Code
//+------------------------------------------------------------------+
//| KeltnerChannel.mq5 |
//| Keltner Channel MQL5 Indicator — ATR-Based Volatility Channel |
//| Middle: EMA(20) | Bands: +/- 1.5 x ATR(10) |
//| Educational indicator; no verified strategy backtest supplied. |
//+------------------------------------------------------------------+
#property copyright "Pineify — pineify.app"
#property link "https://pineify.app"
#property version "1.01"
#property strict
//--- Indicator chart properties
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots 4
//--- Plot 1 — Middle EMA band
#property indicator_label1 "KC_Middle"
#property indicator_type1 DRAW_LINE
#property indicator_color1 clrDodgerBlue
#property indicator_style1 STYLE_SOLID
#property indicator_width1 2
//--- Plot 2 — Upper band
#property indicator_label2 "KC_Upper"
#property indicator_type2 DRAW_LINE
#property indicator_color2 clrOrangeRed
#property indicator_style2 STYLE_DOT
#property indicator_width2 1
//--- Plot 3 — Lower band
#property indicator_label3 "KC_Lower"
#property indicator_type3 DRAW_LINE
#property indicator_color3 clrLimeGreen
#property indicator_style3 STYLE_DOT
#property indicator_width3 1
//--- Plot 4 — normalized width exposed without distorting the price scale
#property indicator_label4 "KC_Width"
#property indicator_type4 DRAW_NONE
#property indicator_color4 clrGray
#property indicator_style4 STYLE_SOLID
#property indicator_width4 1
//--- Input parameters: indicator settings
input int InpMAPeriod = 20; // EMA period for middle line
input int InpATRPeriod = 10; // ATR period for band width
input double InpMultiplier = 1.5; // ATR multiplier (upper/lower)
input ENUM_MA_METHOD InpMAMethod = MODE_EMA; // Moving average type
input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; // Price for middle line
//--- Input parameters: alerts
input bool InpAlertOnBreak = true; // Alert on price breaking bands
input bool InpAlertOnReturn = false; // Alert on price returning inside
//--- Indicator buffers
double g_midBuffer[]; // index 0 — middle EMA line
double g_upBuffer[]; // index 1 — upper band
double g_loBuffer[]; // index 2 — lower band
double g_widthBuffer[]; // index 3 — band width (upper - lower)
//--- Handle variables for underlying indicators
int g_maHandle = INVALID_HANDLE;
int g_atrHandle = INVALID_HANDLE;
//--- State for cross-detection alerts
datetime g_lastAlertTime = 0;
//+------------------------------------------------------------------+
//| Indicator initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
if(InpMAPeriod<1 || InpATRPeriod<1 || InpMultiplier<=0) return INIT_PARAMETERS_INCORRECT;
//--- Bind buffers to their indices (INDICATOR_DATA = plot data)
SetIndexBuffer(0, g_midBuffer, INDICATOR_DATA);
SetIndexBuffer(1, g_upBuffer, INDICATOR_DATA);
SetIndexBuffer(2, g_loBuffer, INDICATOR_DATA);
SetIndexBuffer(3, g_widthBuffer, INDICATOR_DATA);
ArraySetAsSeries(g_midBuffer,false); ArraySetAsSeries(g_upBuffer,false);
ArraySetAsSeries(g_loBuffer,false); ArraySetAsSeries(g_widthBuffer,false);
for(int p=0;p<4;p++) PlotIndexSetDouble(p,PLOT_EMPTY_VALUE,EMPTY_VALUE);
//--- Label each plot for the DataWindow tooltip
PlotIndexSetString(0, PLOT_LABEL, "KC_Mid(" + IntegerToString(InpMAPeriod) + ")");
PlotIndexSetString(1, PLOT_LABEL, "KC_Upper(" + IntegerToString(InpMAPeriod) + ")");
PlotIndexSetString(2, PLOT_LABEL, "KC_Lower(" + IntegerToString(InpMAPeriod) + ")");
PlotIndexSetString(3, PLOT_LABEL, "KC_Width");
//--- Set indicator short name and number of decimals
IndicatorSetString(INDICATOR_SHORTNAME,
"KeltnerCh(" + IntegerToString(InpMAPeriod) + "," +
IntegerToString(InpATRPeriod) + "," +
DoubleToString(InpMultiplier, 1) + ")");
IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
//--- Create iMA handle for the middle line
g_maHandle = iMA(_Symbol, _Period, InpMAPeriod, 0, InpMAMethod, InpPrice);
if(g_maHandle == INVALID_HANDLE)
{
Print("FATAL [OnInit]: iMA handle creation failed. Error: ", GetLastError());
return INIT_FAILED;
}
//--- Create iATR handle for band-width calculation
g_atrHandle = iATR(_Symbol, _Period, InpATRPeriod);
if(g_atrHandle == INVALID_HANDLE)
{
Print("FATAL [OnInit]: iATR handle creation failed. Error: ", GetLastError());
return INIT_FAILED;
}
//--- DRAW_BEGIN: skip bars before both indicators have enough data
int drawBegin = MathMax(InpMAPeriod-1, InpATRPeriod);
PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, drawBegin);
PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, drawBegin);
PlotIndexSetInteger(2, PLOT_DRAW_BEGIN, drawBegin);
PlotIndexSetInteger(3, PLOT_DRAW_BEGIN, drawBegin);
Print("Keltner Channel initialised: ", _Symbol, " ", EnumToString(_Period),
" | MA=", InpMAPeriod, " ATR=", InpATRPeriod, " Mult=", InpMultiplier);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Indicator calculation function — fills all four output buffers |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
//--- Minimum bars required: both MA and ATR need warm-up data
int minBars = MathMax(InpMAPeriod, InpATRPeriod) + 2;
if(rates_total < minBars)
return 0;
ArraySetAsSeries(time,false); ArraySetAsSeries(close,false);
int warmup=MathMax(InpMAPeriod-1,InpATRPeriod);
int start;
if(prev_calculated==0 || prev_calculated>rates_total) {
ArrayInitialize(g_midBuffer,EMPTY_VALUE); ArrayInitialize(g_upBuffer,EMPTY_VALUE);
ArrayInitialize(g_loBuffer,EMPTY_VALUE); ArrayInitialize(g_widthBuffer,EMPTY_VALUE);
g_lastAlertTime=0;
start=warmup;
} else start=MathMax(warmup,prev_calculated-1);
// Default chronological arrays match the output buffers.
double maBuf[],atrBuf[];
ArraySetAsSeries(maBuf,false); ArraySetAsSeries(atrBuf,false);
//--- Copy MA and ATR values from their indicator handles
int copiedMA = CopyBuffer(g_maHandle, 0, 0, rates_total, maBuf);
int copiedATR = CopyBuffer(g_atrHandle, 0, 0, rates_total, atrBuf);
if(copiedMA < rates_total || copiedATR < rates_total)
{
Print("WARNING: Buffer copy incomplete. MA=", copiedMA, " ATR=", copiedATR);
return 0;
}
//--- Compute Keltner Channel values bar by bar
for(int i = start; i < rates_total; i++)
{
double ma = maBuf[i];
double atr = atrBuf[i];
g_midBuffer[i] = ma; // middle = EMA
g_upBuffer[i] = ma + (InpMultiplier * atr); // upper band
g_loBuffer[i] = ma - (InpMultiplier * atr); // lower band
g_widthBuffer[i] = ma!=0 ? (g_upBuffer[i]-g_loBuffer[i])/MathAbs(ma)*100.0 : EMPTY_VALUE; // width %
}
//--- Alert on band break (only if enabled and on a new bar)
if((InpAlertOnBreak || InpAlertOnReturn) && prev_calculated > 0 && rates_total>=warmup+3)
{
int last = rates_total - 2; // completed bar; last-1 is the preceding completed bar
if(time[last] != g_lastAlertTime)
{
g_lastAlertTime = time[last];
double closeLast = close[last];
double closePrev = close[last - 1];
//--- Price crossed above upper band
if(InpAlertOnBreak && closeLast > g_upBuffer[last] && closePrev <= g_upBuffer[last - 1])
Alert("KC BreakOUT: ", _Symbol, " closed above upper band at ",
DoubleToString(closeLast, _Digits));
//--- Price crossed below lower band
if(InpAlertOnBreak && closeLast < g_loBuffer[last] && closePrev >= g_loBuffer[last - 1])
Alert("KC BreakDOWN: ", _Symbol, " closed below lower band at ",
DoubleToString(closeLast, _Digits));
//--- Price returned inside the channel (optional alert)
if(InpAlertOnReturn)
{
if(closeLast <= g_upBuffer[last] && closeLast >= g_loBuffer[last])
{
if(closePrev > g_upBuffer[last - 1] || closePrev < g_loBuffer[last - 1])
Alert("KC Return: ", _Symbol, " price re-entered the channel.");
}
}
}
}
return rates_total;
}
//+------------------------------------------------------------------+
//| Indicator deinitialization function — release handles |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_maHandle != INVALID_HANDLE) IndicatorRelease(g_maHandle);
if(g_atrHandle != INVALID_HANDLE) IndicatorRelease(g_atrHandle);
Print("Keltner Channel deinitialised. Reason: ", reason);
}
//+------------------------------------------------------------------+Copy this code into MetaEditor, save it in the MQL5/Indicators folder, and compile with F7.
Change the settings or add your own signal filter
Describe the inputs, buffers, alerts, visual style, or confirmation filter you want. Pineify generates editable MQL5 source code that you can inspect and compile in MetaEditor.
Frequently Asked Questions
Related MQL5 pages
Technical references
Risk and testing note
These educational examples require independent compilation and testing. A compile pass or an illustrative setting does not establish execution correctness or profitability. Any reported performance must be supported by reproducible data and test settings; past results do not predict future returns. Trading involves significant risk of loss. This content is not financial advice.