The original taxonomy was four classical families plus a statistics group, with the F1-F12 expansion slotted in as sub-categories. This regroups the whole 71-indicator catalogue into eight top-level families, each with at least five members: Moving Averages (12), Momentum Oscillators (13), Trend & Directional (9), Price Oscillators (5), Volatility & Bands (12), Trailing Stops (5), Volume (9), Price Statistics (7). - Wiki: docs/wiki/indicators/ reorganised into eight family folders; all 71 indicator pages moved with `git mv`. Every internal cross-link is normalised to `../<family>/Indicator-X.md`, each page's `Family` field is set to its new family, and two pre-existing `../Indicator-Chaining.md` links (should have been `../../`) are corrected. A link check confirms every relative wiki link resolves. - Indicators-Overview.md fully rewritten around the eight families; Home.md indicator reference and the README family table follow suit. - Warmup-Periods.md gains the eight F13 indicators; CHANGELOG records the 46-indicator expansion (25 -> 71) and the eight-family taxonomy. - Tests: Node indicators.test.js and Python test_new_indicators.py cover all eight new indicators (Node 91/91, Python 117/117 green). cargo fmt + clippy (core/wickra/data/wasm/node) clean; 508 core tests, 25 data tests and 74 doctests green.
9.8 KiB
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:
impl Indicator for Kama {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
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.md — the two endpoints (
fastandslow) KAMA interpolates between. - Indicator-Hma.md — the other "smart" trend filter in Wickra.
- Indicators-Overview.md — the full taxonomy.