The 33 Markdown files under docs/wiki/ were never tracked. Commit them
into the repository so the documentation is versioned alongside the
code: 8 top-level pages plus 25 per-indicator deep dives under
indicators/{momentum,trend,volatility,volume}/.
The pages are kept in-repo (not pushed to a flat GitHub Wiki), so the
relative indicators/<family>/... links in Home.md resolve correctly
when rendered on GitHub.
8.2 KiB
Keltner Channels
A pure composition of EMA on typical price plus ATR-scaled envelopes. The middle line is the trend filter, the bands are the volatility cone.
Quick reference
| Item | Value |
|---|---|
| Family | Volatility |
| Sub-category | envelope (composed: EMA + ATR) |
| Input type | Candle (uses high, low, close) |
| Output type | KeltnerOutput { upper: f64, middle: f64, lower: f64 } |
| Output range | unbounded; lower ≤ middle ≤ upper |
| Default parameters | ema_period = 20, atr_period = 10, multiplier = 2.0 |
| Warmup period | max(ema_period, atr_period) (20 for defaults) — see Warmup notes |
| Interpretation | trend-following envelope; tags signal momentum, not exhaustion |
Formula
middle_t = EMA_{ema_period}( typical_price_t ) // tp = (H+L+C)/3
upper_t = middle_t + multiplier * ATR_{atr_period}_t
lower_t = middle_t - multiplier * ATR_{atr_period}_t
The middle line is an EMA of typical price, not of close
(crates/wickra-core/src/indicators/keltner.rs:62,
candle.typical_price()).
Parameters
| Name | Type | Default | Constraint | Source |
|---|---|---|---|---|
ema_period |
usize |
20 |
> 0 |
Keltner::new (keltner.rs:33) |
atr_period |
usize |
10 |
> 0 |
Keltner::new (keltner.rs:33) |
multiplier |
f64 |
2.0 |
finite and > 0.0 |
Keltner::new (keltner.rs:34-36) |
Python defaults from
#[pyo3(signature = (ema_period=20, atr_period=10, multiplier=2.0))] in
bindings/python/src/lib.rs. Keltner::classic() returns the same
configuration.
Inputs / Outputs
impl Indicator for Keltner {
type Input = Candle;
type Output = KeltnerOutput;
fn update(&mut self, candle: Candle) -> Option<KeltnerOutput>;
}
pub struct KeltnerOutput { pub upper: f64, pub middle: f64, pub lower: f64 }
- Python streaming. Returns
(upper, middle, lower)tuple orNone. - Python batch.
Keltner.batch(high, low, close)returns a 2-Dnp.ndarrayof shape(n, 3)with columns[upper, middle, lower]; warmup rows areNaNacross all three columns. - Node streaming. Returns a
{ upper, middle, lower }object ornull. - Node batch.
keltner.batch(high, low, close)returns a flatArray<number>of lengthn * 3interleaved per row:[u0, m0, l0, u1, m1, l1, …].
Warmup
warmup_period() reports max(ema_period, atr_period) — for the
default (20, 10, 2.0) that is 20.
Important caveat verified empirically. Because Keltner::update
calls self.ema.update(...)? before self.atr.update(...)?, the ATR
sub-indicator only receives an input on candles where the EMA already
has a value. The actual first emission therefore occurs after roughly
ema_period + atr_period - 1 candles, not max(ema_period, atr_period).
With the classic (20, 10, 2.0) configuration the first non-None
output is the 29th candle (index 28), not the 20th. Code reference:
keltner.rs:61-69. Plan your data prefix accordingly.
Edge cases
- Flat market. A constant-OHLC series produces
upper == middle == lowerbecause ATR collapses to0. The pinned testflat_market_collapses_bandscovers this. - Trending market. When ATR rises, both bands widen symmetrically around the EMA centerline.
- Reset.
reset()resets both the underlying EMA and ATR; the configured periods/multiplier are preserved. - NaN / infinity.
Candle::newrejects non-finite OHLC values up front; the indicator never receives them. - Invalid params.
ema_period == 0,atr_period == 0, or non-positivemultiplierreturns an error fromKeltner::new.
Examples
Rust
use wickra::{BatchExt, Candle, Indicator, Keltner};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let candles = vec![
Candle::new(10.0, 11.0, 9.0, 10.5, 1.0, 0)?,
Candle::new(10.5, 12.0, 10.0, 11.5, 1.0, 0)?,
Candle::new(11.5, 13.0, 11.0, 12.5, 1.0, 0)?,
Candle::new(12.5, 14.0, 12.0, 13.5, 1.0, 0)?,
Candle::new(13.5, 15.0, 13.0, 14.5, 1.0, 0)?,
];
let mut k = Keltner::new(3, 3, 2.0)?;
for (i, v) in k.batch(&candles).into_iter().enumerate() {
println!("i={i} -> {:?}", v);
}
Ok(())
}
Output:
i=0 -> None
i=1 -> None
i=2 -> None
i=3 -> None
i=4 -> Some(KeltnerOutput { upper: 17.166666666666664, middle: 13.166666666666666, lower: 9.166666666666666 })
Notice the first emission is at i = 4 (the 5th candle), not i = 2,
even though max(ema=3, atr=3) = 3. This is the EMA-gates-ATR effect
documented under Warmup.
Python
import numpy as np
import wickra as ta
k = ta.Keltner(3, 3, 2.0)
h = np.array([11.0, 12.0, 13.0, 14.0, 15.0])
l = np.array([ 9.0, 10.0, 11.0, 12.0, 13.0])
c = np.array([10.5, 11.5, 12.5, 13.5, 14.5])
print(k.batch(h, l, c))
Output:
[[ nan nan nan]
[ nan nan nan]
[ nan nan nan]
[ nan nan nan]
[17.16666667 13.16666667 9.16666667]]
Node
const w = require('wickra');
const k = new w.Keltner(3, 3, 2.0);
const flat = k.batch(
[11, 12, 13, 14, 15],
[ 9, 10, 11, 12, 13],
[10.5, 11.5, 12.5, 13.5, 14.5],
);
console.log('length:', flat.length);
console.log('row 4 [upper, middle, lower]:', flat.slice(12, 15));
Output:
length: 15
row 4 [upper, middle, lower]: [ 17.166666666666664, 13.166666666666666, 9.166666666666666 ]
Interpretation
- Trend filter. Persistent closes above the upper band signal trend continuation, much like Bollinger's "walking the band" pattern; Keltner is generally tighter than Bollinger on noisy series because ATR responds more smoothly than a rolling stddev.
- Squeeze cross-over. A common "squeeze" setup compares Bollinger bandwidth to Keltner channel width: when Bollinger fits inside Keltner, a volatility expansion is statistically more likely.
- Pullback entries. In a defined uptrend, pullbacks to the middle EMA line are a classic continuation entry; the lower band acts as the disaster stop.
Common pitfalls
- Reported warmup understates the true warmup.
warmup_period()reportsmax(ema_period, atr_period), but because the EMA is evaluated first and short-circuits the ATR update via?, the indicator only emits after roughlyema_period + atr_period - 1candles. For the classic(20, 10, 2.0)you need 29 candles, not 20, before the first validKeltnerOutput. Inspectingis_ready()is the safest gate. - Typical price ≠ close. The middle EMA runs on
(H + L + C) / 3, not on close. A pre-computed "EMA of close" panel will not equal the Keltner middle line and trying to align them at floating-point precision will fail.
References
- Chester W. Keltner, How to Make Money in Commodities, 1960. The original construction used a 10-day SMA of typical price with an envelope sized by the 10-day average range. The modern variant (EMA centerline + ATR envelope) is the form Wickra implements.
- Linda Bradford Raschke popularised the EMA + ATR rephrasing in the 1990s; this is the version most TA libraries ship today.
See also
- EMA — the centerline component.
- ATR — the envelope width component.
- Bollinger Bands — envelope using stddev rather than ATR; useful side-by-side comparison.
- Donchian Channels — envelope using rolling extrema with no smoothing.