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:
@@ -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 3–5
|
||||
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. **Price–SMA 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·(n−1)+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_{t−lag}
|
||||
ZLEMA_t = EMA_period(de_lagged)_t
|
||||
```
|
||||
|
||||
The trick (Ehlers & Way, 2010): `price_t − price_{t−lag}` 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.
|
||||
Reference in New Issue
Block a user