F13c: restructure the indicator catalogue into eight families

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.
This commit is contained in:
kingchenc
2026-05-22 21:21:56 +02:00
parent 6643f7a81d
commit d2f99efd78
78 changed files with 612 additions and 616 deletions
@@ -0,0 +1,213 @@
# DEMA
> Double Exponential Moving Average — Patrick Mulloy's `2·EMA EMA(EMA)`,
> a single-line trend filter that removes the first-order lag of a plain
> EMA.
## 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 | `period` is required (no default in either binding) |
| Warmup period | `2·period 1` |
| Interpretation | EMA-style smoothing with less lag; sits ahead of `Ema` on a sustained trend. |
## Formula
Let `EMA1 = EMA(price, period)` and `EMA2 = EMA(EMA1, period)`. Then:
```
DEMA_t = 2 * EMA1_t - EMA2_t
```
Both inner EMAs use the same `period`, hence the same
`α = 2 / (period + 1)`. The subtraction is a finite-difference
approximation of "remove the lag introduced by single EMA smoothing":
if EMA lags the true series by `L`, then EMA(EMA) lags by roughly `2L`,
so `2·EMA EMA(EMA)` cancels most of the first-order error.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|---------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Period shared by both internal EMAs. `period = 0` errors with `Error::PeriodZero`. |
(Python class `wickra.DEMA(period)` has no `#[pyo3(signature)]` default;
pass `period` explicitly.)
## Inputs / Outputs
From `crates/wickra-core/src/indicators/dema.rs`:
```rust
impl Indicator for Dema {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
```
Python `update` returns `float | None`, `batch` returns a 1-D
`numpy.ndarray` (`float64`, `NaN` for warmup). Node `update` returns
`number | null`, `batch` returns `Array<number>` with `NaN` placeholders.
## Warmup
`Dema::new(period).warmup_period() == 2 * period - 1`. The comment in
the source explains it cleanly:
> EMA1 seeds at `period`, then EMA2 needs another `period 1` values to
> seed.
`Ema::new(period)` only starts producing output once it has seen
`period` inputs. So `ema1` emits its first value at input `period`. From
that point on, `ema2` starts receiving inputs (the outputs of `ema1`)
and itself needs `period` of them to seed — first emission at "input
`period` of `ema1`" = input `2·period 1` of `Dema`. For
`Dema::new(14)` this gives `27`, matching the table in
[Warmup Periods](../../Warmup-Periods.md).
The implementation uses the `?` operator to short-circuit:
`let e1 = self.ema1.update(input)?; let e2 = self.ema2.update(e1)?;`,
so `ema2` is only fed once `ema1` actually emits — which is exactly
what the warmup arithmetic above models.
## Edge cases
- **Constant series.** Feeding `[100.0; n]` eventually produces
`Some(100.0)`: once both EMAs converge to `100.0`, the output is
`2 · 100 100 = 100`. The unit test `constant_series_yields_constant_dema`
pins this with `Dema::new(5)` over 60 constants.
- **NaN / infinity inputs.** Inherited from the inner `Ema`: non-finite
inputs are silently dropped and the previously emitted value (if any)
is preserved. Inputs that fail to pass `is_finite()` never reach the
`2·EMA1 EMA2` arithmetic.
- **Reset.** `dema.reset()` resets both internal EMAs. The next `update`
starts a full `2·period 1` warmup countdown.
## Examples
### Rust
```rust
use wickra::{BatchExt, Dema, Indicator};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut dema = Dema::new(5)?;
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
let out: Vec<Option<f64>> = dema.batch(&prices);
println!("warmup_period = {}", dema.warmup_period());
println!("{:?}", out);
Ok(())
}
```
Output:
```
warmup_period = 9
[None, None, None, None, None, None, None, None, Some(9.0), Some(10.0), Some(11.0), Some(12.0), Some(13.000000000000002), Some(14.000000000000002), Some(15.000000000000002), Some(16.000000000000004), Some(17.0), Some(18.0), Some(19.0), Some(20.0)]
```
The first `Some` arrives at index 8 (the 9th input), exactly as
predicted by `2·5 1 = 9`. On a linear ramp `1, 2, …, 20`, DEMA tracks
the input ramp almost perfectly because the lag has been cancelled to
first order — the floating-point tail of `13.000000000000002` is
ordinary IEEE-754 drift. The unit test
`linear_uptrend_dema_above_ema_eventually` pins the property that
`Dema` exceeds `Ema` of the same period on a sustained uptrend.
### Python
```python
import numpy as np
import wickra as ta
dema = ta.DEMA(5)
out = dema.batch(np.arange(1.0, 21.0))
print("warmup_period =", dema.warmup_period())
print(out)
```
Output:
```
warmup_period = 9
[nan nan nan nan nan nan nan nan 9. 10. 11. 12. 13. 14. 15. 16. 17. 18.
19. 20.]
```
### Node
```javascript
const ta = require('wickra');
const dema = new ta.DEMA(5);
const prices = Array.from({ length: 20 }, (_, i) => i + 1);
console.log(dema.batch(prices));
console.log('warmupPeriod:', dema.warmupPeriod());
```
Output:
```
[
NaN, NaN,
NaN, NaN,
NaN, NaN,
NaN, NaN,
9, 10,
11, 12,
13.000000000000002, 14.000000000000002,
15.000000000000002, 16.000000000000004,
17, 18,
19, 20
]
warmupPeriod: 9
```
## Interpretation
`Dema` is the canonical "I want EMA, but with less lag" answer. On a
sustained directional trend the DEMA line sits ahead of an `Ema` of the
same period (the unit test pins this). The same signals you use for
`Ema` — price-vs-MA crossover, fast-vs-slow MA crossover — apply, and
they fire earlier. In return for the lower lag you accept more
sensitivity to noise: on choppy data DEMA will whipsaw earlier than EMA
of the same period.
Prefer `Dema` over `Ema` when you want a faster trend filter without
moving to a smaller `period` (which would also amplify noise). Prefer
`Tema` for *even* less lag at the cost of further noise sensitivity, or
`Hma` if you want lag reduction *plus* an inherent smoothing step.
## Common pitfalls
- **Picking a `period` that's too short for a noisy market.** Because
`Dema` removes lag rather than adding smoothing, on choppy series it
amplifies high-frequency oscillations. If you reach for `Dema(5)` on
a tick-by-tick feed and get a jittery line, the fix is to *raise*
`period``Dema(20)` is often a better compromise than `Dema(5)`.
- **Assuming the first `Dema` value lines up with the first `Ema`
value at the same period.** `Ema(14)` first emits at input 14;
`Dema(14)` first emits at input 27. If you align a DEMA series to an
EMA series in a backtest, account for the offset or use the
`~np.isnan(...)` mask (Python) / `is_some()` filter (Rust) to drop the
warmup rows.
## References
Patrick G. Mulloy, *"Smoothing Data with Faster Moving Averages"*,
**Technical Analysis of Stocks & Commodities**, January 1994 (DEMA), and
*"Smoothing Data with Less Lag"*, **Technical Analysis of Stocks &
Commodities**, February 1994 (TEMA).
## See also
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — the building block.
- [Indicator-Tema.md](../moving-averages/Indicator-Tema.md) — three-EMA version, less lag still.
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — same lag-reduction goal, built on WMAs.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,200 @@
# EMA
> Exponential Moving Average with smoothing factor `α = 2 / (period + 1)`,
> seeded from the SMA of the first `period` inputs (the TA-Lib convention).
## 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 | `period` is required; or `Ema::with_alpha(α)` for a custom smoothing factor |
| Warmup period | `period` |
| Interpretation | Smoother, less laggy than `Sma` of the same length. |
## Formula
For `t >= period` (after warmup):
```
α = 2 / (period + 1)
seed = (1 / period) * Σ_{i=0}^{period-1} price_i // SMA of first `period` inputs
EMA_t = α * price_t + (1 - α) * EMA_{t-1} // recursive update
```
The first emitted value (at input `period`) is the seed itself, identical
to `Sma::new(period)` on the same prefix. From input `period + 1` onward
the recursive formula takes over. (`Ema::with_alpha(α)` skips the seed and
uses the very first input as the initial state, so `warmup_period() == 1`
in that mode — see the `with_alpha` method for details.)
Wilder's smoothing (used by `Rsi`/`Atr`/`Adx`) uses `α = 1/period`
instead; that is a different smoothing constant and a different
indicator family.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|----------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Window length used to derive `α`. `period = 0` errors with `Error::PeriodZero`. |
| `α` (alternative constructor `Ema::with_alpha`) | `f64` | none | `(0.0, 1.0]` and finite | Custom smoothing factor; bypasses the period-derived α. Reported `period` is 1, `warmup_period() == 1`. Invalid `α` errors with `Error::InvalidPeriod`. |
(The Python class `wickra.EMA(period)` does not set a `#[pyo3(signature)]`
default; the period must be passed explicitly. `with_alpha` is Rust-only.)
## Inputs / Outputs
From `crates/wickra-core/src/indicators/ema.rs`:
```rust
impl Indicator for Ema {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
```
Python streams as `float | None`, batches as a 1-D `numpy.ndarray`
(`NaN` for warmup). Node streams as `number | null`, batches as
`Array<number>` with `NaN` placeholders.
## Warmup
`Ema::new(period).warmup_period() == period`. The first non-empty value
is the SMA of the first `period` inputs (the "seed"); from there each
new input contributes `α * input + (1 α) * previous`. The unit test
`warmup_returns_none_until_seed` and the test
`first_value_equals_sma_seed` pin this contract.
This is the same warmup count as `Sma::new(period)` because the seed
itself is an SMA — `Ema` is "no slower to start emitting than `Sma`, just
more reactive afterwards".
## Edge cases
- **Constant series.** Feeding `[42.0; n]` returns `Some(42.0)` from input
`period` onward; the seed is `42.0`, and `α · 42 + (1 α) · 42 = 42`.
The unit test `constant_series_converges_to_constant` pins this.
- **NaN / infinity inputs.** The first line of `update` is
`if !input.is_finite() { return self.state; }`. Non-finite inputs are
silently dropped: they do not advance warmup, do not corrupt the
state, and the previously emitted value (if any) is returned. The unit
test `ignores_non_finite_input` pins this.
- **Reset.** `ema.reset()` clears both the smoothed state and the warmup
buffer; the next `update` starts a new warmup countdown.
## Examples
### Rust
```rust
use wickra::{BatchExt, Ema, Indicator};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ema = Ema::new(3)?;
let out: Vec<Option<f64>> = ema.batch(&[1.0, 2.0, 3.0, 10.0]);
println!("{:?}", out);
println!("alpha = {}", ema.alpha());
Ok(())
}
```
Output:
```
[None, None, Some(2.0), Some(6.0)]
alpha = 0.5
```
`period = 3` gives `α = 2 / 4 = 0.5`. The seed at input 3 is the SMA of
`[1, 2, 3] = 2.0`; the next step is `0.5 · 10 + 0.5 · 2 = 6.0`. This
matches the `step_after_seed_uses_alpha_formula` unit test.
### Python
```python
import wickra as ta
ema = ta.EMA(3)
for x in [1.0, 2.0, 3.0, 10.0]:
print(x, '->', ema.update(x))
print('alpha:', ema.alpha)
print('warmup_period:', ema.warmup_period())
```
Output:
```
1.0 -> None
2.0 -> None
3.0 -> 2.0
10.0 -> 6.0
alpha: 0.5
warmup_period: 3
```
### Node
```javascript
const ta = require('wickra');
const ema = new ta.EMA(3);
for (const x of [1, 2, 3, 10]) {
console.log(x, '->', ema.update(x));
}
```
Output:
```
1 -> null
2 -> null
3 -> 2
10 -> 6
```
## Interpretation
`Ema` is the "default" smoothed trend filter for most practitioners. The
two main signals are price-vs-EMA and EMA-fast-vs-EMA-slow crossovers
(the latter is the basis of `MacdIndicator`). Compared with `Sma` at the
same period, `Ema` reacts faster to direction changes at the cost of
slightly noisier output — useful when you care about the inflection
point, not the long-run level.
Prefer `Ema` over `Sma` when you want a single-line trend filter with
moderate lag. Prefer `Dema` / `Tema` when the EMA lag is too much for
your timeframe. Prefer `Hma` when you want lag reduction *and* a built-in
noise filter (a triple WMA chain rather than a triple EMA chain).
## Common pitfalls
- **Confusing `α = 2/(n+1)` with Wilder's `α = 1/n`.** Wickra's `Ema`
uses the TA-Lib convention `α = 2/(n+1)`. The same numerical period
passed to `Rsi(14)` or `Atr(14)` uses `α = 1/14 ≈ 0.0714`, not
`α = 2/15 ≈ 0.1333`. They are different smoothing schemes; comparing
an EMA(14) line directly to the RSI/ATR's internal smoothing will not
match. If you want a Wilder-style EMA, build it on top of `Ema` with
the custom factor: `Ema::with_alpha(1.0 / 14.0)`.
- **Assuming the first emitted EMA is "the EMA".** The first value is
the SMA seed, not a recursively-smoothed EMA. The series only starts
behaving like an EMA from input `period + 1` onward. For short series,
this means the first emission tracks `Sma::new(period)` exactly — that
is the intended behaviour, not a bug.
## References
The TA-Lib seeding convention used here ("EMA is seeded with an SMA")
is documented in the TA-Lib source and replicated by virtually every
commercial charting platform. The recursive form
`EMA_t = α · price + (1 α) · EMA_{t-1}` is the standard exponential
smoothing identity attributed to Robert Brown (1956).
## See also
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — equal weights, identical seed.
- [Indicator-Dema.md](../moving-averages/Indicator-Dema.md) — `2·EMA EMA(EMA)`.
- [Indicator-Tema.md](../moving-averages/Indicator-Tema.md) — `3·EMA 3·EMA(EMA) + EMA(EMA(EMA))`.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,228 @@
# HMA
> Hull Moving Average — Alan Hull's
> `WMA(2·WMA(n/2) WMA(n), √n)`, a near-lag-free trend filter that
> combines a fast `Wma(n/2)`, a slow `Wma(n)`, and a final smoothing pass.
## 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 | `period` is required (no default in either binding) |
| Warmup period (`warmup_period()`) | `period + round(√period).max(1) 1` — exact first-emission index |
| Interpretation | Near-zero-lag trend line with an inherent smoothing step. |
## Formula
```
half = max(period / 2, 1) // integer division
smooth = max(round(sqrt(period)), 1) // nearest integer, floor at 1
raw_t = 2 * WMA(price, half)_t - WMA(price, period)_t
HMA_t = WMA(raw, smooth)_t
```
The "magic" is the `2·WMA(n/2) WMA(n)` step: the fast WMA leads the
slow WMA on a trend, so doubling the fast and subtracting the slow
produces a series that is *ahead* of the input by roughly the WMA lag.
The final `WMA(…, √n)` then smooths the resulting overshoot back down
to a clean line. For `period = 9` this gives `half = 4`, `smooth = 3`;
for `period = 14`, `half = 7`, `smooth = 4`.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|---------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Top-level lookback. The inner WMA periods are derived from it. `period = 0` errors with `Error::PeriodZero`. |
(Python class `wickra.HMA(period)` has no `#[pyo3(signature)]` default;
pass `period` explicitly.)
## Inputs / Outputs
From `crates/wickra-core/src/indicators/hma.rs`:
```rust
impl Indicator for Hma {
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` / `Array<number>` with
`NaN`.
## Warmup
`warmup_period()` returns:
```
period + round(sqrt(period)).max(1) - 1
```
which gives `11` for `Hma::new(9)`, `17` for `Hma::new(14)`,
`19` for `Hma::new(16)`. This figure is **exact**: the first non-`None`
output lands on input `warmup_period()` (index `warmup_period() - 1`).
The number reflects how the three inner WMAs warm up *in parallel*: the
slow `WMA(period)` emits at input `period`, then the smoothing
`WMA(√period)` needs `√period 1` more inputs on top.
```rust
fn update(&mut self, input: f64) -> Option<f64> {
// Both raw WMAs are fed unconditionally so neither delays the other.
let h = self.half_wma.update(input);
let f = self.full_wma.update(input);
match (h, f) {
(Some(h), Some(f)) => self.smooth_wma.update(2.0 * h - f),
_ => None,
}
}
```
`half_wma` and `full_wma` receive every input, so `full_wma` emits at
input `period` (not later). The `2·half full` diff then flows into
`smooth_wma`, which needs `round(√period)` of those — giving a first
emission at exactly `period + round(√period) 1`.
| `period` | `round(√period)` | `warmup_period()` | First emission (input #) |
|----------|------------------|-------------------|--------------------------|
| 9 | 3 | 11 | 11 |
| 14 | 4 | 17 | 17 |
| 16 | 4 | 19 | 19 |
This is pinned by the `first_emission_matches_warmup_period` test in
`hma.rs`: the first call that returns `Some` is exactly at
`warmup_period() - 1` (0-indexed).
## Edge cases
- **Constant series.** Feeding `[10.0; n]` produces `Some(10.0)` once
the chain is warm. All three WMAs converge to `10`, so
`raw = 2·10 10 = 10`, then `WMA(10, smooth) = 10`. The unit test
`constant_series_yields_constant_hma` pins this with `Hma::new(9)`
over 80 constants.
- **NaN / infinity inputs.** Inherited from the inner `Wma`: non-finite
inputs are silently dropped at the half/full WMA boundary and never
reach the `2·h f` arithmetic.
- **Reset.** `hma.reset()` resets all three internal WMAs; the next
`update` starts a full warmup countdown.
## Examples
### Rust
```rust
use wickra::{BatchExt, Hma, Indicator};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut hma = Hma::new(9)?;
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
let out: Vec<Option<f64>> = hma.batch(&prices);
println!("warmup_period = {}", hma.warmup_period());
println!("{:?}", out);
Ok(())
}
```
Output:
```
warmup_period = 11
[None, None, None, None, None, None, None, None, None, None, Some(11.0), Some(12.0), Some(13.0), Some(14.0), Some(15.0), Some(16.0), Some(17.0), Some(18.0), Some(19.0), Some(20.0)]
```
The first `Some` lands at index 10 (the 11th input) — exactly
`warmup_period() - 1`, as the [Warmup](#warmup) section explains. On the
linear ramp `1, 2, …, 20`, HMA tracks price exactly with no visible lag.
### Python
```python
import numpy as np
import wickra as ta
hma = ta.HMA(9)
out = hma.batch(np.arange(1.0, 21.0))
print("warmup_period =", hma.warmup_period())
print(out)
```
Output:
```
warmup_period = 11
[nan nan nan nan nan nan nan nan nan nan 11. 12. 13. 14. 15. 16. 17. 18.
19. 20.]
```
### Node
```javascript
const ta = require('wickra');
const hma = new ta.HMA(9);
const prices = Array.from({ length: 20 }, (_, i) => i + 1);
console.log(hma.batch(prices));
console.log('warmupPeriod:', hma.warmupPeriod());
```
Output:
```
[
NaN, NaN, NaN, NaN, NaN, NaN,
NaN, NaN, NaN, NaN, 11, 12,
13, 14, 15, 16, 17, 18,
19, 20
]
warmupPeriod: 11
```
## Interpretation
`Hma` is the lag-reduction trend filter that does *not* require you to
choose between responsiveness and noise: the final `WMA(√period)` pass
is a built-in smoothing step that prevents the kind of whipsaw a `Tema`
of the same period would produce on noisy data. On clean trending data
it sits effectively on top of price; on choppy data the smoothing pass
keeps the line readable.
The textbook signal is colour-coded slope: HMA turning up = uptrend,
turning down = downtrend. Crossover patterns (`Hma(9)` vs `Hma(20)`)
also work and tend to be cleaner than the equivalent EMA pair.
Prefer `Hma` over `Dema` / `Tema` when your data is noisy enough that
the lag-reduction in those would manifest as whipsaws. Prefer `Tema` /
`Dema` on cleaner data where you want one fewer smoothing step.
## Common pitfalls
- **Mis-reading the warmup as a lag.** `warmup_period()` is the exact
first-emission index (`Hma::new(9).warmup_period() == 11`, first
`Some` at the 11th input), so it can be used directly for `Chain`
alignment. The leading `None`/`NaN` values are warmup, not lag — once
HMA emits it tracks price with near-zero lag.
- **Picking `period = 2` or `3`.** The inner `half = period / 2` is an
integer division floored at 1. For `period = 2`, `half = 1`,
`smooth = 1`, and you essentially end up with `Wma(2·price WMA(2))`
which is a sharp, noisy line. HMA is designed for `period >= 9` or so;
for shorter lookbacks reach for `Ema(period)` or `Wma(period)` instead.
## References
Alan Hull, *"How to Reduce Lag in a Moving Average"*, 2005 — the
original HMA derivation, hosted on Hull's site at
<https://alanhull.com/hull-moving-average>.
## See also
- [Indicator-Wma.md](../moving-averages/Indicator-Wma.md) — the building block.
- [Indicator-Tema.md](../moving-averages/Indicator-Tema.md) — same lag-reduction goal, EMA-based.
- [Indicator-Kama.md](../moving-averages/Indicator-Kama.md) — adaptive smoothing instead of fixed.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,250 @@
# 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`:
```rust
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](../../Warmup-Periods.md).
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]` produces `Some(100.0)`:
both `direction` and `volatility` are zero, the source branches
`if volatility == 0.0 { 0.0 } else { ... }` so `ER = 0`,
`SC = slow_sc² ≈ 0.00416`, and
`100 + 0.00416 · (100 100) = 100`. The unit test
`constant_series_yields_constant_kama` pins this with
`Kama::classic()`.
- **NaN / infinity inputs.** The first line of `update` is
`if !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 next `update` starts a fresh `er_period + 1` warmup
countdown.
## Examples
### Rust
```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
```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
```javascript
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 default `er_period = 10`), but
the seed `KAMA_{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 35
emissions are noticeably below (or above, depending on direction) the
current price. If that matters, drop the first `er_period` post-warmup
emissions, not just the warmup itself.
- **Tuning `fast` and `slow` independently of `er_period`.** Kaufman's
derivation assumes `slow >> fast` so that the per-bar SC has room to
move. Picking, say, `(10, 5, 6)` gives `fast_sc ≈ 0.333` and
`slow_sc ≈ 0.286`, so SC barely changes regardless of the efficiency
ratio — KAMA degenerates into "an EMA somewhere around period 6".
Keep `slow` at least `5×` `fast` if 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](../moving-averages/Indicator-Ema.md) — the two endpoints (`fast` and
`slow`) KAMA interpolates between.
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — the other "smart" trend filter in
Wickra.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,183 @@
# SMA
> Simple Moving Average — the equal-weighted rolling mean of the last
> `period` closes, maintained as an O(1) rolling-sum state machine.
## 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 | `period` is required (no default in either binding) |
| Warmup period | `period` |
| Interpretation | Smoothed price level; price-vs-SMA crossings flag direction changes. |
## Formula
```
SMA_t = (1 / n) * Σ_{i=0}^{n-1} price_{t-i}
```
where `n = period`. Maintained incrementally as `sum -= window.pop_front();
sum += new_price; out = sum / n`, so `update` is O(1) regardless of
`period`.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|----------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Length of the rolling window. `period = 0` errors with `Error::PeriodZero`. `period = 1` is a pass-through. |
(There is no Python `#[pyo3(signature = …)]` default for `SMA`, so
`wickra.SMA(period)` requires the period explicitly.)
## Inputs / Outputs
From `crates/wickra-core/src/indicators/sma.rs`:
```rust
impl Indicator for Sma {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
```
A single `f64` close in, an `Option<f64>` out. The Python binding maps
this to `float | None` (streaming) or a `numpy.ndarray` of dtype
`float64` with `NaN` for warmup rows (batch). The Node binding maps it to
`number | null` / `Array<number>` with `NaN` for warmup.
## Warmup
`Sma::new(period).warmup_period() == period`. The first non-empty value
is emitted on the `period`-th `update()` call, because the window needs to
hold exactly `period` values before the mean is defined. There is no
seeding step beyond filling the window — `Sma` only ever stores its
running sum and the `VecDeque` of values, so its readiness condition is
literally `window.len() == period`.
## Edge cases
- **Constant series.** Feeding `[7.0; n]` returns `Some(7.0)` from input
`period` onward; the running-sum bookkeeping is exact for constants
(the unit test `constant_series_yields_constant_sma` pins this).
- **NaN / infinity inputs.** The first line of `update` is
`if !input.is_finite() { return self.value(); }`. Non-finite inputs are
**silently dropped** — they do not advance the window, do not corrupt
the sum, and the previous valid value (if any) is returned. The unit
test `ignores_non_finite_input_but_keeps_state` pins this behaviour.
- **Reset.** `sma.reset()` clears the window and the sum, returning the
indicator to a fresh `is_ready() == false` state. The next `update`
starts a new warmup countdown.
## Examples
### Rust
```rust
use wickra::{BatchExt, Indicator, Sma};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut sma = Sma::new(3)?;
let out: Vec<Option<f64>> = sma.batch(&[2.0, 4.0, 6.0, 8.0, 10.0]);
println!("{:?}", out);
println!("warmup_period = {}", sma.warmup_period());
Ok(())
}
```
Output:
```
[None, None, Some(4.0), Some(6.0), Some(8.0)]
warmup_period = 3
```
The first two inputs return `None` while the window fills; the third
emits `(2 + 4 + 6) / 3 = 4.0` and every subsequent input slides the
window by one. This matches the `known_reference_values` test in
`crates/wickra-core/src/indicators/sma.rs`.
### Python
```python
import numpy as np
import wickra as ta
sma = ta.SMA(3)
print(sma.batch(np.array([2.0, 4.0, 6.0, 8.0, 10.0])))
print("warmup_period =", sma.warmup_period())
```
Output:
```
[nan nan 4. 6. 8.]
warmup_period = 3
```
Warmup rows come back as `NaN` so the result aligns 1:1 with the input
array.
### Node
```javascript
const ta = require('wickra');
const sma = new ta.SMA(3);
console.log(sma.batch([2, 4, 6, 8, 10]));
console.log('warmupPeriod:', sma.warmupPeriod());
```
Output:
```
[ NaN, NaN, 4, 6, 8 ]
warmupPeriod: 3
```
## Interpretation
`Sma` is a smoothed price level. The two canonical signals are:
1. **PriceSMA crossover.** Close above the SMA suggests an uptrend, close
below suggests a downtrend. The longer the SMA, the slower (and more
trustworthy) the signal.
2. **Two-SMA crossover.** A fast SMA crossing above a slow SMA is the
classic "golden cross"; below is the "death cross". Either of `Ema`
or `Hma` will give earlier (but noisier) signals at the same period.
Prefer `Sma` when you want the simplest possible reference price — for
example, as the middle band of [`BollingerBands`](../../Indicators-Overview.md),
which uses an SMA by construction. Prefer `Ema` if you want the same
smoothness profile but slightly less lag on direction changes.
## Common pitfalls
- **Treating `period = 0` as "use a default".** `Sma::new(0)` returns
`Err(Error::PeriodZero)` in Rust and a `ValueError` in Python; there is
no implicit default. Pass an explicit period.
- **Slicing batch results with `> warmup_period` instead of
`~np.isnan(...)`.** In Python the batch output has `NaN` for warmup
rows; in Rust it has `None`. Use the warmup-aware mask to filter — see
the [Quickstart: Python](../../Quickstart-Python.md#macd-a-multi-column-indicator-and-its-warmup-nans)
pattern. Slicing by `prices.size - warmup_period` works for a single
indicator but breaks the moment you compose two of them via `Chain`.
## References
The simple moving average predates technical analysis as a discipline.
The implementation here follows the standard "rolling sum, slide on each
update" formulation; the matching reference implementations are TA-Lib
and pandas (`rolling(period).mean()`).
## See also
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — same smoothness budget, less lag.
- [Indicator-Wma.md](../moving-averages/Indicator-Wma.md) — linear weights instead of equal.
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — built on three WMAs for near-zero
lag.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,170 @@
# SMMA
> Smoothed Moving Average — Wilder's running moving average (RMA): an
> SMA-seeded exponential average with a slow `1 / period` smoothing factor.
## 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 | `period` is required (no default in either binding) |
| Warmup period | `period` |
| Interpretation | Heavily smoothed price level; the average underlying Wilder's RSI and ATR. |
## Formula
```
SMMA_period = SMA(price_1 … price_period) (seed)
SMMA_t = (SMMA_{t-1} * (period - 1) + price_t) / period (t > period)
```
This is algebraically an exponential moving average with smoothing factor
`alpha = 1 / period` — substantially slower than the `Ema` factor of
`2 / (period + 1)` at the same `period`. The recurrence is O(1): each
`update` touches only the previous value.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|---------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Smoothing length. `period = 0` errors with `Error::PeriodZero`. `period = 1` is a pass-through. |
There is no Python `#[pyo3(signature = …)]` default for `SMMA`, so
`wickra.SMMA(period)` requires the period explicitly.
## Inputs / Outputs
From `crates/wickra-core/src/indicators/smma.rs`:
```rust
impl Indicator for Smma {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
```
A single `f64` close in, an `Option<f64>` out. Python maps this to
`float | None` (streaming) or a `numpy.ndarray` with `NaN` warmup rows
(batch); Node maps it to `number | null` / `Array<number>` with `NaN`
warmup.
## Warmup
`Smma::new(period).warmup_period() == period`. The first `period - 1`
inputs are buffered while the seed accumulates; the `period`-th `update()`
emits the simple average of those inputs as `SMMA_period`. Every later
input applies the `(prev·(n1)+x)/n` recurrence.
## Edge cases
- **Constant series.** Feeding `[7.0; n]` returns `Some(7.0)` from input
`period` onward — the recurrence is a fixed point for constants
(`constant_series_yields_the_constant` pins this).
- **NaN / infinity inputs.** The first line of `update` is
`if !input.is_finite() { return self.current; }`. Non-finite inputs are
**silently dropped** — they neither advance the seed nor perturb the
recurrence, and the previous valid value (if any) is returned.
- **Reset.** `smma.reset()` clears the seed buffer and the current value,
restarting the warmup countdown.
## Examples
### Rust
```rust
use wickra::{BatchExt, Indicator, Smma};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut smma = Smma::new(3)?;
let out: Vec<Option<f64>> = smma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
println!("{:?}", out);
println!("warmup_period = {}", smma.warmup_period());
Ok(())
}
```
Output:
```
[None, None, Some(2.0), Some(2.6666666666666665), Some(3.4444444444444446)]
warmup_period = 3
```
The third input emits the seed `(1 + 2 + 3) / 3 = 2.0`; the fourth applies
`(2.0·2 + 4) / 3 = 8/3`; the fifth `(8/3·2 + 5) / 3 = 31/9`. This matches
the `warmup_then_recurrence` test in
`crates/wickra-core/src/indicators/smma.rs`.
### Python
```python
import numpy as np
import wickra as ta
smma = ta.SMMA(3)
print(smma.batch(np.array([1.0, 2.0, 3.0, 4.0, 5.0])))
print("warmup_period =", smma.warmup_period())
```
Output:
```
[ nan nan 2. 2.6666667 3.4444444]
warmup_period = 3
```
### Node
```javascript
const ta = require('wickra');
const smma = new ta.SMMA(3);
console.log(smma.batch([1, 2, 3, 4, 5]));
console.log('warmupPeriod:', smma.warmupPeriod());
```
Output:
```
[ NaN, NaN, 2, 2.6666666666666665, 3.4444444444444446 ]
warmupPeriod: 3
```
## Interpretation
`Smma` is a very smooth, lag-heavy price level. Because its smoothing
factor is `1 / period` rather than `2 / (period + 1)`, an `Smma(n)` is
roughly as smooth as an `Ema(2n 1)` — useful when you want maximum
noise rejection from a single line. Its main role in this library,
however, is structural: it is the exact smoothing kernel inside
[`Rsi`](../momentum-oscillators/Indicator-Rsi.md) and [`Atr`](../volatility-bands/Indicator-Atr.md),
so reaching for `Smma` directly lets you reproduce Wilder-style averages
on any series.
## Common pitfalls
- **Confusing it with `Ema` at the same period.** `Smma(n)` and `Ema(n)`
are *not* interchangeable — `Smma` lags far more. Match `Ema(2n 1)`
if you need comparable smoothness.
- **Treating `period = 0` as "use a default".** `Smma::new(0)` returns
`Err(Error::PeriodZero)` in Rust and a `ValueError` in Python; pass an
explicit period.
## References
The smoothed moving average is J. Welles Wilder Jr.'s running average
from *New Concepts in Technical Trading Systems* (1978); it is the
averaging step in his RSI, ATR and ADX. The implementation here follows
the standard SMA-seeded formulation, matching TA-Lib's `RMA`.
## See also
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — faster exponential average.
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — the equal-weighted mean used as
the SMMA seed.
- [Indicator-Trima.md](../moving-averages/Indicator-Trima.md) — the other F1 average.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,171 @@
# T3
> Tillson T3 — a six-fold cascaded EMA recombined with a volume factor `v`
> to give a smooth, low-lag trend line.
## 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 | `period` required; `v = 0.7` (Python default) |
| Warmup period | `6·period 5` |
| Interpretation | Smooth trend line with less lag than a same-period EMA. |
## Formula
T3 is the *generalised DEMA* (`GD`) applied three times. Tim Tillson's
expansion of `GD(GD(GD(price)))` over six chained EMAs — `e1 … e6`, each
of the same `period`, where `e2 = EMA(e1)`, `e3 = EMA(e2)`, … — is:
```
v2 = v², v3 = v³
c1 = v3
c2 = 3·v2 + 3·v3
c3 = 6·v2 3·v 3·v3
c4 = 1 + 3·v + v3 + 3·v2
T3 = c1·e6 + c2·e5 + c3·e4 + c4·e3
```
The four coefficients always sum to `1`, so a constant price series maps
to itself. The volume factor `v` controls the lag/overshoot trade-off:
`v = 0` collapses T3 to the plain triple-cascaded EMA `e3`; the
conventional `v = 0.7` adds a corrective hump that sharpens turns.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|---------|----------------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Length of every EMA in the cascade. `period = 0` errors with `Error::PeriodZero`. |
| `v` | `f64` | `0.7` (Python) | `[0.0, 1.0]`| Volume factor. Non-finite or out-of-range values error with `Error::InvalidPeriod`. |
The Python binding defaults `v` to `0.7` via `#[pyo3(signature = (period, v=0.7))]`;
`period` is always explicit. The Node and WASM constructors take both
arguments explicitly.
## Inputs / Outputs
From `crates/wickra-core/src/indicators/t3.rs`:
```rust
impl Indicator for T3 {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
```
A single `f64` close in, an `Option<f64>` out. Python maps this to
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
`Array<number>` (NaN warmup).
## Warmup
`T3::new(period, v).warmup_period() == 6·period 5`. Each stage of the
SMA-seeded EMA cascade adds `period 1` bars of delay: `e1` seeds at
input `period`, `e2` at `2·period 1`, …, `e6` at `6·period 5`. T3
emits its first value once `e6` is ready, since the output formula needs
`e3` through `e6`.
## Edge cases
- **Constant series.** Because `c1 + c2 + c3 + c4 = 1` for any `v`, a flat
input series produces a flat output equal to the constant
(`coefficients_sum_to_one` and `constant_series_yields_the_constant`
pin this).
- **`v = 0`.** The coefficients become `c1 = c2 = c3 = 0`, `c4 = 1`, so
`T3` is exactly the third stage of the EMA cascade
(`zero_volume_factor_collapses_to_triple_cascaded_ema` pins this).
- **NaN / infinity inputs.** Non-finite inputs are silently dropped — the
cascade is not advanced — and the previous valid value is returned.
- **Reset.** `t3.reset()` clears all six EMAs and the cached value.
## Examples
### Rust
```rust
use wickra::{BatchExt, Indicator, T3};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let prices: Vec<f64> = (1..=40).map(f64::from).collect();
let mut t3 = T3::new(3, 0.7)?;
let out = t3.batch(&prices);
println!("warmup_period = {}", t3.warmup_period());
println!("first ready index = {:?}", out.iter().position(Option::is_some));
Ok(())
}
```
Output:
```
warmup_period = 13
first ready index = Some(12)
```
`T3(3, 0.7)` warms up after `6·3 5 = 13` inputs, so the first non-`None`
output sits at index `12`. On a pure ramp the output then tracks the input
trend with a smooth, near-constant offset.
### Python
```python
import numpy as np
import wickra as ta
t3 = ta.T3(5) # v defaults to 0.7
prices = np.linspace(100.0, 140.0, 60)
out = t3.batch(prices)
print("warmup_period =", t3.warmup_period())
print("ready values:", np.count_nonzero(~np.isnan(out)))
```
Output:
```
warmup_period = 25
ready values: 36
```
### Node
```javascript
const ta = require('wickra');
const t3 = new ta.T3(5, 0.7);
const prices = Array.from({ length: 60 }, (_, i) => 100 + i);
console.log('warmupPeriod:', t3.warmupPeriod());
console.log('last:', t3.batch(prices).at(-1));
```
## Interpretation
`T3` is a "best of both" trend line — close to `Tema` in lag reduction but
visibly smoother, because the six-EMA cascade filters noise the
three-EMA `Tema` lets through. Use it as a single trend filter or as the
slow leg of a crossover where you want a clean line. Raise `v` toward `1`
for sharper turns (more overshoot), lower it toward `0` for maximum
smoothness (`v = 0` is just a triple EMA).
## Common pitfalls
- **Treating `v` as optional outside Python.** Only the Python binding
defaults `v` to `0.7`; the Rust, Node and WASM constructors require it.
- **Underestimating warmup.** `6·period 5` grows fast — a `T3(20)` needs
`115` bars before its first value.
## References
Tim Tillson, "Better Moving Averages", *Technical Analysis of Stocks &
Commodities* (1998). The six-EMA expansion and coefficient formulas here
match Tillson's published derivation and TA-Lib's `T3`.
## See also
- [Indicator-Tema.md](../moving-averages/Indicator-Tema.md) — the three-EMA relative.
- [Indicator-Dema.md](../moving-averages/Indicator-Dema.md) — the two-EMA relative.
- [Indicator-Zlema.md](../moving-averages/Indicator-Zlema.md) — low-lag average via de-lagging.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,207 @@
# TEMA
> Triple Exponential Moving Average — Mulloy's
> `3·EMA1 3·EMA2 + EMA3` (where each EMA is fed from the previous one),
> the second-order lag-reduction sibling of DEMA.
## 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 | `period` is required (no default in either binding) |
| Warmup period | `3·period 2` |
| Interpretation | Even less lag than `Dema`, at the cost of more noise sensitivity. |
## Formula
Let `EMA1 = EMA(price, period)`, `EMA2 = EMA(EMA1, period)`,
`EMA3 = EMA(EMA2, period)`. Then:
```
TEMA_t = 3 * EMA1_t - 3 * EMA2_t + EMA3_t
```
All three EMAs share the same `period`, hence the same
`α = 2 / (period + 1)`. The coefficients `(3, 3, 1)` are the
second-order finite-difference correction that removes both the
first-order and second-order EMA lag terms — they come from expanding
`(1 L)^{-3}` where `L` is the lag operator.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|---------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Period shared by all three internal EMAs. `period = 0` errors with `Error::PeriodZero`. |
(Python class `wickra.TEMA(period)` has no `#[pyo3(signature)]` default;
pass `period` explicitly.)
## Inputs / Outputs
From `crates/wickra-core/src/indicators/tema.rs`:
```rust
impl Indicator for Tema {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
```
Python `update` returns `float | None`, `batch` returns a 1-D
`numpy.ndarray` (`float64`, `NaN` for warmup). Node `update` returns
`number | null`, `batch` returns `Array<number>` with `NaN`
placeholders.
## Warmup
`Tema::new(period).warmup_period() == 3 * period - 2`. Each stacked EMA
adds `period 1` more inputs to the warmup count:
- `ema1` emits first at input `period`.
- `ema2`, fed from `ema1`, emits first at input `period + (period 1) = 2·period 1`.
- `ema3`, fed from `ema2`, emits first at input `(2·period 1) + (period 1) = 3·period 2`.
For `Tema::new(14)` this gives `40` (matches the table in
[Warmup Periods](../../Warmup-Periods.md)); for `Tema::new(5)` (the example
below) it gives `13`. The implementation uses `?` short-circuit on every
stage, so each inner EMA is only fed once the previous one emits.
## Edge cases
- **Constant series.** Feeding `[42.0; n]` produces `Some(42.0)` once all
three EMAs have converged: `3·42 3·42 + 42 = 42`. The unit test
`constant_series_yields_constant_tema` pins this with `Tema::new(5)`
over 80 constants.
- **NaN / infinity inputs.** Inherited from the inner `Ema`: non-finite
inputs are silently dropped at the `ema1` boundary and never reach the
`3·EMA1 3·EMA2 + EMA3` arithmetic.
- **Reset.** `tema.reset()` resets all three internal EMAs; the next
`update` starts a full `3·period 2` warmup countdown.
## Examples
### Rust
```rust
use wickra::{BatchExt, Indicator, Tema};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut tema = Tema::new(5)?;
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
let out: Vec<Option<f64>> = tema.batch(&prices);
println!("warmup_period = {}", tema.warmup_period());
println!("{:?}", out);
Ok(())
}
```
Output:
```
warmup_period = 13
[None, None, None, None, None, None, None, None, None, None, None, None, Some(13.0), Some(14.0), Some(15.000000000000002), Some(16.000000000000004), Some(17.000000000000007), Some(18.000000000000007), Some(19.000000000000007), Some(20.0)]
```
The first `Some` lands at index 12 (the 13th input), matching
`3·5 2 = 13`. On the linear ramp `1, 2, …, 20`, TEMA tracks the input
ramp essentially exactly because both first- and second-order lag have
been cancelled; the floating-point tail
(`15.000000000000002`, `16.000000000000004`, …) is ordinary IEEE-754
drift from the recursive subtractions.
### Python
```python
import numpy as np
import wickra as ta
tema = ta.TEMA(5)
out = tema.batch(np.arange(1.0, 21.0))
print("warmup_period =", tema.warmup_period())
print(out)
```
Output:
```
warmup_period = 13
[nan nan nan nan nan nan nan nan nan nan nan nan 13. 14. 15. 16. 17. 18.
19. 20.]
```
### Node
```javascript
const ta = require('wickra');
const tema = new ta.TEMA(5);
const prices = Array.from({ length: 20 }, (_, i) => i + 1);
console.log(tema.batch(prices));
console.log('warmupPeriod:', tema.warmupPeriod());
```
Output:
```
[
NaN, NaN,
NaN, NaN,
NaN, NaN,
NaN, NaN,
NaN, NaN,
NaN, NaN,
13, 14,
15.000000000000002, 16.000000000000004,
17.000000000000007, 18.000000000000007,
19.000000000000007, 20
]
warmupPeriod: 13
```
## Interpretation
`Tema` removes more lag than `Dema` and noticeably more than `Ema`.
On a clean trending series the line stays glued to price; on a noisy
or sideways series the same lag-cancellation amplifies the noise — TEMA
overshoots and reverses faster than DEMA, and very much faster than EMA.
The signals are the same crossover patterns: price-vs-TEMA and
fast-TEMA-vs-slow-TEMA. The `(3, 3, 1)` coefficient pattern is also
what makes `Trix` (also in this family) work — `Trix` is the percentage
change of `EMA3`, the triple-smoothed series.
Prefer `Tema` when `Dema` still feels too laggy and your data is clean
enough to tolerate the extra noise sensitivity. Prefer `Hma` if you want
a similar lag profile but with a built-in smoothing step (WMA chain
instead of EMA chain), which behaves more gracefully on noisy data.
## Common pitfalls
- **Forgetting the `3·period 2` warmup.** `Tema::new(50)` will not
emit until input 148. That is a significant chunk of any short-term
backtest. If you are running a side-by-side panel of indicators with
different warmups, filter rows on `~np.isnan(...)` (Python) /
`is_some()` (Rust) per indicator rather than picking one global
warmup cutoff.
- **Using TEMA for noisy intraday data without a smoothing step.** The
same lag-cancellation that makes TEMA attractive on clean data turns
into whipsaws on tick-by-tick feeds. Either raise `period` materially
or switch to `Hma`, which has a final WMA smoothing pass built in.
## References
Patrick G. Mulloy, *"Smoothing Data with Less Lag"*, **Technical Analysis
of Stocks & Commodities**, February 1994 (TEMA). The coefficient pattern
`(3, 3, 1)` for cancelling first- and second-order EMA lag is derived
in the same article.
## See also
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — the building block.
- [Indicator-Dema.md](../moving-averages/Indicator-Dema.md) — second-order's sibling.
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — similar lag profile, built on WMAs.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,166 @@
# TRIMA
> Triangular Moving Average — a simple moving average applied twice, which
> triangular-weights the window so the middle bars carry the most weight.
## 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 | `period` is required (no default in either binding) |
| Warmup period | `period` |
| Interpretation | Very smooth price level; the triangular weighting suppresses edge bars. |
## Formula
`TRIMA(n)` is `SMA` stacked on `SMA`. For period `n` the two lengths are:
```
odd n: n1 = n2 = (n + 1) / 2
even n: n1 = n / 2, n2 = n / 2 + 1
TRIMA_t = SMA_{n2}( SMA_{n1}(price) )_t
```
Composing two equal-weight means convolves two rectangular windows, which
yields a triangular weight profile over the original `n` closes — the
centre bar gets the largest weight, the two edges the smallest. Both
stacked SMAs are O(1), so `update` is O(1) regardless of `period`.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|---------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Window length. `period = 0` errors with `Error::PeriodZero`. `period = 1` and `period = 2` degenerate to short SMAs. |
There is no Python `#[pyo3(signature = …)]` default for `TRIMA`, so
`wickra.TRIMA(period)` requires the period explicitly.
## Inputs / Outputs
From `crates/wickra-core/src/indicators/trima.rs`:
```rust
impl Indicator for Trima {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
```
A single `f64` close in, an `Option<f64>` out. Python maps this to
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
`Array<number>` (NaN warmup).
## Warmup
`Trima::new(period).warmup_period() == period`. The inner SMA emits after
`n1` inputs; the outer SMA then needs `n2 1` more, and `n1 + n2 1 = n`
for both the odd and even splits. So the first non-`None` output lands on
exactly the `period`-th `update()`.
## Edge cases
- **Constant series.** `[42.0; n]` returns `Some(42.0)` from input
`period` onward — both SMAs are exact for constants
(`constant_series_yields_the_constant` pins this).
- **NaN / infinity inputs.** `update` returns `self.outer.value()` for a
non-finite input *without* feeding either SMA, so the inner SMA's stale
value is never double-counted into the outer SMA. State is left
untouched.
- **Reset.** `trima.reset()` resets both inner and outer SMAs, restarting
the warmup countdown.
## Examples
### Rust
```rust
use wickra::{BatchExt, Indicator, Trima};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut trima = Trima::new(5)?;
let out: Vec<Option<f64>> = trima.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
println!("{:?}", out);
println!("warmup_period = {}", trima.warmup_period());
Ok(())
}
```
Output:
```
[None, None, None, None, Some(3.0), Some(4.0), Some(5.0)]
warmup_period = 5
```
`TRIMA(5)` is `SMA(3)` of `SMA(3)`. `SMA(3)` of `1..=7` is
`[_, _, 2, 3, 4, 5, 6]`; `SMA(3)` of that is `[_, _, _, _, 3, 4, 5]`. This
matches the `odd_period_reference_values` test in
`crates/wickra-core/src/indicators/trima.rs`.
### Python
```python
import numpy as np
import wickra as ta
trima = ta.TRIMA(5)
print(trima.batch(np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0])))
print("warmup_period =", trima.warmup_period())
```
Output:
```
[nan nan nan nan 3. 4. 5.]
warmup_period = 5
```
### Node
```javascript
const ta = require('wickra');
const trima = new ta.TRIMA(5);
console.log(trima.batch([1, 2, 3, 4, 5, 6, 7]));
console.log('warmupPeriod:', trima.warmupPeriod());
```
Output:
```
[ NaN, NaN, NaN, NaN, 3, 4, 5 ]
warmupPeriod: 5
```
## Interpretation
`Trima` is one of the smoothest single-line averages in the library: the
triangular weight profile damps the most recent bar far more than a plain
`Sma` does, so whipsaws are rare. The cost is lag — a `Trima(n)` lags
roughly like an `Sma(n/2)` doubled. Use it as a slow trend filter where a
clean, low-noise line matters more than fast reaction; prefer
[`Ema`](../moving-averages/Indicator-Ema.md) or [`Hma`](../moving-averages/Indicator-Hma.md) when responsiveness
matters.
## Common pitfalls
- **Expecting `Sma`-like lag.** Stacking two means roughly doubles the
effective lag; size the period accordingly.
- **Treating `period = 0` as "use a default".** `Trima::new(0)` returns
`Err(Error::PeriodZero)` in Rust and a `ValueError` in Python.
## References
The triangular moving average is a standard double-smoothed SMA; the
odd/even split used here (`n1`, `n2`) matches TA-Lib's `TRIMA`.
## See also
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — the building block applied twice.
- [Indicator-Wma.md](../moving-averages/Indicator-Wma.md) — linear (not triangular) weights.
- [Indicator-Smma.md](../moving-averages/Indicator-Smma.md) — the other F1 average.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,175 @@
# VWMA
> Volume-Weighted Moving Average — a rolling mean of closes where each bar
> is weighted by its own traded volume.
## Quick reference
| Field | Value |
|-------|-------|
| Family | Moving Averages |
| Input type | `Candle` (uses `close` and `volume`) |
| Output type | `f64` |
| Output range | unbounded; tracks the input price scale |
| Default parameters | `period` is required (no default in either binding) |
| Warmup period | `period` |
| Interpretation | Trend line that leans toward high-conviction (high-volume) bars. |
## Formula
```
VWMA_t = Σ(close_i · volume_i) / Σ(volume_i) over the last `period` bars
```
A heavy bar pulls the average toward its close; a thin bar barely moves
it. Both the numerator (`Σ price·volume`) and denominator (`Σ volume`)
are maintained as O(1) rolling sums, so `update` is O(1) regardless of
`period`.
If **every** bar in the window has zero volume the weighted mean is
undefined (`0 / 0`). VWMA then falls back to the plain unweighted mean of
the `period` closes, so the output is always finite and defined.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|---------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Rolling window length in bars. `period = 0` errors with `Error::PeriodZero`. |
There is no Python `#[pyo3(signature = …)]` default for `VWMA`, so
`wickra.VWMA(period)` requires the period explicitly.
## Inputs / Outputs
From `crates/wickra-core/src/indicators/vwma.rs`:
```rust
impl Indicator for Vwma {
type Input = Candle;
type Output = f64;
// update(&mut self, input: Candle) -> Option<f64>
}
```
`VWMA` is a **candle-input** indicator: it reads `close` and `volume` from
each `Candle`. In Python the streaming `update` accepts a 6-tuple or a
dict; the batch helper takes `close` and `volume` numpy arrays. Node and
WASM expose `update(close, volume)` and `batch(close, volume)`.
## Warmup
`Vwma::new(period).warmup_period() == period`. The first `period 1`
candles fill the rolling window; the `period`-th `update()` produces the
first weighted mean.
## Edge cases
- **Constant closes.** Closes all equal to `c` give `VWMA = c` regardless
of the volumes (`Σ c·v / Σ v = c`), and the zero-volume fallback also
yields `c` (`constant_series_yields_the_constant` pins this).
- **Zero-volume window.** If every bar in the window has `volume = 0`,
VWMA returns the unweighted mean of the `period` closes
(`zero_volume_window_falls_back_to_unweighted_mean` pins this).
- **Candle validation.** `Candle::new` already rejects NaN/infinite fields
and negative volume, so `update` never sees an invalid bar — there is no
separate non-finite guard.
- **Reset.** `vwma.reset()` clears the window and all three rolling sums.
## Examples
### Rust
```rust
use wickra::{Candle, Indicator, Vwma};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut vwma = Vwma::new(2)?;
// (close, volume): (10, 1) then (20, 3).
let a = Candle::new(10.0, 10.0, 10.0, 10.0, 1.0, 0)?;
let b = Candle::new(20.0, 20.0, 20.0, 20.0, 3.0, 1)?;
println!("{:?}", vwma.update(a));
println!("{:?}", vwma.update(b));
Ok(())
}
```
Output:
```
None
Some(17.5)
```
The window holds two bars: `(10·1 + 20·3) / (1 + 3) = 70 / 4 = 17.5`. The
heavier bar at `20` dominates, so the result sits well above the simple
mean of `15`. This matches the `reference_value` test in
`crates/wickra-core/src/indicators/vwma.rs`.
### Python
```python
import numpy as np
import wickra as ta
vwma = ta.VWMA(2)
close = np.array([10.0, 20.0, 30.0])
volume = np.array([1.0, 3.0, 1.0])
print(vwma.batch(close, volume))
print("warmup_period =", vwma.warmup_period())
```
Output:
```
[ nan 17.5 22.5]
warmup_period = 2
```
### Node
```javascript
const ta = require('wickra');
const vwma = new ta.VWMA(2);
console.log(vwma.batch([10, 20, 30], [1, 3, 1]));
console.log('warmupPeriod:', vwma.warmupPeriod());
```
Output:
```
[ NaN, 17.5, 22.5 ]
warmupPeriod: 2
```
## Interpretation
`Vwma` is a trend line that respects participation. Compared with an
equal-weighted `Sma` of the same period, it reacts faster to moves backed
by heavy volume and lags moves on thin volume. The classic read is the
`Vwma`-vs-`Sma` relationship: `Vwma` above `Sma` means recent strength was
volume-backed (more trustworthy); `Vwma` below `Sma` means the up-moves
came on light volume. It is a session-independent cousin of
[`Vwap`](../volume/Indicator-Vwap.md) — VWAP weights by volume since the
start of the stream, VWMA over a fixed rolling window.
## Common pitfalls
- **Feeding it scalar prices.** `VWMA` needs volume; it takes a `Candle`,
not an `f64`. Use `Sma`/`Wma` for a pure price series.
- **Assuming a zero-volume window is an error.** It is not — VWMA falls
back to the unweighted mean. If that fallback matters to you, screen the
window's total volume yourself.
## References
The volume-weighted moving average is a standard volume-weighted rolling
mean; the rolling-sum formulation here matches the common pandas
implementation `(close*volume).rolling(n).sum() / volume.rolling(n).sum()`,
with an explicit zero-volume fallback added for robustness.
## See also
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — the equal-weighted counterpart.
- [Indicator-Vwap.md](../volume/Indicator-Vwap.md) — volume-weighted price
since the start of the stream.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,185 @@
# WMA
> Weighted Moving Average with linear weights `1, 2, …, period`, so the
> most recent bar carries the most weight.
## 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 | `period` is required (no default in either binding) |
| Warmup period | `period` |
| Interpretation | Front-weighted trend filter; faster than `Sma`, smoother than `Ema`. |
## Formula
```
weights = [1, 2, ..., n] // n = period
W = n * (n + 1) / 2 // sum of weights
WMA_t = (1 / W) * Σ_{i=0}^{n-1} (n - i) * price_{t-i}
= (1 / W) * (n * price_t + (n-1) * price_{t-1} + ... + 1 * price_{t-n+1})
```
Maintained in O(1) using the identity that, when sliding the window by
one, every retained element's weight drops by exactly one and the
newcomer enters at weight `n`:
```
new_weight_sum = old_weight_sum - old_value_sum + n * new_input
new_value_sum = old_value_sum - oldest_value + new_input
```
This is the bookkeeping in the steady-state branch of `update`; during
warmup the full `Σ weight·value` is computed once when the window first
fills.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|---------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Length of the rolling window. `period = 0` errors with `Error::PeriodZero`. `period = 1` is a pass-through. |
(The Python class `wickra.WMA(period)` does not set a `#[pyo3(signature)]`
default; pass the period explicitly.)
## Inputs / Outputs
From `crates/wickra-core/src/indicators/wma.rs`:
```rust
impl Indicator for Wma {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
```
Python returns `float | None` from `update` and a `numpy.ndarray`
(`float64`, `NaN` for warmup) from `batch`. Node returns `number | null`
and `Array<number>` (with `NaN` placeholders) respectively.
## Warmup
`Wma::new(period).warmup_period() == period`. Like `Sma`, the first
emission lands on the `period`-th `update()` call: the window needs
exactly `period` values for the weighted sum to be defined. There is no
seeding step beyond filling the window.
## Edge cases
- **Constant series.** For `[c; n]`, every element contributes `c · weight_i`
and the result is `c · ΣW / ΣW = c`. The proptest
`proptest_matches_naive` exercises this implicitly across many random
inputs; the textbook `period = 4` test confirms `WMA(4)` of
`[1, 2, 3, 4]` is exactly `(1·1 + 2·2 + 3·3 + 4·4) / 10 = 30 / 10 = 3.0`.
- **NaN / infinity inputs.** The first line of `update` is
`if !input.is_finite() { return self.value(); }`. Non-finite inputs are
silently dropped — they do not advance warmup, do not corrupt the
rolling sums, and the previously emitted value (if any) is returned.
- **Reset.** `wma.reset()` clears the window and both rolling sums; the
next `update` starts a new warmup countdown.
## Examples
### Rust
```rust
use wickra::{BatchExt, Indicator, Wma};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut wma = Wma::new(4)?;
let out: Vec<Option<f64>> = wma.batch(&[1.0, 2.0, 3.0, 4.0]);
println!("{:?}", out);
println!("warmup_period = {}", wma.warmup_period());
Ok(())
}
```
Output:
```
[None, None, None, Some(3.0)]
warmup_period = 4
```
The fourth input emits `(1·1 + 2·2 + 3·3 + 4·4) / (1+2+3+4) = 30 / 10 = 3.0`.
This matches the `known_values_period_4` unit test in
`crates/wickra-core/src/indicators/wma.rs`.
### Python
```python
import numpy as np
import wickra as ta
wma = ta.WMA(4)
print(wma.batch(np.array([1.0, 2.0, 3.0, 4.0])))
print("warmup_period =", wma.warmup_period())
```
Output:
```
[nan nan nan 3.]
warmup_period = 4
```
### Node
```javascript
const ta = require('wickra');
const wma = new ta.WMA(4);
console.log(wma.batch([1, 2, 3, 4]));
console.log('warmupPeriod:', wma.warmupPeriod());
```
Output:
```
[ NaN, NaN, NaN, 3 ]
warmupPeriod: 4
```
## Interpretation
`Wma` sits between `Sma` and `Ema` on the lag/responsiveness spectrum:
because the most recent bar carries weight `n` (vs `1` for the oldest),
direction changes propagate faster than in `Sma`, but the smooth linear
decay produces less of the "exponential tail" overshoot you sometimes
see with `Ema`. The same two crossover signals (price-vs-WMA and
fast-WMA-vs-slow-WMA) apply.
The most important downstream use of `Wma` inside Wickra is `Hma`:
`Hma` is built entirely from three `Wma` instances (see
[Indicator-Hma.md](../moving-averages/Indicator-Hma.md)).
## Common pitfalls
- **Mistaking linear weights for exponential ones.** A `Wma(20)` is *not*
an `Ema(20)`; the weights decay linearly `(20, 19, 18, …, 1)` rather
than geometrically, so very old bars still contribute (weight 1) where
in an EMA they would have decayed to near zero. If you want the
exponential decay, use `Ema`.
- **Comparing `Wma(period)` to a "WMA" from a different library and
finding the seed off.** Wickra's `Wma` has no separate seeding step —
it simply returns `None` until the window is full and then returns the
exact weighted mean from input `period` onward. Some libraries
pre-seed with a partial-window value; that is a different convention
and will produce different first-few-bar values.
## References
The linearly-weighted moving average is older than most named indicators
and has no single canonical citation; TA-Lib's `WMA` is the standard
reference implementation and matches Wickra's output bit-for-bit.
## See also
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — equal weights instead of linear.
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — exponential decay instead of linear.
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — Hull MA, built from three WMAs.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
@@ -0,0 +1,166 @@
# ZLEMA
> Zero-Lag Exponential Moving Average — an EMA fed a de-lagged price series
> so it tracks turns with almost no group delay.
## 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 | `period` is required (no default in either binding) |
| Warmup period | `lag + period` where `lag = (period 1) / 2` |
| Interpretation | Low-lag trend line; crossings of price react far sooner than a plain EMA. |
## Formula
```
lag = (period 1) / 2 (integer division)
de_lagged_t = 2·price_t price_{tlag}
ZLEMA_t = EMA_period(de_lagged)_t
```
The trick (Ehlers & Way, 2010): `price_t price_{tlag}` is a momentum
term. Adding it to the current price *over-shoots* in the direction of the
recent move by exactly enough to cancel the EMA's lag. The inner EMA then
smooths that de-lagged series with the usual `α = 2 / (period + 1)`.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|---------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | EMA length. `period = 0` errors with `Error::PeriodZero`. The lag offset is derived as `(period 1) / 2`. |
There is no Python `#[pyo3(signature = …)]` default for `ZLEMA`, so
`wickra.ZLEMA(period)` requires the period explicitly. The derived `lag`
is exposed as a read-only property.
## Inputs / Outputs
From `crates/wickra-core/src/indicators/zlema.rs`:
```rust
impl Indicator for Zlema {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
```
A single `f64` close in, an `Option<f64>` out. Python maps this to
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
`Array<number>` (NaN warmup).
## Warmup
`Zlema::new(period).warmup_period() == lag + period`. The de-lagged series
is undefined until `lag` prior inputs exist, so it produces its first
value on input `lag + 1`; the inner EMA then needs `period` de-lagged
values to seed. The first non-`None` output therefore lands on input
`lag + period`.
## Edge cases
- **Constant series.** De-lagging a constant gives the same constant
(`2c c = c`), so `ZLEMA` of a flat series is flat
(`constant_series_yields_the_constant` pins this).
- **NaN / infinity inputs.** Non-finite inputs are silently dropped: the
rolling lag buffer is not advanced and the inner EMA is not fed, so the
previous valid value (if any) is returned.
- **`period = 1`.** `lag = 0`, the de-lagged series equals the raw price,
and `ZLEMA(1)` degenerates to a pass-through.
- **Reset.** `zlema.reset()` clears the lag buffer and the inner EMA.
## Examples
### Rust
```rust
use wickra::{BatchExt, Indicator, Zlema};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut zlema = Zlema::new(3)?;
let out: Vec<Option<f64>> = zlema.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
println!("{:?}", out);
println!("lag = {}, warmup_period = {}", zlema.lag(), zlema.warmup_period());
Ok(())
}
```
Output:
```
[None, None, None, Some(4.0), Some(5.0)]
lag = 1, warmup_period = 4
```
`ZLEMA(3)` has `lag = 1`. The de-lagged series of `[1,2,3,4,5]` is
`[_, 3, 4, 5, 6]`; `EMA(3)` of that seeds at `mean(3,4,5) = 4.0`, then
`0.5·6 + 0.5·4 = 5.0`. This matches the `reference_values` test in
`crates/wickra-core/src/indicators/zlema.rs`.
### Python
```python
import numpy as np
import wickra as ta
zlema = ta.ZLEMA(3)
print(zlema.batch(np.array([1.0, 2.0, 3.0, 4.0, 5.0])))
print("lag =", zlema.lag, "warmup_period =", zlema.warmup_period())
```
Output:
```
[nan nan nan 4. 5.]
lag = 1 warmup_period = 4
```
### Node
```javascript
const ta = require('wickra');
const zlema = new ta.ZLEMA(3);
console.log(zlema.batch([1, 2, 3, 4, 5]));
console.log('warmupPeriod:', zlema.warmupPeriod());
```
Output:
```
[ NaN, NaN, NaN, 4, 5 ]
warmupPeriod: 4
```
## Interpretation
`Zlema` is a low-lag trend line. Use it where an `Ema` would lag too much
into a reversal — for example as the fast leg of a crossover system, or
as a trailing reference that should react quickly. The momentum injection
that removes the lag also makes `Zlema` overshoot on sharp spikes, so it
is noisier than the `Ema` it is built on; pair it with a slower filter if
whipsaws are a concern.
## Common pitfalls
- **Expecting `Ema`-identical values.** `Zlema` is deliberately *not* an
`Ema` — it leads price. The two only coincide for `period = 1`.
- **Forgetting the extra warmup.** Warmup is `lag + period`, not `period`;
budget `(period 1) / 2` extra bars before the first output.
## References
John Ehlers and Ric Way, "Zero Lag (Well, Almost)", *Technical Analysis
of Stocks & Commodities* (2010). The implementation here uses the standard
`lag = (period 1) / 2` and an SMA-seeded inner EMA.
## See also
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — the inner average ZLEMA de-lags.
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — another low-lag average, via WMAs.
- [Indicator-T3.md](../moving-averages/Indicator-T3.md) — low-lag average via a six-EMA cascade.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.