How can a smoothed price level carry a separately smoothed range envelope? That is the practical question behind Keltner Channels. This tutorial builds the answer from finalized data, shows the exact arithmetic, and gives you a laboratory for breaking the indicator safely.
The mental model in one minute
The selected modern variant uses an SMA-seeded close EMA(20) as its center and a separately SMA-seeded Wilder ATR(10) as its distance, multiplied by two.
The output is a measurement. It is not an order, forecast, confidence interval, or promise that a familiar-looking market pattern will repeat.
Contract before chart
- Variant: This is the modern EMA-plus-ATR variant, not Chester Keltner's original typical-price and average-range construction.
- Defaults: EMA period=20, ATR period=10, multiplier=2.
- First publication: index 19 when both the EMA basis and ATR envelope are ready.
- Data: finalized, causal observations on one price basis.
- Missing, malformed, or mixed-basis input: reject rather than repair silently.
Formula and symbols
| Symbol | Meaning | Unit |
|---|---|---|
| SMA-seeded EMA of close | price units | |
| Wilder ATR of True Range | price units | |
| positive envelope multiplier | dimensionless | |
| upper and lower channels | price units |
Calculate it by hand
Suppose the current EMA middle is 100, the previous ten-period ATR is 3, the current True Range is 5, and k=2.
- Updated ATR:
(9 × 3 + 5) / 10 = 3.2. - Upper channel:
100 + 2 × 3.2 = 106.4. - Lower channel:
100 - 2 × 3.2 = 93.6. - Full channel width:
106.4 - 93.6 = 12.8 = 2 × k × ATR.
If close level stays near 100 while range grows, the envelope can widen without the center moving much. That separation is the central Keltner mental model.
That calculation is deliberately small enough to audit without code. The 240-row fixtures then show how the same rule behaves through warm-up, regime changes, ties, gaps, shocks, and expiry.
The complete algorithm
- Validate finalized high, low, and close on one price basis.
- Build the close EMA with an n-row SMA seed and alpha
2/(n+1). - Independently build True Range and the ATR seed/recursion.
- Withhold the channel until both middle and ATR are ready.
- Publish middle plus/minus k ATR; after a revision, recompute both recursive paths.
The flow exposes the point where a result becomes unavailable, where recursive state begins, or where a rolling owner expires.
See the mechanism
The map connects formula terms to the exact data they consume.
Follow a complete causal trace
Only rows at or before the active observation may influence the displayed state. Warm-up is shown as unavailable rather than silently converted to zero.
Memory is part of the definition
Two recursive memories: EMA level decays at 1-2/(ema_period+1) and ATR range decays at (atr_period-1)/atr_period. Neither has a single expiry row.
The channel is symmetric around the EMA and has full width 2kATR. It is not a statistical confidence interval.
This is why two indicators can react differently to the same shock even when their plotted bands appear similar.
Six ways to try to break it
| Scenario | Teaching job | Expected lesson |
|---|---|---|
atr-expansion | ATR envelope expansion | Range expansion widens the envelope around a slower basis. |
ema-trend | EMA trend response | The center follows level while width follows range. |
gap-widening | Gap-driven widening | Previous-close gaps enter ATR immediately. |
flat-control | Flat-range control | Zero ATR collapses the channel onto its EMA basis. |
mixed-memory | Independent level and range memory | Range changes first, then level changes, exposing the channel's two independent recursive states. |
responsive | Responsive 10/7 × 1.5 | Shorter seeds respond faster and a smaller multiplier tightens width. |
Predict the active component before opening the lab. Then jump to the relevant checkpoint and inspect the formula trace rather than guessing from line shape.
Interactive laboratory
Open the Keltner Channels guided lab. It contains six 240-row scenarios. Change the period or multiplier where the formula exposes one; the lab recomputes immediately, stops playback, resets to an informative state, and preserves the same calculation path used by Step. Use the checkpoint selector to compare first publication, mechanism, boundary, memory, platform reconciliation, and mature state.
Production implementation
The Python and
TypeScript references
are topic-scoped and intentionally readable. They validate before calculating,
return aligned arrays, and use None/null for unavailable states. Shared
fixtures establish parity; independent family checks establish properties
without trusting either implementation as the oracle.
When production output disagrees with a chart, log:
- symbol and source ownership;
- timeframe, session, timestamp, and bar finality;
- adjustment basis;
- parameter values and first-ready index;
- the earliest divergent component;
- window inclusion, seed, denominator, multiplier, equality, and null policy;
- the earliest revised observation.
Compare, do not conflate
| Nearby topic | What changes |
|---|---|
| Bollinger Bands | use finite-window close dispersion rather than recursive True Range. |
| ATR | supplies the distance state but not the EMA center. |
| Original Keltner construction | used a different center/range design; it is excluded here. |
Misuse boundary
- Do not: Calling every moving-average range envelope a Keltner Channel without naming its variant.
- Do not: Assuming band contact has one universal interpretation.
- Do not: Comparing channels with different EMA seeds, ATR periods, first-bar rules, or range styles.
The synthetic examples establish implementation behavior only. No historical association, transaction cost, out-of-sample result, or investment suitability claim is made.
Historical example decision
No named historical episode is used. The mechanism is learned more cleanly from controlled synthetic paths, and a named case would require point-in-time, licensed, adjustment-consistent OHLC evidence before publication.
Summary
You can now state the precise question Keltner Channels answers, calculate it, explain its memory, diagnose a platform mismatch, and separate a measurement from a trading claim. Use the family comparison rather than chart resemblance to choose the next concept.
Primary evidence
See the topic reference ledger for source roles, dates, and limitations.
Rendered from the canonical Mermaid sources linked by this article.
Keltner Channels calculation flow
Purpose: show the exact selected state, readiness branch, and output.
Takeaway: the formula, memory, and unavailable state are part of the contract, not chart formatting.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
Keltner Channels (KC)
- Organization or authors: TradingView
- Source type: Maintained platform documentation
- Publication, version, or access date: accessed 2026-07-26
- URL or identifier: Keltner Channels (KC)
- Accessed: 2026-07-26
- Jurisdiction: not jurisdiction-specific
- Evidence role: sourced definition, maintained implementation, or bounded historical context
- Supports: Documents the modern EMA plus ATR example and notes historical variants.
- Limitations: Its interpretation text is not used as predictive evidence.
New Concepts in Technical Trading Systems
- Organization or authors: J. Welles Wilder Jr.
- Source type: Original book
- Publication, version, or access date: 1978
- URL or identifier: ISBN 978-0-89459-027-6
- Accessed: 2026-07-26
- Jurisdiction: not jurisdiction-specific
- Evidence role: sourced definition, maintained implementation, or bounded historical context
- Supports: Introduces True Range and Wilder’s recursive smoothing framework.
- Limitations: Bibliographic source; this package still freezes its own first-bar and seed alignment.
ATR — Average True Range
- Organization or authors: TA-Lib
- Source type: Maintained official documentation
- Publication, version, or access date: maintained; accessed 2026-07-26
- URL or identifier: ATR — Average True Range
- Accessed: 2026-07-26
- Jurisdiction: not jurisdiction-specific
- Evidence role: sourced definition, maintained implementation, or bounded historical context
- Supports: Documents ATR as a Wilder indicator and its library availability.
- Limitations: The first-ready row follows TA-Lib’s own True Range convention.
TRANGE — True Range
- Organization or authors: TA-Lib
- Source type: Maintained official documentation
- Publication, version, or access date: maintained; accessed 2026-07-26
- URL or identifier: TRANGE — True Range
- Accessed: 2026-07-26
- Jurisdiction: not jurisdiction-specific
- Evidence role: sourced definition, maintained implementation, or bounded historical context
- Supports: Defines the three-way maximum and TA-Lib first-row availability.
- Limitations: TA-Lib’s first-row convention differs from this package.
Public evidence boundary
The fixture is author-generated synthetic teaching material. Every displayed number is an author-derived calculation from those inputs. No provider observation, historical return, causal claim, forecast, or profitability result is published. Platform documentation supports only the named platform convention; local seeds, nulls, equality, and scope remain implementation choices.
Full dependency-light reference implementations in both supported languages.
function finite(v:number[],n:string){if(!Array.isArray(v)||!v.length||v.some(x=>!Number.isFinite(x)))throw new Error(`${n} must contain finite numbers`)}
function hlc(h:number[],l:number[],c:number[]){finite(h,"high");finite(l,"low");finite(c,"close");if(h.length!==l.length||h.length!==c.length)throw new Error("high, low, and close lengths must match");if(h.some((x,i)=>x<l[i]||c[i]<l[i]||c[i]>x))throw new Error("each bar requires low <= close <= high")}
export function true_range(high:number[],low:number[],close:number[]){hlc(high,low,close);const true_range:number[]=[],high_low:number[]=[],high_gap:(number|null)[]=[],low_gap:(number|null)[]=[],driver:string[]=[];for(let i=0;i<high.length;i++){const a=high[i]-low[i],b=i?Math.abs(high[i]-close[i-1]):null,c=i?Math.abs(low[i]-close[i-1]):null;const choices:[number,string][]=[[a,"high-low"]];if(b!==null&&c!==null)choices.push([b,"high-gap"],[c,"low-gap"]);const best=choices.reduce((x,y)=>y[0]>x[0]?y:x);true_range.push(best[0]);high_low.push(a);high_gap.push(b);low_gap.push(c);driver.push(best[1])}return{true_range,high_low,high_gap,low_gap,driver}}
function period(v:number,n="period"){if(!Number.isInteger(v)||v<1)throw new Error(`${n} must be a positive integer`)}
export function average_true_range(high:number[],low:number[],close:number[],p=14){period(p);const tr=true_range(high,low,close).true_range,atr:(number|null)[]=Array(tr.length).fill(null);if(tr.length>=p){let value=tr.slice(0,p).reduce((a,b)=>a+b,0)/p;atr[p-1]=value;for(let i=p;i<tr.length;i++){value+=(tr[i]-value)/p;atr[i]=value}}return{true_range:tr,atr}}
function emaSeed(values:number[],p:number){period(p);finite(values,"close");const out:(number|null)[]=Array(values.length).fill(null);if(values.length<p)return out;let value=values.slice(0,p).reduce((a,b)=>a+b,0)/p;out[p-1]=value;const alpha=2/(p+1);for(let i=p;i<values.length;i++){value+=alpha*(values[i]-value);out[i]=value}return out}
export function keltner_channels(high:number[],low:number[],close:number[],emaPeriod=20,atrPeriod=10,multiplier=2){if(!Number.isFinite(multiplier)||multiplier<=0)throw new Error("multiplier must be finite and positive");hlc(high,low,close);period(emaPeriod,"ema_period");period(atrPeriod,"atr_period");const middle=emaSeed(close,emaPeriod),a=average_true_range(high,low,close,atrPeriod),upper:(number|null)[]=Array(close.length).fill(null),lower:(number|null)[]=Array(close.length).fill(null);for(let i=0;i<close.length;i++)if(middle[i]!==null&&a.atr[i]!==null){upper[i]=(middle[i] as number)+multiplier*(a.atr[i] as number);lower[i]=(middle[i] as number)-multiplier*(a.atr[i] as number)}return{true_range:a.true_range,atr:a.atr,middle,upper,lower}}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.