6287bd48c1
* feat(adxr): add Wilder Average Directional Movement Index Rating
ADXR is the trend-strength smoother Wilder published alongside ADX in
*New Concepts in Technical Trading Systems* (1978):
ADXR_t = (ADX_t + ADX_{t - (period - 1)}) / 2
The lookback length is the same period that feeds the underlying ADX.
Because the older ADX is period - 1 bars stale, ADXR responds more
slowly than ADX and is the canonical metric for comparing
trend-strength across instruments.
Implementation reuses the existing wickra_core::Adx engine plus a
period-length ring of past ADX values; warmup is 3 * period - 1
(41 for period = 14). Bindings: Python PyAdxr (PyArray1 batch),
Node AdxrNode (number scalar), WASM WasmAdxr. Fuzz target covers
the candle-input path. Python + Node streaming-vs-batch tests
parametrised, plus a pure-uptrend reference value (ADXR == 100
when ADX saturates at 100). Criterion bench added under crates/
wickra/benches/indicators.rs.
README family table and indicator counter updated (71 -> 72).
* feat(rwi): add Mike Poulos Random Walk Index
RWI compares actual price displacement to what a random walk would
produce over the same horizon: for each lookback i in [2, period],
RWI_High_t(i) = (high_t - low_{t-i+1}) / (ATR_i(t) * sqrt(i))
RWI_Low_t(i) = (high_{t-i+1} - low_t) / (ATR_i(t) * sqrt(i))
Per-bar output is the maximum across lookbacks for each direction;
a reading > 1 means the trend beats random-walk noise, > 2 is the
typical strong-trend threshold. Multi-output (high, low). period
must be >= 2 (the shortest meaningful lookback); period < 2 returns
InvalidPeriod. Warmup = period (e.g. 14 for the standard default).
Bindings: Python PyRwi (PyArray2 shape (n, 2)), Node RwiNode +
RwiValue struct, WASM WasmRwi (Object/Reflect for update,
Float64Array interleaved for batch). Fuzz target adds the candle
input case. Python parametric streaming-vs-batch test and pure
uptrend reference test (RWI_High dominates RWI_Low and exceeds 1).
Node parametric streaming-vs-interleaved-batch test. Criterion
bench under crates/wickra/benches/indicators.rs.
README family table and indicator counter updated (72 -> 73).
* feat(tii): add M.H. Pee Trend Intensity Index
TII is a [0, 100] oscillator that asks 'what fraction of the recent
SMA deviations are positive?'. The construction is
dev_t = close_t - SMA(close, sma_period)_t
SD_pos = sum of positive dev_t over the last dev_period bars
SD_neg = sum of |negative dev_t| over the last dev_period bars
TII = 100 * SD_pos / (SD_pos + SD_neg)
Saturates at 100 on a pure uptrend (every close above the lagging
SMA), at 0 on a pure downtrend, and returns the neutral mid-point 50
on a perfectly flat window. The output is clamped to [0, 100] as
the rolling-sum subtraction loop can accumulate a few ULP of error
on long histories. Canonical Pee parameters (sma_period=60,
dev_period=30) wired as Python defaults; warmup is
sma_period + dev_period - 1 (89 for the defaults).
Bindings: Python PyTii (PyArray1 batch), Node TiiNode (scalar
update + batch), WASM WasmTii via the two-arg wasm_scalar_indicator!
macro. Fuzz target adds the scalar path. Python parametric
streaming-vs-batch test plus pure-uptrend (TII == 100) and
flat-market (TII == 50) reference tests. Node parametric
streaming-vs-batch test. Criterion bench under crates/wickra/
benches/indicators.rs.
README family table and indicator counter updated (73 -> 74).
* feat(kst): add Pring Know Sure Thing oscillator
KST is Martin Pring's long-horizon momentum gauge: four smoothed
rate-of-change components combined with fixed weights (1, 2, 3, 4),
plus an SMA signal line.
RCMA_i = SMA(ROC(close, roc_i), sma_i) for i in 1..=4
KST = 1*RCMA_1 + 2*RCMA_2 + 3*RCMA_3 + 4*RCMA_4
Signal = SMA(KST, signal_period)
Kst::classic() exposes Pring's recommended parameter set
(roc = (10, 15, 20, 30), sma = (10, 10, 10, 15), signal = 9);
warmup = max(roc_i + sma_i) + signal_period - 1 (53 for the classic
parameters). All four parallel branches are fed unconditionally so
they warm in lock-step.
Bindings: Python PyKst (PyArray2 shape (n, 2)) with a KST.classic()
staticmethod, Node KstNode + KstValue with a KST.classic() factory,
WASM WasmKst with both new(...) and classic() constructors plus
Object/Reflect for update and Float64Array for batch. Fuzz target
adds the scalar multi-output path. Python tests gain a new
MULTI_SCALAR section parametric over scalar-input/multi-output
indicators, plus a classic-on-constant-series reference test. Node
tests gain a KST entry in the multi-output section. Criterion
benchmark added under crates/wickra/benches/indicators.rs.
README family table and indicator counter updated (74 -> 75).
* feat(wave-trend): add LazyBear Wave Trend Oscillator
Two-line mean-reverting momentum gauge built from the typical price
and three cascaded EMAs:
ap = (high + low + close) / 3
esa = EMA(ap, channel_period)
d = EMA(|ap - esa|, channel_period)
ci = (ap - esa) / (0.015 * d)
wt1 = EMA(ci, average_period)
wt2 = SMA(wt1, signal_period)
WaveTrend::classic() exposes LazyBear's defaults
(channel = 10, average = 21, signal = 4); warmup is
2 * channel_period + average_period + signal_period - 3 (42 for the
classic defaults). On a perfectly flat market the SMA-seeded EMA
introduces a single-ULP drift between ap and esa, which on a tiny d
would make the ratio explode to -1/0.015 = -66.67; a price-scaled
flat-tolerance guard (d <= 16 * EPSILON * max(|esa|, 1)) collapses
the channel index to 0 in that regime so both lines remain at zero.
Bindings: Python PyWaveTrend (PyArray2 shape (n, 2)) with a
WaveTrend.classic() staticmethod, Node WaveTrendNode + WaveTrendValue
with a WaveTrend.classic() factory, WASM WasmWaveTrend with both
new(...) and classic() constructors. Fuzz target adds the candle
multi-output path (sorted alphabetically). Python parametric
streaming-vs-batch test plus a flat-market reference test. Node
parametric streaming-vs-interleaved-batch test. Criterion bench
under crates/wickra/benches/indicators.rs.
README family table and indicator counter updated (75 -> 76).
* fix(family-06): re-add KST::classic() factory + drop dup fuzz block
Family-06 PR's tests call ta.KST.classic() / wickra.KST.classic() — main's
KST binding shipped without the static factory. Add classic() in Python
(staticmethod) and Node (napi factory); WASM already had it. Also drop the
duplicate Kst::classic().unwrap() block in fuzz/indicator_update.rs that
the merge left behind (main's API no longer returns Result).
* test(rwi): drop dead count==0 guard
The loop `for i in 2..=period` makes `count = tr_end - tr_start = i - 1`
which is always >= 1, so the `if count == 0 { continue; }` branch was
unreachable defensive code that codecov flagged on the family-06 PR.
Fuzzing Wickra
cargo-fuzz harnesses for
the parsing and stateful entry points of Wickra. Fuzzing requires a nightly
Rust toolchain.
Setup
cargo install cargo-fuzz
rustup toolchain install nightly
Targets
| Target | What it exercises |
|---|---|
csv_reader |
CandleReader over arbitrary bytes — headers, cells, BOM, binary noise. |
binance_envelope |
RawWsEnvelope deserialization from arbitrary strings. |
indicator_update |
Every scalar-input indicator (SMA / EMA / WMA / RSI / DEMA / TEMA / HMA / ROC / TRIX / SMMA / TRIMA / ZLEMA / KAMA / T3 / MOM / CMO / TSI / PMO / StochRSI / DPO / PPO / Coppock / StdDev / UlcerIndex / HistoricalVolatility / LinearRegression / LinRegSlope / LinRegAngle / VHF / ZScore / MACD / Bollinger) streamed + batched over arbitrary f64 sequences (NaN, ±inf, jumps). |
indicator_update_candle |
Every candle-input indicator (ATR, NATR, TrueRange, ChaikinVolatility, Keltner, Donchian, PSAR, SuperTrend, ChandelierExit, ChandeKrollStop, ATRTrailingStop, ADX, Aroon, AroonOscillator, Vortex, MassIndex, ChoppinessIndex, CCI, WilliamsR, AwesomeOscillator, AcceleratorOscillator, UltimateOscillator, BalanceOfPower, OBV, MFI, VWAP, RollingVWAP, VWMA, ADL, VPT, CMF, ChaikinOscillator, ForceIndex, EaseOfMovement, TypicalPrice, MedianPrice, WeightedClose, Stochastic) streamed + batched over fuzz-derived OHLCV candles. |
tick_aggregator |
TickAggregator over arbitrary (price, volume, timestamp) triples. |
Run
# From the repository root:
cargo +nightly fuzz run csv_reader
cargo +nightly fuzz run binance_envelope
cargo +nightly fuzz run indicator_update
cargo +nightly fuzz run indicator_update_candle
cargo +nightly fuzz run tick_aggregator
Each run continues until a crash is found or it is interrupted. A short time-boxed smoke run is useful in CI:
cargo +nightly fuzz run csv_reader -- -max_total_time=60
The expectation for every target is that it never panics: malformed or
adversarial input must surface as an Err, never a crash.