E4: commit the documentation sources
The 33 Markdown files under docs/wiki/ were never tracked. Commit them
into the repository so the documentation is versioned alongside the
code: 8 top-level pages plus 25 per-indicator deep dives under
indicators/{momentum,trend,volatility,volume}/.
The pages are kept in-repo (not pushed to a flat GitHub Wiki), so the
relative indicators/<family>/... links in Home.md resolve correctly
when rendered on GitHub.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
# 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 | Trend |
|
||||
| Sub-category | Exponential family |
|
||||
| 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('D:/Coding/Wickra/bindings/node');
|
||||
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](Indicator-Ema.md) — the building block.
|
||||
- [Indicator-Tema.md](Indicator-Tema.md) — three-EMA version, less lag still.
|
||||
- [Indicator-Hma.md](Indicator-Hma.md) — same lag-reduction goal, built on WMAs.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -0,0 +1,201 @@
|
||||
# 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 | Trend |
|
||||
| Sub-category | Exponential family |
|
||||
| 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('D:/Coding/Wickra/bindings/node');
|
||||
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](Indicator-Sma.md) — equal weights, identical seed.
|
||||
- [Indicator-Dema.md](Indicator-Dema.md) — `2·EMA − EMA(EMA)`.
|
||||
- [Indicator-Tema.md](Indicator-Tema.md) — `3·EMA − 3·EMA(EMA) + EMA(EMA(EMA))`.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -0,0 +1,242 @@
|
||||
# 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 | Trend |
|
||||
| Sub-category | Adaptive & hybrid |
|
||||
| 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` — see below; the practical first-emission index can lag this number |
|
||||
| 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
|
||||
|
||||
This is the one case in the trend family where the reported
|
||||
`warmup_period()` is a **lower bound**, not the exact first-emission
|
||||
index.
|
||||
|
||||
The `warmup_period()` method 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 number assumes the three inner WMAs
|
||||
warm up *in parallel*: the slow `WMA(period)` would emit at input
|
||||
`period`, and the smoothing `WMA(√period)` would then need `√period − 1`
|
||||
more inputs.
|
||||
|
||||
In practice the implementation uses the `?` short-circuit:
|
||||
|
||||
```rust
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
let h = self.half_wma.update(input)?; // returns early if None
|
||||
let f = self.full_wma.update(input)?; // ONLY called when half emits
|
||||
let diff = 2.0 * h - f;
|
||||
self.smooth_wma.update(diff)
|
||||
}
|
||||
```
|
||||
|
||||
`self.full_wma.update(input)` is only reached after `self.half_wma`
|
||||
starts emitting (i.e. from input `half = period/2` onward). So
|
||||
`full_wma` does not see input until iteration `half`, and then needs
|
||||
`period` of its own inputs — it emits first at iteration
|
||||
`half + period − 1`. The diff then flows into `smooth_wma`, which needs
|
||||
`smooth` of those — first emission at iteration
|
||||
`half + period - 1 + smooth - 1` = `half + period + smooth − 2`.
|
||||
|
||||
For the three example periods this gives:
|
||||
|
||||
| `period` | `half` | `smooth` | `warmup_period()` (reported) | Actual first emission |
|
||||
|----------|--------|----------|------------------------------|------------------------|
|
||||
| 9 | 4 | 3 | 11 | 14 |
|
||||
| 14 | 7 | 4 | 17 | 23 |
|
||||
| 16 | 8 | 4 | 19 | 26 |
|
||||
|
||||
The numbers in the "Actual first emission" column are verified by
|
||||
streaming `Hma::new(period).update(...)` over a linear ramp and noting
|
||||
the first call that returns `Some`. The discrepancy is a known
|
||||
implementation quirk: the reported value is the theoretical floor; the
|
||||
streaming order pushes the practical emission later. If you need the
|
||||
exact first-non-`None` index for chaining or array alignment, prefer
|
||||
checking `is_ready()` or filtering on `~np.isnan(...)` after the fact.
|
||||
|
||||
## 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 (reported) = {}", hma.warmup_period());
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period (reported) = 11
|
||||
[None, None, None, None, None, None, None, None, None, None, None, None, None, Some(14.0), Some(15.0), Some(16.0), Some(17.0), Some(18.0), Some(19.0), Some(20.0)]
|
||||
```
|
||||
|
||||
The reported warmup says `11`, but the first `Some` lands at index 13
|
||||
(the 14th input) for the reason given in the [Warmup](#warmup) section.
|
||||
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 (reported) =", hma.warmup_period())
|
||||
print(out)
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period (reported) = 11
|
||||
[nan nan nan nan nan nan nan nan nan nan nan nan nan 14. 15. 16. 17. 18.
|
||||
19. 20.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('D:/Coding/Wickra/bindings/node');
|
||||
const hma = new ta.HMA(9);
|
||||
const prices = Array.from({ length: 20 }, (_, i) => i + 1);
|
||||
console.log(hma.batch(prices));
|
||||
console.log('warmupPeriod (reported):', hma.warmupPeriod());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[
|
||||
NaN, NaN, NaN, NaN, NaN, NaN,
|
||||
NaN, NaN, NaN, NaN, NaN, NaN,
|
||||
NaN, 14, 15, 16, 17, 18,
|
||||
19, 20
|
||||
]
|
||||
warmupPeriod (reported): 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
|
||||
|
||||
- **Trusting `warmup_period()` for chaining or array alignment.** As
|
||||
the table above shows, `Hma::new(9).warmup_period() == 11` but the
|
||||
first actual emission is at the 14th input. If you use HMA as the
|
||||
first stage of a `Chain`, the chain's overall warmup will lag what
|
||||
`Chain::warmup_period()` reports. Filter on `is_some()` /
|
||||
`~np.isnan(...)` after the fact, or precompute the actual index by
|
||||
streaming a small ramp once.
|
||||
- **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](Indicator-Wma.md) — the building block.
|
||||
- [Indicator-Tema.md](Indicator-Tema.md) — same lag-reduction goal, EMA-based.
|
||||
- [Indicator-Kama.md](Indicator-Kama.md) — adaptive smoothing instead of fixed.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -0,0 +1,251 @@
|
||||
# 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 | Trend |
|
||||
| Sub-category | Adaptive & hybrid |
|
||||
| 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('D:/Coding/Wickra/bindings/node');
|
||||
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](Indicator-Ema.md) — the two endpoints (`fast` and
|
||||
`slow`) KAMA interpolates between.
|
||||
- [Indicator-Hma.md](Indicator-Hma.md) — the other "smart" trend filter in
|
||||
Wickra.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -0,0 +1,184 @@
|
||||
# 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 | Trend |
|
||||
| Sub-category | Simple 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('D:/Coding/Wickra/bindings/node');
|
||||
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](Indicator-Ema.md) — same smoothness budget, less lag.
|
||||
- [Indicator-Wma.md](Indicator-Wma.md) — linear weights instead of equal.
|
||||
- [Indicator-Hma.md](Indicator-Hma.md) — built on three WMAs for near-zero
|
||||
lag.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -0,0 +1,208 @@
|
||||
# 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 | Trend |
|
||||
| Sub-category | Exponential family |
|
||||
| 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('D:/Coding/Wickra/bindings/node');
|
||||
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](Indicator-Ema.md) — the building block.
|
||||
- [Indicator-Dema.md](Indicator-Dema.md) — second-order's sibling.
|
||||
- [Indicator-Hma.md](Indicator-Hma.md) — similar lag profile, built on WMAs.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -0,0 +1,186 @@
|
||||
# WMA
|
||||
|
||||
> Weighted Moving Average with linear weights `1, 2, …, period`, so the
|
||||
> most recent bar carries the most weight.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trend |
|
||||
| Sub-category | Simple 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('D:/Coding/Wickra/bindings/node');
|
||||
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](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](Indicator-Sma.md) — equal weights instead of linear.
|
||||
- [Indicator-Ema.md](Indicator-Ema.md) — exponential decay instead of linear.
|
||||
- [Indicator-Hma.md](Indicator-Hma.md) — Hull MA, built from three WMAs.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
Reference in New Issue
Block a user