KAMA
Kaufman's Adaptive Moving Average — picks its own smoothing constant on every bar from a fast/slow EMA pair, weighted by an efficiency ratio that measures how trending the recent price action has been.
Quick reference
| Field | Value |
|---|---|
| Family | Moving Averages |
| Input type | f64 (single close) |
| Output type | f64 |
| Output range | unbounded; tracks the input price scale |
| Default parameters | Python: (er_period=10, fast=2, slow=30); Rust: Kama::classic() returns the same triple |
Warmup period (warmup_period()) | er_period + 1 — see below; the first emission lands at this index, but on a fresh KAMA that emission equals the seed (the input itself) |
| Interpretation | Fast in trending markets, slow in choppy markets — by construction. |
Formula
For each new input price_t (with n = er_period):
direction_t = | price_t - price_{t-n} |
volatility_t = Σ_{i=1}^{n} | price_{t-i+1} - price_{t-i} |
ER_t = direction_t / volatility_t // 0 = pure chop, 1 = pure trend; 0 if volatility = 0
fast_sc = 2 / (fast + 1) // fast EMA smoothing constant
slow_sc = 2 / (slow + 1) // slow EMA smoothing constant
SC_t = (ER_t * (fast_sc - slow_sc) + slow_sc) ^ 2
KAMA_t = KAMA_{t-1} + SC_t * (price_t - KAMA_{t-1})The squared SC_t is Kaufman's choice (he found that squaring widens the dynamic range between "act like a fast EMA" and "act like a slow EMA"). On the very first emission KAMA_{t-1} is seeded with the oldest price in the window (window.front()), which is the convention in the source.
Parameters
| Name | Type | Default (Python KAMA(...)) | Valid range | Description |
|---|---|---|---|---|
er_period | usize | 10 | >= 1 | Lookback for the efficiency ratio. Larger → smoother ER, slower adaptation. |
fast | usize | 2 | >= 1, strictly < slow | Fast EMA period; sets the lower bound on responsiveness. |
slow | usize | 30 | >= 1, strictly > fast | Slow EMA period; sets the upper bound on smoothness. |
Any of er_period, fast, slow being 0 errors with Error::PeriodZero; fast >= slow errors with Error::InvalidPeriod. The Python defaults come from #[pyo3(signature = (er_period=10, fast=2, slow=30))] in bindings/python/src/lib.rs; the Rust convenience constructor Kama::classic() returns the same triple.
Inputs / Outputs
From crates/wickra-core/src/indicators/kama.rs:
use wickra::{Indicator, Kama};
// Kama: Input = f64, Output = f64
const _: fn(&mut Kama, f64) -> Option<f64> = <Kama as Indicator>::update;Python returns float | None (streaming) / numpy.ndarray (batch, NaN for warmup). Node returns number | null (streaming) / Array<number> with NaN (batch). warmup_period() is exposed in Rust and Python but not on the Node KAMA class (consult bindings/node/index.d.ts for the surface).
Warmup
Kama::new(er_period, fast, slow).warmup_period() == er_period + 1. The "off-by-one" is because the efficiency ratio compares price_t to price_{t-er_period} and sums er_period consecutive absolute diffs; that requires er_period + 1 prices in the window. For Kama::classic() (er_period = 10) the first emission lands on input 11, matching the table in Warmup Periods.
The implementation uses a VecDeque of capacity er_period + 1. Once full, every subsequent update pops the front and pushes the new input — update is O(er_period) in principle (the volatility sum is re-computed) but O(1) in period/fast/slow since the EMA-style recursion has no window.
Note: on the first emission, prev = window.front() (the oldest price), and the output is prev + SC · (input − prev). On a perfectly trending series (ER ≈ 1, SC ≈ fast_sc² ≈ 0.444) this means the first KAMA value is materially below the latest price; on [1, 2, …, 20] for instance, KAMA's first emission at input 11 is 5.444…, not 11. See the example output below.
Edge cases
- Constant series. Feeding
[100.0; n]producesSome(100.0): bothdirectionandvolatilityare zero, the source branchesif volatility == 0.0 { 0.0 } else { ... }soER = 0,SC = slow_sc² ≈ 0.00416, and100 + 0.00416 · (100 − 100) = 100. The unit testconstant_series_yields_constant_kamapins this withKama::classic(). - NaN / infinity inputs. The first line of
updateisif !input.is_finite() { return self.state; }. Non-finite inputs are silently dropped; the window is not advanced, the previously emitted value is preserved. - Reset.
kama.reset()clears both the window and the smoothed state. The nextupdatestarts a fresher_period + 1warmup countdown.
Examples
Rust
use wickra::{BatchExt, Indicator, Kama};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut kama = Kama::classic(); // (10, 2, 30)
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
let out: Vec<Option<f64>> = kama.batch(&prices);
println!("warmup_period = {}", kama.warmup_period());
println!("{:?}", out);
Ok(())
}Output:
warmup_period = 11
[None, None, None, None, None, None, None, None, None, None, Some(5.444444444444443), Some(8.358024691358022), Some(10.421124828532234), Some(12.011736015851241), Some(13.339853342139579), Some(14.522140745633099), Some(15.62341152535172), Some(16.679673069639843), Some(17.710929483133246), Some(18.728294157296247)]Kama::classic().periods() returns (10, 0.6666666666666666, 0.06451612903225806) — the second and third numbers are fast_sc = 2/3 and slow_sc = 2/31, not the integer fast/slow periods themselves. On the linear ramp 1, 2, …, 20 the efficiency ratio is 1.0 (every step moves direction the same as volatility), so SC = fast_sc² ≈ 0.4444. The first emission 5.444… is 1 + 0.4444 · (11 − 1) and each subsequent value follows the same recursion.
Python
import numpy as np
import wickra as ta
kama = ta.KAMA() # defaults: er_period=10, fast=2, slow=30
out = kama.batch(np.arange(1.0, 21.0))
print("warmup_period =", kama.warmup_period())
print(out)Output:
warmup_period = 11
[ nan nan nan nan nan nan
nan nan nan nan 5.44444444 8.35802469
10.42112483 12.01173602 13.33985334 14.52214075 15.62341153 16.67967307
17.71092948 18.72829416]Node
const ta = require('wickra');
const kama = new ta.KAMA(10, 2, 30); // no default constructor; pass the triple
const prices = Array.from({ length: 20 }, (_, i) => i + 1);
console.log(kama.batch(prices));Output:
[
NaN, NaN,
NaN, NaN,
NaN, NaN,
NaN, NaN,
NaN, NaN,
5.444444444444443, 8.358024691358022,
10.421124828532234, 12.011736015851241,
13.339853342139579, 14.522140745633099,
15.62341152535172, 16.679673069639843,
17.710929483133246, 18.728294157296247
](The Node KAMA class does not expose warmupPeriod(); use the Rust or Python binding if you need that getter from your application.)
Interpretation
KAMA's defining property is that it changes its own behaviour with the market. In a clean trend the efficiency ratio approaches 1, SC approaches fast_sc², and KAMA behaves like a fast EMA — it tracks price closely. In a choppy sideways market the efficiency ratio collapses toward 0, SC approaches slow_sc², and KAMA effectively freezes — its line goes nearly flat regardless of how violently price oscillates around it. This is by design: Kaufman's argument is that you should not chase noise.
The two usable signals are slope (positive = uptrend; flat = ranging; negative = downtrend) and price-vs-KAMA crossover. Because KAMA can sit nearly flat for long stretches in a range, "price crossed KAMA" generates fewer false signals than the same test against an EMA of similar period.
Prefer Kama over a static EMA/SMA when the market regime varies materially (trending → ranging → trending). Prefer a fixed Ema / Hma when you want a predictable smoothing profile that is independent of price action.
Common pitfalls
- Assuming
warmup_period()is when the line is "good". The first emission lands at input 11 (for the defaulter_period = 10), but the seedKAMA_{t-1} = window.front()is the oldest price in the window, so the very first emitted value is biased toward the 10-bars-ago price. On a strong trend this means the first 3–5 emissions are noticeably below (or above, depending on direction) the current price. If that matters, drop the firster_periodpost-warmup emissions, not just the warmup itself. - Tuning
fastandslowindependently ofer_period. Kaufman's derivation assumesslow >> fastso that the per-bar SC has room to move. Picking, say,(10, 5, 6)givesfast_sc ≈ 0.333andslow_sc ≈ 0.286, so SC barely changes regardless of the efficiency ratio — KAMA degenerates into "an EMA somewhere around period 6". Keepslowat least5×fastif you want the adaptive behaviour to actually matter.
References
Perry J. Kaufman, Smarter Trading, McGraw-Hill, 1995 (book-length introduction); reprinted in Kaufman's Trading Systems and Methods across multiple editions, where the squared-SC choice is justified empirically.
See also
- Indicator-Ema — the two endpoints (
fastandslow) KAMA interpolates between. - Indicator-Hma — the other "smart" trend filter in Wickra.
- Indicators-Overview — the full taxonomy.