Files
wickra/docs/wiki/Warmup-Periods.md
T
kingchenc 8aa74cb638 release(0.2.1): bump to 0.2.1, skipping Windows ARM64 this cycle
The 0.2.0 release left wickra@npm stuck at 0.1.4 and never created a
GitHub Release entry because the brand-new `wickra-win32-arm64-msvc`
sub-package name was caught by npm's spam-detection filter on its first
publish attempt (same situation that affected `wickra-win32-x64-msvc`
through 0.1.4 until npm Support unblocked it). A support ticket is open;
until it is resolved, ship 0.2.1 for the five platforms whose
sub-packages are already on npm and re-add Windows ARM64 in a follow-up
release.

Changes for this cycle:
- bindings/node/package.json: remove "wickra-win32-arm64-msvc" from
  optionalDependencies and "aarch64-pc-windows-msvc" from
  napi.triples.additional.
- bindings/node/npm/win32-arm64-msvc/: removed (will be restored fresh
  once the npm name is unblocked).
- .github/workflows/release.yml: comment out the
  aarch64-pc-windows-msvc entry of the node-build matrix with a
  TODO/restore note.
- Bump every workspace and binding version to 0.2.1 (Cargo.toml,
  pyproject.toml, bindings/node/package.json, five npm/<target>
  templates, the wiki version table). Cargo.lock regenerated.
- CHANGELOG: new [0.2.1] block consolidating every fix that has landed
  on main since 0.2.0 (HV epsilon, examples CI step, fuzz cargo-fuzz
  install, MSRV 1.85 -> 1.86 / 1.77 -> 1.88, criterion 0.5 -> 0.8,
  tokio-tungstenite 0.24 -> 0.29, tick_aggregator gap-fill cap, every
  GitHub Action SHA-pin bump). Compare-link added.

The arm64 loader branch in bindings/node/index.js is left untouched: a
Windows ARM64 user installing 0.2.1 will get the standard
`Cannot find module 'wickra-win32-arm64-msvc'` error from the loader,
which is accurate. PyPI's win-arm64 wheel is unaffected.

Verified locally:
  cargo fmt/clippy/test --workspace --all-features -> 630 passed / 0 failed
  cargo build -p wickra-examples --bins -> clean
  cargo build -p wickra-node -> clean
2026-05-23 22:20:20 +02:00

175 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Warmup Periods
Every Wickra indicator returns `None` (Rust), `None` (Python), or `null`
(Node) for its first few inputs while it gathers enough data to produce a
defined value. The number of inputs an indicator needs before it emits its
first non-empty value is its **warmup period**, surfaced everywhere as
`warmup_period()` / `warmupPeriod()`.
After the first emission, the indicator never goes back to a "no value yet"
state — it has rolled its state forward and will produce a steady value on
every subsequent `update()`. Calling `reset()` returns to the warming-up
state, equivalent to a freshly constructed instance.
## How to read the formula column
The formulas below are taken verbatim from the `warmup_period()` methods in
`crates/wickra-core/src/indicators/<name>.rs`. The "Inputs at first
emission" column says, in 1-indexed terms, which `update()` call returns the
first `Some`/non-`NaN` value. They are the same number; "first emission
index" in 0-indexed terms is `warmup_period 1`.
## Single-output indicators
> The rows are keyed by **constructor**, not by indicator name. `Vwap`
> appears twice — once for the cumulative `Vwap::new()` and once for the
> finite-window `RollingVwap::new(period)` — because the two share the
> indicator name `Vwap` (see [Indicators-Overview.md](Indicators-Overview.md))
> but have different warmup periods. That is the only such pair; every other
> row is one canonical indicator.
| Indicator | Constructor | Formula | `warmup_period()` for shown args | Inputs at first emission |
|-----------------|----------------------------------------------|----------------------------------|----------------------------------|--------------------------|
| `Sma` | `Sma::new(14)` | `period` | 14 | 14th |
| `Ema` | `Ema::new(14)` | `period` | 14 | 14th |
| `Wma` | `Wma::new(14)` | `period` | 14 | 14th |
| `Dema` | `Dema::new(14)` | `2 * period - 1` | 27 | 27th |
| `Tema` | `Tema::new(14)` | `3 * period - 2` | 40 | 40th |
| `Hma` | `Hma::new(14)` | `period + round(sqrt(period)).max(1) - 1` | 17 | 17th |
| `Kama` | `Kama::new(10, 2, 30)` | `er_period + 1` | 11 | 11th |
| `Rsi` | `Rsi::new(14)` | `period + 1` | 15 | 15th |
| `Cci` | `Cci::new(20)` | `period` | 20 | 20th |
| `Roc` | `Roc::new(12)` | `period + 1` | 13 | 13th |
| `WilliamsR` | `WilliamsR::new(14)` | `period` | 14 | 14th |
| `Mfi` | `Mfi::new(14)` | `period` | 14 | 14th |
| `Trix` | `Trix::new(15)` | `3 * period - 1` | 44 | 44th |
| `AwesomeOscillator` | `AwesomeOscillator::new(5, 34)` | `slow_period` | 34 | 34th |
| `Atr` | `Atr::new(14)` | `period` | 14 | 14th |
| `Psar` | `Psar::new(0.02, 0.02, 0.20)` | constant `2` | 2 | 2nd |
| `Obv` | `Obv::new()` | constant `1` | 1 | 1st |
| `Vwap` | `Vwap::new()` | constant `1` | 1 | 1st |
| `RollingVwap` | `RollingVwap::new(20)` | `period` | 20 | 20th |
| `Smma` | `Smma::new(14)` | `period` | 14 | 14th |
| `Trima` | `Trima::new(20)` | `period` | 20 | 20th |
| `Zlema` | `Zlema::new(14)` | `lag + period` (`lag = (period 1) / 2`) | 20 | 20th |
| `T3` | `T3::new(5, 0.7)` | `6 * period - 5` | 25 | 25th |
| `Vwma` | `Vwma::new(20)` | `period` | 20 | 20th |
| `Mom` | `Mom::new(10)` | `period + 1` | 11 | 11th |
| `Cmo` | `Cmo::new(14)` | `period + 1` | 15 | 15th |
| `Tsi` | `Tsi::new(25, 13)` | `long + short` | 38 | 38th |
| `Pmo` | `Pmo::new(35, 20)` | constant `2` | 2 | 2nd |
| `StochRsi` | `StochRsi::new(14, 14)` | `rsi_period + stoch_period` | 28 | 28th |
| `UltimateOscillator` | `UltimateOscillator::new(7, 14, 28)` | `max(short, mid, long) + 1` | 29 | 29th |
| `Ppo` | `Ppo::new(12, 26)` | `slow` | 26 | 26th |
| `Dpo` | `Dpo::new(20)` | `max(period, period / 2 + 2)` | 20 | 20th |
| `Coppock` | `Coppock::new(14, 11, 10)` | `max(roc_long, roc_short) + wma_period` | 24 | 24th |
| `AroonOscillator` | `AroonOscillator::new(14)` | `period + 1` | 15 | 15th |
| `MassIndex` | `MassIndex::new(9, 25)` | `2 * ema_period + sum_period - 2` | 41 | 41st |
| `Natr` | `Natr::new(14)` | `period` | 14 | 14th |
| `StdDev` | `StdDev::new(20)` | `period` | 20 | 20th |
| `UlcerIndex` | `UlcerIndex::new(14)` | `2 * period - 1` | 27 | 27th |
| `HistoricalVolatility` | `HistoricalVolatility::new(20, 252)` | `period + 1` | 21 | 21st |
| `BollingerBandwidth` | `BollingerBandwidth::new(20, 2.0)` | `period` | 20 | 20th |
| `PercentB` | `PercentB::new(20, 2.0)` | `period` | 20 | 20th |
| `AtrTrailingStop` | `AtrTrailingStop::new(14, 3.0)` | `atr_period` | 14 | 14th |
| `Adl` | `Adl::new()` | constant `1` | 1 | 1st |
| `VolumePriceTrend` | `VolumePriceTrend::new()` | constant `1` | 1 | 1st |
| `ChaikinMoneyFlow` | `ChaikinMoneyFlow::new(20)` | `period` | 20 | 20th |
| `ChaikinOscillator` | `ChaikinOscillator::new(3, 10)` | `slow` | 10 | 10th |
| `ForceIndex` | `ForceIndex::new(13)` | `period + 1` | 14 | 14th |
| `EaseOfMovement` | `EaseOfMovement::new(14)` | `period + 1` | 15 | 15th |
| `TypicalPrice` | `TypicalPrice::new()` | constant `1` | 1 | 1st |
| `MedianPrice` | `MedianPrice::new()` | constant `1` | 1 | 1st |
| `WeightedClose` | `WeightedClose::new()` | constant `1` | 1 | 1st |
| `LinearRegression` | `LinearRegression::new(14)` | `period` | 14 | 14th |
| `LinRegSlope` | `LinRegSlope::new(14)` | `period` | 14 | 14th |
| `AcceleratorOscillator` | `AcceleratorOscillator::classic()` | `ao_slow + signal_period - 1` | 38 | 38th |
| `BalanceOfPower` | `BalanceOfPower::new()` | constant `1` | 1 | 1st |
| `ChoppinessIndex` | `ChoppinessIndex::new(14)` | `period` | 14 | 14th |
| `VerticalHorizontalFilter` | `VerticalHorizontalFilter::new(28)` | `period + 1` | 29 | 29th |
| `TrueRange` | `TrueRange::new()` | constant `1` | 1 | 1st |
| `ChaikinVolatility` | `ChaikinVolatility::new(10, 10)` | `ema_period + roc_period` | 20 | 20th |
| `ZScore` | `ZScore::new(20)` | `period` | 20 | 20th |
| `LinRegAngle` | `LinRegAngle::new(14)` | `period` | 14 | 14th |
## Multi-output indicators
These indicators emit several values at once (a struct in Rust, a tuple in
Python, an object in Node) and every column / field transitions from "not
ready" to "ready" together — there are no rows that have a `signal` but no
`macd`, for example.
| Indicator | Constructor | Formula | `warmup_period()` for shown args | Inputs at first emission | Outputs |
|-------------------|--------------------------------------|------------------------------------------|----------------------------------|--------------------------|--------------------------------------------------------|
| `MacdIndicator` | `MacdIndicator::new(12, 26, 9)` | `slow + signal - 1` | 34 | 34th | `macd`, `signal`, `histogram` |
| `BollingerBands` | `BollingerBands::new(20, 2.0)` | `period` | 20 | 20th | `upper`, `middle`, `lower`, `stddev` |
| `Stochastic` | `Stochastic::new(14, 3)` | `k_period + d_period - 1` | 16 | 16th | `k`, `d` |
| `Adx` | `Adx::new(14)` | `2 * period` | 28 | 28th | `plus_di`, `minus_di`, `adx` |
| `Aroon` | `Aroon::new(14)` | `period + 1` | 15 | 15th | `up`, `down` |
| `Keltner` | `Keltner::new(20, 10, 2.0)` | `ema_period.max(atr_period)` | 20 | 20th | `upper`, `middle`, `lower` |
| `Donchian` | `Donchian::new(20)` | `period` | 20 | 20th | `upper`, `middle`, `lower` |
| `Vortex` | `Vortex::new(14)` | `period + 1` | 15 | 15th | `plus`, `minus` |
| `SuperTrend` | `SuperTrend::new(10, 3.0)` | `atr_period` | 10 | 10th | `value`, `direction` |
| `ChandelierExit` | `ChandelierExit::new(22, 3.0)` | `period` | 22 | 22nd | `long_stop`, `short_stop` |
| `ChandeKrollStop` | `ChandeKrollStop::new(10, 1.0, 9)` | `atr_period + stop_period - 1` | 18 | 18th | `stop_long`, `stop_short` |
## "Off-by-one" cases worth memorising
A few indicators look like they should warm up at `period` but in fact need
`period + 1` inputs. The reason is always the same — they consume *diffs*
or *previous-close* differences, not the prices themselves, and the very
first input has nothing to diff against.
- **`Rsi::new(period)` warmup is `period + 1`.** RSI is based on Wilder's
smoothing over per-tick gains and losses. With 14 prices you only have 13
diffs; you need 15 prices to compute 14 diffs and seed `avg_gain` /
`avg_loss`. The Rust unit test that pins this is
`warmup_period_is_period_plus_one`:
```rust
let rsi = Rsi::new(14).unwrap();
assert_eq!(rsi.warmup_period(), 15);
```
- **`Roc::new(period)` warmup is `period + 1`.** ROC compares the current
price to the price `period` bars ago; that comparison only makes sense
starting at input `period + 1`.
- **`Aroon::new(period)` warmup is `period + 1`.** Aroon scans a `period + 1`-bar
window to find the bars-since-high and bars-since-low.
- **`Kama::new(er_period, ...)` warmup is `er_period + 1`.** Kaufman's
efficiency ratio needs `er_period` differences, which costs one extra
bar.
## Cross-checking from your own code
The cleanest way to verify any of these from your application code is the
indicator's own `warmup_period()`:
```rust
use wickra::{Indicator, MacdIndicator};
let macd = MacdIndicator::classic(); // (12, 26, 9)
assert_eq!(macd.warmup_period(), 34);
```
```python
import wickra as ta
assert ta.MACD(12, 26, 9).warmup_period() == 34
```
```javascript
const wickra = require('wickra');
const sma = new wickra.SMA(20);
console.log(sma.warmupPeriod()); // -> 20
```
(Since `wickra@0.2.1`, `warmupPeriod()` is exposed on every Node and
WASM class — single- and multi-output — alongside `update()`, `reset()`
and `isReady()`. Consult `bindings/node/index.d.ts` for the
authoritative TypeScript surface.)
## See also
- [Streaming vs Batch](Streaming-vs-Batch.md) — the `is_ready()` gate, and
why a `len(prices) > warmup_period` check is the wrong abstraction.
- [Indicator Chaining](Indicator-Chaining.md) — how warmups stack inside a
`Chain`.
- Source: <https://github.com/kingchenc/wickra>