Keltner Channel: An EMA, an ATR, and a Port That Is Not Identical
A Keltner Channel is an exponential moving average with a band drawn two average true ranges either side of it. batch_keltner computes that with three parameters — a twenty-bar average for the middle, a ten-bar range for the width, a multiplier of two — and returns an upper, middle and lower row.
Two things about it are worth more than the usual description. Our engine ships no normalized companion row for this channel, unlike its two neighbours in the trading indicators set. And the port is not bit-identical to the Python it came from, which is true of nothing else in this cluster and turns out to be a real difference rather than a rounding one.
What the Keltner Channel is built from
There are two ingredients, and they measure different things.
Our middle row is ema(close, 20) — an exponential average, so recent closes weigh more and the line never fully forgets an old bar. Half-width comes from atr(high, low, close, 10): true range smoothed by Wilder's method, where true range is the largest of today's high-low span, the gap up from yesterday's close, and the gap down from it.
Notice what that second choice buys. A standard deviation of closes, which is what Bollinger Bands use, cannot see a gap at all — an asset that closes at 100 and opens at 80 contributes one large deviation and no acknowledgement that the move happened between bars. True range sees it directly. On a 24/7 venue gaps are rare and this barely matters; on anything with a session boundary it matters a great deal.
Three lines, and no fourth row
Bollinger ships two normalized rows beside its bands. Donchian ships one. Keltner ships none, and that gap is deliberate enough to be worth stating precisely.
Our crate's volatility module registers twelve oscillator rows: true range, normalized ATR, standard deviation, historical volatility, bbands_pctb, bbands_width, donchian_width, ulcer index, choppiness, relative volatility, the 52-week extremes, and the mass index. No keltner_width exists. No percent-position row exists either.
So there is no engine-supplied way to ask how far inside the channel a close sits, or how wide the channel is relative to itself, without doing the division yourself. The lower pane on the charts below therefore plots the ATR the band is literally built from, rather than a normalized series our engine does not compute. Inventing one for a diagram is how a page ends up describing software that does not exist.
The port that is not bit-identical
Every other numeric page in this cluster reports a bitwise match against the Python original. This one cannot, and the reason is structural.
Both differences are real. The Python signature takes a single period and hands it to both halves, so its default channel is a twenty-bar average with a twenty-bar range; ours splits them and defaults the range to ten. And our crate carries two ATR kernels — one seeded from the first n true ranges at index n−1, one that skips the very first bar and seeds at index n. batch_keltner calls the first. The Python twin is a faithful port of the second.
Measured across 275 symbols and 352,842 values. The middle row is bitwise identical, 0.000e+00, so the EMA port is exact and the whole difference lives in the width. Our two ATR kernels differ from each other by up to 1.201e+02 in absolute terms and 98.4% in relative ones, on 170,333 values. At matched periods the channel differs by up to 2.402e+02 — exactly twice the ATR gap, which is the multiplier, and therefore proof that nothing else contributes. At each side's own defaults, every one of the 352,842 values differs. The seeding gap takes a median of 353 bars to decay below 1e-9 relative, and it flipped 95 of 17,723 breakout decisions on its own.
That last number is the one that matters. A discrepancy nothing acts on is trivia; this one moves half a percent of the signals, so a strategy validated against the Python and deployed against the crate is trading a slightly different rule. Neither kernel is wrong. They are answers to two defensible questions about what to do with the first bar, which has no previous close and therefore no gap term.
What the breakout actually returned
Outcomes below resolve first-touch over twenty bars against a symmetric 8% bracket, so break-even sits at exactly 50% before fees, and a bar tagging both levels is scored a loss. Identical resolution to the Bollinger and Donchian pages, so the three sets of numbers compare directly.
Closes above the upper band fired on 13,444 bars, 3.9% of the tape, and resolved 45.4% long against a 49.1% base rate. That is 3.7 points below entering on an arbitrary bar. Closes below the lower band did no better in mirror: 17,664 signals, 45.9% short, 3.3 points under.
Both readings lose, and they lose by more than the Bollinger touch did. I've seen the Keltner breakout recommended specifically as the trend-friendly alternative to fading a Bollinger rail, on the argument that a range-based band is harder to breach. Harder, yes — 3.9% of bars versus 6.4%. Better, no.
The squeeze that is on half the time
The popular cross-indicator setup asks whether the Bollinger channel has contracted entirely inside the Keltner one, then trades the bar it stops doing so.
Run both at our engine's own defaults and that condition held on 171,175 of 347,067 bars. 49.3% — very nearly half the tape. Whatever a rare coiled spring looks like, this is not it: two sigma of closes is simply narrower than two ten-bar ATRs about half the time, because one measures deviation about a mean and the other measures span including gaps.
Release bars, where the containment ended, fired 12,729 times and resolved 48.4% long against the 49.1% base. Forward range came in at a median 37.3% against 36.5% on any bar — 1.02x, which is nothing. Direction ran 2.1 points better than the base, the only figure on this page that leans the right way, and it leans by too little to build on.
Where the Keltner Channel fails
Below are two real failures, one from each side, and they fail for the same underlying reason.
An upper-band break says price moved more than two recent average ranges. When volatility has been compressed, two ATRs is a small number, so the band gets breached by moves that are unremarkable in absolute terms — one instance shown closed 2.2% above its upper rail and then surrendered 17.2%. The channel had simply forgotten how large this asset's moves normally are.
Lower-band breaks invert it. The most violent example here closed 7.1% under the lower rail and then rose 37.2%, because a capitulation bar widens the ATR that defines the rail, and the rail chases price down just as the low goes in. Confirmation from something without a lookback — a candlestick reversal, or the MACD turning — is the only cheap defence I trust here.
Frequently asked questions
What is a Keltner Channel?
An exponential moving average of the close with a band two average true ranges wide either side, emitted by batch_keltner as three rows.
Keltner or Bollinger? They measure different things: true range including gaps, versus standard deviation of closes. Our band touches lost against the base rate in both, and the Keltner reading lost slightly more.
What settings does the engine use? A twenty-bar EMA, a ten-bar ATR and a multiplier of two. Note the two separate lookbacks — most descriptions assume one number for both.
Is there a Keltner width or percent-position indicator? Not in our crate. Bollinger and Donchian each have a normalized companion row; this channel has none, so any such series is yours to compute and yours to validate.
Does the TTM squeeze work? The containment condition held about half the time at our defaults, and the release bar returned 48.4% against a 49.1% base with essentially no range expansion. On this tape, no.
This is educational material, not financial advice. Every figure here was measured on past bars, past behavior generalizes poorly to future bars, and trading carries real risk of loss — size any position so that being wrong stays survivable.