Keltner Channels look simple on a chart: a middle line with a band above and a band below. The trouble starts when you try to reproduce one in code. You find that ta.kc gives slightly different bands from the version you wrote by hand, that the "ATR length" setting you saw in another script has no counterpart in the built-in, and that two published indicators with the same name draw visibly different channels. None of these are bugs. They come from the fact that "Keltner Channel" names a family of indicators, and the Pine Script built-in implements one specific member of it.
This guide builds Keltner Channels in Pine Script v6 three ways: with the built-in ta.kc, by hand so you control every part, and combined with Bollinger Bands to detect when one sits inside the other. Along the way it explains what each part of the calculation does, why the width behaves the way it does, and which choices change the result. The code is complete and written for v6, and the final section covers alerts.
What a Keltner Channel actually measures
A Keltner Channel has three lines. The middle line, called the basis, is a moving average of price. The upper and lower lines sit a fixed number of volatility units above and below it. The volatility unit is a measure of bar range, usually the average true range, and the number of units is the multiplier.
upper = basis + multiplier * range
lower = basis - multiplier * rangeThe original version, published by Chester Keltner in 1960, used a simple moving average of the typical price with a ten-period average of the high-to-low range. The version most charting platforms ship today is the one popularized later by Linda Raschke, which swaps in an exponential moving average for the basis and the average true range for the width. When someone says "Keltner Channels" today, they almost always mean this second version, and that is the one the Pine Script built-in follows.
The important point for a coder is that the three choices are independent. You can pick any moving average for the basis, any range measure for the width, and any multiplier. Every published variation is a different combination of those three, which explains why two indicators with the same name disagree on the same chart.
Keltner Channels versus Bollinger Bands
The two indicators are often confused because they look alike: a smoothed middle line with two outer bands. The difference is what drives the width.
Bollinger Bands use the standard deviation of closing prices. Standard deviation looks only at how far closes have strayed from their own average, so the bands react quickly to a single large close. A big move expands them immediately, and a run of tightly packed closes contracts them fast.
Keltner Channels use a range measure built from the high, low, and previous close. Because the range is itself smoothed, the channel changes shape gradually. A single wide bar nudges the width instead of jerking it. In practice this makes Keltner Channels the calmer of the two, which is why they are often used as a stable reference that the more reactive Bollinger Bands move in and out of.
| Keltner Channels | Bollinger Bands | |
|---|---|---|
| Basis | Exponential moving average (in the Pine built-in) | Simple moving average |
| Width driver | Smoothed true range | Standard deviation of close |
| Reaction to one large bar | Gradual | Immediate |
| Typical multiplier | 1.5 to 2 | 2 |
If you want the Bollinger side of this comparison in detail, Bollinger Bands in Pine Script covers its inputs and common mistakes.
The built-in: ta.kc
Pine Script v6 includes ta.kc, which returns the basis, upper, and lower lines as a tuple. Its arguments are the source series, the length, the multiplier, and an optional flag that chooses whether the range is the true range or the plain high-low range. The flag defaults to true range.
//@version=6
indicator("Keltner Channels (built-in)", overlay = true)
length = input.int(20, "Length", minval = 1)
mult = input.float(2.0, "Multiplier", minval = 0.1, step = 0.1)
useTR = input.bool(true, "Use true range")
[basis, upper, lower] = ta.kc(close, length, mult, useTR)
plot(basis, "Basis", color = color.orange)
u = plot(upper, "Upper", color = color.teal)
l = plot(lower, "Lower", color = color.teal)
fill(u, l, color = color.new(color.teal, 90))A few details are worth knowing. The call must be made on every bar, at the top level of the script, because ta.kc keeps internal state across bars. Putting it inside an if block produces inconsistent values, the same trap that affects other ta.* functions. The tuple on the left must have exactly three names, matching the three returned series. And the fill call needs the plot handles u and l, which is why the upper and lower plots are assigned to variables.
There is also a companion function, ta.kcw, which returns the channel width as a single series. It is useful when you want to compare how wide the channel is now against its own history, for example to see whether volatility is unusually compressed. Width is measured relative to the basis, so it can be compared across different price levels.
The two settings that matter most are length and multiplier. Length controls how smooth the basis and the range are. A shorter length hugs price and widens or narrows quickly. A longer length produces a steadier channel that lags more. The multiplier scales the width. Neither has a universally correct value, and any default you see, 20 and 2 being the common pair, is a convention rather than a finding. Choose values deliberately, and check them against your own symbol and timeframe.
Building it by hand
The built-in is convenient, but it fixes three decisions for you. If you want a different moving average, a separate length for the volatility measure, or a different range source, you need to write the calculation yourself. It is also the best way to understand what the function does.
//@version=6
indicator("Keltner Channels (manual)", overlay = true)
length = input.int(20, "Basis length", minval = 1)
atrLen = input.int(10, "ATR length", minval = 1)
mult = input.float(2.0, "Multiplier", minval = 0.1, step = 0.1)
maType = input.string("EMA", "Basis type", options = ["EMA", "SMA", "WMA"])
basis = switch maType
"SMA" => ta.sma(close, length)
"WMA" => ta.wma(close, length)
=> ta.ema(close, length)
band = ta.atr(atrLen)
upper = basis + mult * band
lower = basis - mult * band
plot(basis, "Basis", color = color.orange)
u = plot(upper, "Upper", color = color.teal)
l = plot(lower, "Lower", color = color.teal)
fill(u, l, color = color.new(color.teal, 90))Here the basis type is a selectable input, and the width comes from ta.atr, which has its own length. This gives you the common two-length layout seen in many published scripts, where the basis uses 20 and the ATR uses 10.
Notice the switch expression. It returns a value from whichever branch matches, and the final branch with no condition acts as the default. Because every branch returns a series float, the result can be used directly in arithmetic. The switch runs on every bar, so each moving average keeps its history even while it is not the selected one. That matters when a user flips the input, since the newly selected line is already warmed up.
Why your version will not match ta.kc exactly
If you set both versions to the same length and multiplier and overlay them, the lines will be close but not identical. The cause is the smoothing used for the range. ta.atr smooths the true range with a running moving average (RMA), also known as Wilder's smoothing. The Keltner built-in, by contrast, is generally described as smoothing the range with an exponential moving average, though you should confirm that against TradingView's reference and your own plots. RMA and EMA are both exponential in character but use different smoothing constants, so the same length yields a slightly different range.
You can test this yourself in two minutes: plot the difference between your manual upper line and the built-in upper line. If you want your hand-written version to follow the built-in's range more closely, replace ta.atr(atrLen) with ta.ema(ta.tr, length). This is a good habit in general. When a hand-built indicator drifts from a built-in, compare the intermediate series, not just the final lines. The difference is almost always in a smoothing choice, and plotting the pieces reveals it at once.
For a deeper look at how ATR itself is computed and why its smoothing matters, see ATR in Pine Script.
Reading the channel without over-reading it
A channel is a description of recent volatility around a trend estimate. It is not a forecast, and a close outside it does not mean anything will happen next. The useful questions are descriptive.
The first is how wide the channel is compared with its own recent history. A narrow channel means recent bars have had small ranges, and a wide one means large ranges. Plotting ta.kcw in a separate pane, or comparing it to a moving average of itself, answers this directly.
The second is how price sits relative to the bands. In a steady move, closes often ride near one outer band without crossing it far. A close beyond the band is simply a bar that moved further than the smoothed range would suggest, whether that is a short burst of momentum or one outlier bar. The indicator cannot tell these apart, which is a good reason to avoid reading a single close as meaningful by itself.
The third is the slope of the basis. Because the basis is an exponential average, its slope shows the direction of the trend estimate. A flat basis with tight bands describes a quiet, range-bound stretch. A sloping basis with wide bands describes an active move.
These readings describe what has already happened. They are inputs to your own analysis, not instructions, and any rule you build on them should be tested over a long sample before you rely on it.
Detecting a squeeze: Bollinger Bands inside Keltner Channels
The most common use of Keltner Channels in code is as a reference for Bollinger Bands. Because Bollinger Bands react quickly and Keltner Channels move gradually, the Bollinger bands can contract until they sit fully inside the Keltner Channel. That condition is often called a squeeze, and it marks a stretch where closing prices have been unusually tightly clustered relative to bar ranges.
The test is a pair of comparisons: the upper Bollinger band must be below the upper Keltner band, and the lower Bollinger band must be above the lower Keltner band.
//@version=6
indicator("Bollinger inside Keltner", overlay = true)
length = input.int(20, "Length", minval = 2)
bbMult = input.float(2.0, "Bollinger multiplier", minval = 0.1, step = 0.1)
kcMult = input.float(1.5, "Keltner multiplier", minval = 0.1, step = 0.1)
[kcMid, kcUpper, kcLower] = ta.kc(close, length, kcMult, true)
[bbMid, bbUpper, bbLower] = ta.bb(close, length, bbMult)
squeezeOn = bbUpper < kcUpper and bbLower > kcLower
squeezeOff = not squeezeOn and squeezeOn[1]
plot(kcMid, "Keltner basis", color = color.gray)
plot(kcUpper, "Keltner upper", color = color.teal)
plot(kcLower, "Keltner lower", color = color.teal)
plot(bbMid, "Bollinger basis", display = display.none)
plot(bbUpper, "Bollinger upper", color = color.purple)
plot(bbLower, "Bollinger lower", color = color.purple)
bgcolor(squeezeOn ? color.new(color.orange, 85) : na)
plotshape(squeezeOff, "Squeeze released", shape.diamond, location.belowbar, color.blue, size = size.tiny)The two multipliers are set differently on purpose. A Bollinger multiplier of 2 with a Keltner multiplier of 1.5 is a widely used pairing, because it makes the squeeze condition demanding: the Bollinger bands must contract enough to fit inside a fairly narrow Keltner channel. If both multipliers are 2, squeezes occur far more often, and the shading loses its meaning. Whatever pair you choose, treat it as a parameter you can vary.
The squeezeOff line uses the history operator. squeezeOn[1] is the value of squeezeOn on the previous bar, so squeezeOff is true only on the first bar where the squeeze condition stops holding. This one-bar flag is easier to alert on than the shading, which stays on for many bars.
The orange shading and the blue diamond are descriptions of volatility, not predictions. The release of a squeeze says that Bollinger width is no longer smaller than the Keltner width. It says nothing about which way price goes afterwards, or whether it moves at all.
Seeing both sets of bands together on real data makes the logic much easier to follow than reading the code. In PineScripter you can paste or generate a script like this one and run it on the Chart against real market data, then step through it with Bar Replay to see exactly which bar flips the condition. PineScripter's market data covers crypto spot markets, so that is what you would be looking at. The free Keltner Channels calculator is also handy for checking a single set of values by hand against what your script plots.
Alerts that do not fire on the wrong bar
Once the channel works, the next request is usually an alert when price closes outside it. Two rules keep that alert honest.
//@version=6
indicator("Keltner close alerts", overlay = true)
length = input.int(20, "Length", minval = 1)
mult = input.float(2.0, "Multiplier", minval = 0.1, step = 0.1)
[basis, upper, lower] = ta.kc(close, length, mult, true)
closeAbove = ta.crossover(close, upper)
closeBelow = ta.crossunder(close, lower)
plot(basis, "Basis", color = color.orange)
plot(upper, "Upper", color = color.teal)
plot(lower, "Lower", color = color.teal)
plotshape(closeAbove, "Close above upper", shape.triangleup, location.belowbar, color.teal, size = size.tiny)
plotshape(closeBelow, "Close below lower", shape.triangledown, location.abovebar, color.red, size = size.tiny)
alertcondition(closeAbove, "Close above upper band", "Price crossed above the upper Keltner band")
alertcondition(closeBelow, "Close below lower band", "Price crossed below the lower Keltner band")First, remember that on a live bar the close is just the latest price, so the crossover condition can be true mid-bar and false by the end of it. If you create the TradingView alert with the "Only Once Per Bar Close" trigger option, it evaluates only on the confirmed close, which removes that flicker. The reasoning is covered in Why Your Pine Script Alert Fires on the Wrong Bar.
Second, the channel itself is computed from the current bar's high and low, so on an unfinished bar the bands move as the bar develops. That is another reason to act on closed bars only. For the broader picture of how this kind of drift happens and how to avoid it, read Repainting Explained.
If you connect the alert to the Alert Log in PineScripter, each firing is recorded with its name, price, and time, which makes it easy to check afterwards whether the alert fired when you expected it to.
Common mistakes
Most problems with Keltner Channels in Pine Script come from a short list of causes.
Calling ta.kc inside a conditional block is the first. The function needs to see every bar to build its smoothing, so keep it at the top level and use the returned values inside conditions.
Mixing up multipliers is the second. A Keltner multiplier of 2 and a Bollinger multiplier of 2 do not describe equivalent widths. They are scaled against different volatility measures, so compare the lines visually rather than assuming the numbers line up.
Expecting a hand-built version to match the built-in exactly is the third, and the smoothing difference above is the reason. Decide which one is your reference and document it in the script.
Ignoring the timeframe is the fourth. A 20-bar channel on a 5-minute chart and one on a daily chart describe very different spans of time. The same inputs are not comparable across timeframes.
Finally, reading the bands as targets or limits is a conceptual mistake. They are statistical descriptions of recent range, and price passes through them regularly.
If you want to take a Keltner-based idea further, for example by turning it into a test you can run in TradingView's Strategy Tester, From Indicator to Backtestable Strategy in Pine Script walks through the conversion, and Donchian Channel Breakout Strategy in Pine Script shows a close relative built on highest-high and lowest-low bands. PineScripter does not simulate trades or report returns, so that testing step happens in TradingView.
Putting it together
A Keltner Channel is an exponential moving average with volatility bands built from a smoothed range. In Pine Script v6 you can get one in a single line with ta.kc, or build it yourself when you need a different moving average, a separate range length, or a different range source. The built-in and the hand-built version differ slightly because they smooth the range differently, so choose one and stay consistent. The channel is most informative when compared with something else, and Bollinger Bands inside Keltner Channels is the standard pairing for describing compressed volatility. Whatever you build, handle alerts on confirmed bars and treat every reading as a description of the past.
If you would rather describe the indicator in plain English and have the v6 code written, checked, and run on a chart for you, you can try that at PineScripter, where follow-up requests change only the lines that need to change.
PineScripter is a coding tool, not a financial advisor. Nothing in this article is investment advice, and indicators describe past price data and do not predict future results.
Frequently asked questions
What does ta.kc return in Pine Script v6?
It returns three series as a tuple: the basis, the upper band, and the lower band. Its arguments are the source, the length, the multiplier, and an optional flag that chooses between true range and the plain high-low range.
Why does my hand-built Keltner Channel differ slightly from ta.kc?
The usual cause is smoothing. ta.atr uses Wilder's running moving average, while the built-in channel is generally described as using an exponential moving average of the range. Plot the difference between the two upper lines to confirm, or use ta.ema(ta.tr, length) as your range.
What is the difference between Keltner Channels and Bollinger Bands?
Keltner Channels size their width from a smoothed range of bars, so they change gradually. Bollinger Bands size theirs from the standard deviation of closes, so they react faster to a single large move.
What multiplier should I use for Keltner Channels?
There is no universally correct value. 2 is a common default for the channel on its own, and 1.5 is often paired with a Bollinger multiplier of 2 for squeeze detection. Treat it as a parameter and check it on your own symbol and timeframe.
Can ta.kc repaint?
On a live bar the bands move as the high, low, and close develop, so a condition built on them can change before the bar closes. Evaluating alerts on confirmed bar closes avoids acting on that intermediate state.
Does PineScripter backtest Keltner Channel strategies?
No. PineScripter writes and checks the Pine Script, runs it on the chart against real market data, and lets you step through it bar by bar. Strategy backtests are done in TradingView's Strategy Tester.
Run your script on real market data and see what it plots →
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.