This release carries the full post-audit work — 46 new indicators
(25 → 71), an eight-family taxonomy restructure, new bindings for
RollingVWAP, the WASM streaming-update parity, the pyo3/numpy CVE
fix, the SMA/Bollinger drift bound, the O(1) LinearRegression
refactor, the UlcerIndex deque and the PSAR is_ready/reset fixes
plus a refreshed example suite and wiki. The earlier 0.1.5 number
was never published; jumping straight to 0.2.0 is the cleaner signal
for the scope of the change.
Bumped:
- Cargo.toml workspace + wickra-core workspace-dep version
- bindings/python/pyproject.toml
- bindings/node/package.json + optionalDependencies (six platform pins)
- 6 x bindings/node/npm/<target>/package.json
- Cargo.lock regenerated
- CHANGELOG.md [0.2.0] header + compare-link
- docs/wiki/Home.md published-versions table
- docs/wiki/Quickstart-{Rust,Node,WASM}.md + Warmup-Periods.md
version-pinned narrative lines
Verified locally:
- cargo fmt/clippy/test (628 passed, 0 failed)
- cargo deny check (no suppression)
- bindings/node node --test (92/92)
- bindings/python pytest (118/118)
- import wickra reports 0.2.0 with 72 indicator classes
17 KiB
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.
Vwapappears twice — once for the cumulativeVwap::new()and once for the finite-windowRollingVwap::new(period)— because the two share the indicator nameVwap(see 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 isperiod + 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 seedavg_gain/avg_loss. The Rust unit test that pins this iswarmup_period_is_period_plus_one:let rsi = Rsi::new(14).unwrap(); assert_eq!(rsi.warmup_period(), 15);Roc::new(period)warmup isperiod + 1. ROC compares the current price to the priceperiodbars ago; that comparison only makes sense starting at inputperiod + 1.Aroon::new(period)warmup isperiod + 1. Aroon scans aperiod + 1-bar window to find the bars-since-high and bars-since-low.Kama::new(er_period, ...)warmup iser_period + 1. Kaufman's efficiency ratio needser_perioddifferences, 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():
use wickra::{Indicator, MacdIndicator};
let macd = MacdIndicator::classic(); // (12, 26, 9)
assert_eq!(macd.warmup_period(), 34);
import wickra as ta
assert ta.MACD(12, 26, 9).warmup_period() == 34
const wickra = require('wickra');
const sma = new wickra.SMA(20);
console.log(sma.warmupPeriod()); // -> 20
(Since wickra@0.2.0, 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 — the
is_ready()gate, and why alen(prices) > warmup_periodcheck is the wrong abstraction. - Indicator Chaining — how warmups stack inside a
Chain. - Source: https://github.com/kingchenc/wickra