F13c: restructure the indicator catalogue into eight families

The original taxonomy was four classical families plus a statistics group,
with the F1-F12 expansion slotted in as sub-categories. This regroups the
whole 71-indicator catalogue into eight top-level families, each with at
least five members:

  Moving Averages (12), Momentum Oscillators (13), Trend & Directional (9),
  Price Oscillators (5), Volatility & Bands (12), Trailing Stops (5),
  Volume (9), Price Statistics (7).

- Wiki: docs/wiki/indicators/ reorganised into eight family folders; all 71
  indicator pages moved with `git mv`. Every internal cross-link is
  normalised to `../<family>/Indicator-X.md`, each page's `Family` field is
  set to its new family, and two pre-existing `../Indicator-Chaining.md`
  links (should have been `../../`) are corrected. A link check confirms
  every relative wiki link resolves.
- Indicators-Overview.md fully rewritten around the eight families;
  Home.md indicator reference and the README family table follow suit.
- Warmup-Periods.md gains the eight F13 indicators; CHANGELOG records the
  46-indicator expansion (25 -> 71) and the eight-family taxonomy.
- Tests: Node indicators.test.js and Python test_new_indicators.py cover
  all eight new indicators (Node 91/91, Python 117/117 green).

cargo fmt + clippy (core/wickra/data/wasm/node) clean; 508 core tests,
25 data tests and 74 doctests green.
This commit is contained in:
kingchenc
2026-05-22 21:21:56 +02:00
parent 6643f7a81d
commit d2f99efd78
78 changed files with 612 additions and 616 deletions
@@ -0,0 +1,183 @@
# SMA
> Simple Moving Average — the equal-weighted rolling mean of the last
> `period` closes, maintained as an O(1) rolling-sum state machine.
## Quick reference
| Field | Value |
|-------|-------|
| Family | Moving Averages |
| Input type | `f64` (single close) |
| Output type | `f64` |
| Output range | unbounded; tracks the input price scale |
| Default parameters | `period` is required (no default in either binding) |
| Warmup period | `period` |
| Interpretation | Smoothed price level; price-vs-SMA crossings flag direction changes. |
## Formula
```
SMA_t = (1 / n) * Σ_{i=0}^{n-1} price_{t-i}
```
where `n = period`. Maintained incrementally as `sum -= window.pop_front();
sum += new_price; out = sum / n`, so `update` is O(1) regardless of
`period`.
## Parameters
| Name | Type | Default | Valid range | Description |
|----------|----------|---------|-------------|-------------|
| `period` | `usize` | none | `>= 1` | Length of the rolling window. `period = 0` errors with `Error::PeriodZero`. `period = 1` is a pass-through. |
(There is no Python `#[pyo3(signature = …)]` default for `SMA`, so
`wickra.SMA(period)` requires the period explicitly.)
## Inputs / Outputs
From `crates/wickra-core/src/indicators/sma.rs`:
```rust
impl Indicator for Sma {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
```
A single `f64` close in, an `Option<f64>` out. The Python binding maps
this to `float | None` (streaming) or a `numpy.ndarray` of dtype
`float64` with `NaN` for warmup rows (batch). The Node binding maps it to
`number | null` / `Array<number>` with `NaN` for warmup.
## Warmup
`Sma::new(period).warmup_period() == period`. The first non-empty value
is emitted on the `period`-th `update()` call, because the window needs to
hold exactly `period` values before the mean is defined. There is no
seeding step beyond filling the window — `Sma` only ever stores its
running sum and the `VecDeque` of values, so its readiness condition is
literally `window.len() == period`.
## Edge cases
- **Constant series.** Feeding `[7.0; n]` returns `Some(7.0)` from input
`period` onward; the running-sum bookkeeping is exact for constants
(the unit test `constant_series_yields_constant_sma` pins this).
- **NaN / infinity inputs.** The first line of `update` is
`if !input.is_finite() { return self.value(); }`. Non-finite inputs are
**silently dropped** — they do not advance the window, do not corrupt
the sum, and the previous valid value (if any) is returned. The unit
test `ignores_non_finite_input_but_keeps_state` pins this behaviour.
- **Reset.** `sma.reset()` clears the window and the sum, returning the
indicator to a fresh `is_ready() == false` state. The next `update`
starts a new warmup countdown.
## Examples
### Rust
```rust
use wickra::{BatchExt, Indicator, Sma};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut sma = Sma::new(3)?;
let out: Vec<Option<f64>> = sma.batch(&[2.0, 4.0, 6.0, 8.0, 10.0]);
println!("{:?}", out);
println!("warmup_period = {}", sma.warmup_period());
Ok(())
}
```
Output:
```
[None, None, Some(4.0), Some(6.0), Some(8.0)]
warmup_period = 3
```
The first two inputs return `None` while the window fills; the third
emits `(2 + 4 + 6) / 3 = 4.0` and every subsequent input slides the
window by one. This matches the `known_reference_values` test in
`crates/wickra-core/src/indicators/sma.rs`.
### Python
```python
import numpy as np
import wickra as ta
sma = ta.SMA(3)
print(sma.batch(np.array([2.0, 4.0, 6.0, 8.0, 10.0])))
print("warmup_period =", sma.warmup_period())
```
Output:
```
[nan nan 4. 6. 8.]
warmup_period = 3
```
Warmup rows come back as `NaN` so the result aligns 1:1 with the input
array.
### Node
```javascript
const ta = require('wickra');
const sma = new ta.SMA(3);
console.log(sma.batch([2, 4, 6, 8, 10]));
console.log('warmupPeriod:', sma.warmupPeriod());
```
Output:
```
[ NaN, NaN, 4, 6, 8 ]
warmupPeriod: 3
```
## Interpretation
`Sma` is a smoothed price level. The two canonical signals are:
1. **PriceSMA crossover.** Close above the SMA suggests an uptrend, close
below suggests a downtrend. The longer the SMA, the slower (and more
trustworthy) the signal.
2. **Two-SMA crossover.** A fast SMA crossing above a slow SMA is the
classic "golden cross"; below is the "death cross". Either of `Ema`
or `Hma` will give earlier (but noisier) signals at the same period.
Prefer `Sma` when you want the simplest possible reference price — for
example, as the middle band of [`BollingerBands`](../../Indicators-Overview.md),
which uses an SMA by construction. Prefer `Ema` if you want the same
smoothness profile but slightly less lag on direction changes.
## Common pitfalls
- **Treating `period = 0` as "use a default".** `Sma::new(0)` returns
`Err(Error::PeriodZero)` in Rust and a `ValueError` in Python; there is
no implicit default. Pass an explicit period.
- **Slicing batch results with `> warmup_period` instead of
`~np.isnan(...)`.** In Python the batch output has `NaN` for warmup
rows; in Rust it has `None`. Use the warmup-aware mask to filter — see
the [Quickstart: Python](../../Quickstart-Python.md#macd-a-multi-column-indicator-and-its-warmup-nans)
pattern. Slicing by `prices.size - warmup_period` works for a single
indicator but breaks the moment you compose two of them via `Chain`.
## References
The simple moving average predates technical analysis as a discipline.
The implementation here follows the standard "rolling sum, slide on each
update" formulation; the matching reference implementations are TA-Lib
and pandas (`rolling(period).mean()`).
## See also
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — same smoothness budget, less lag.
- [Indicator-Wma.md](../moving-averages/Indicator-Wma.md) — linear weights instead of equal.
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — built on three WMAs for near-zero
lag.
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.