* feat(rvi): add Relative Vigor Index
Dorsey's RVI = SMA(close - open, period) / SMA(high - low, period) over
a rolling window of period candles. Candle input, single parameter
period (default 10). Positive on average-bullish windows, negative on
average-bearish. Holds the previous value if the entire window has
zero range (denominator undefined).
Reference: Donald Dorsey, also pandas-ta rvi.
Touchpoints: rvi.rs + mod.rs + lib.rs re-export, PyRvi + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values reference,
RviNode (4-column OHLC batch) + index.d.ts/index.js + indicators.test
.js factory + reference, WasmRvi + make_candle_ohlc helper, candle-fuzz
target + criterion bench, README + CHANGELOG.
* feat(pgo): add Pretty Good Oscillator
Mark Johnson's PGO = (close - SMA(close, period)) / EMA(TR, period).
Counts roughly how many ATR-equivalents the close sits from its
period-bar mean. Candle input, single parameter period (default 14).
Johnson's heuristic uses +3/-3 crossings as entry signals.
Touchpoints: pgo.rs + mod.rs + lib.rs re-export, PyPgo + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values flat-close
reference, PgoNode (h/l/c) + index.d.ts/index.js + indicators.test.js
factory + reference, WasmPgo, candle-fuzz target + bench, README +
CHANGELOG.
* feat(kst): add Know Sure Thing (Pring)
Pring's long-horizon momentum oscillator: weighted sum of four
SMA-smoothed ROC series with fixed weights 1, 2, 3, 4, plus an SMA
signal line. Nine parameters (four ROC periods, four SMA periods, one
signal period); classic() applies Pring's recommended defaults.
Multi-output indicator emitting KstOutput { kst, signal }.
Touchpoints: kst.rs + mod.rs + lib.rs re-export, PyKst + __init__.py
+ test_new_indicators MULTI + test_known_values flat-input reference,
KstNode + KstValue + index.d.ts/index.js + indicators.test.js multi
factory + reference, WasmKst (manual JsValue object), scalar-fuzz
target (handled outside the f64-output drive helper), README +
CHANGELOG.
* feat(smi): add Stochastic Momentum Index (Blau)
Blau's doubly-EMA-smoothed bounded oscillator: measures the close's
displacement from the centre of the recent high-low range, scaled by
the smoothed range. Candle input, three parameters (period, d_period,
d2_period) with defaults 5 / 3 / 3.
Internally feeds both the displacement-EMA stack and the range-EMA
stack on every candle so they warm up in parallel (gating either
behind the other starves the second by one input).
Touchpoints: smi.rs + mod.rs + lib.rs re-export, PySmi + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values flat-input
reference, SmiNode + index.d.ts/index.js + indicators.test.js factory
+ reference, WasmSmi, candle-fuzz target, README + CHANGELOG.
* feat(laguerre-rsi): add Ehlers Laguerre RSI
Four-stage Laguerre polynomial filter wrapped in an RSI-style up/down
accumulator. Single gamma in [0, 1] (default 0.5) trades lag for
smoothness. State is seeded by setting all four L_i to the first input
so a constant series stays at the neutral 50. Output clamped to
[0, 100] to absorb floating-point rounding.
Reference: Ehlers, Time Warp - Without Space Travel, 2002.
Touchpoints: laguerre_rsi.rs + mod.rs + lib.rs re-export, PyLaguerreRsi
+ __init__.py + test_new_indicators SCALAR + test_known_values neutral
reference, LaguerreRsiNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmLaguerreRsi via scalar macro, scalar-fuzz
target, README + CHANGELOG.
* feat(connors-rsi): add Connors RSI (CRSI)
Larry Connors' 3-component aggregate: RSI(close), RSI(streak), and
PercentRank of the 1-period return over the last period_rank returns.
Each component is bounded in [0, 100] so the aggregate is too.
Three parameters (period_rsi, period_streak, period_rank) with
defaults 3 / 2 / 100. Streak tracks consecutive up/down runs (resets
to 0 on unchanged close).
Touchpoints: connors_rsi.rs + mod.rs + lib.rs re-export, PyConnorsRsi
+ __init__.py + test_new_indicators SCALAR + test_known_values bounded
reference, ConnorsRsiNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmConnorsRsi via scalar macro, scalar-fuzz
target, README + CHANGELOG.
* feat(inertia): add Dorsey Inertia (RVI + LinReg)
Donald Dorsey's Inertia — a LinearRegression smoothing of the RVI
series. Endpoint of an n-bar least-squares fit of RVI is the indicator
reading. Preserves trend direction while damping the ratio. Candle
input, two parameters (rvi_period, linreg_period) with defaults 14 / 20.
Touchpoints: inertia.rs + mod.rs + lib.rs re-export, PyInertia +
__init__.py + test_new_indicators CANDLE_SCALAR + test_known_values
constant reference, InertiaNode (4-column OHLC batch) + index.d.ts /
index.js + indicators.test.js factory + reference, WasmInertia,
candle-fuzz target, README + CHANGELOG.
* test(kst): Move KST out of MULTI dict (it is scalar-input)
KST sits in the MULTI dict (candle-input, multi-output) but its
update() takes a single f64, not a candle tuple. The shared streaming
loop in test_multi_streaming_matches_batch fed the OHLCV tuple in,
which crashed with `TypeError: argument 'value': must be real number,
not tuple` on every Python matrix entry.
Split into a new MULTI_SCALAR_INPUT dict with its own test function
that feeds the close-price stream as floats. KST is currently the
only such indicator; structure is ready for future scalar-input
multi-output additions (e.g. some MACD-shaped indicators).
* test(coverage): Cover SMI zero-range and ConnorsRsi zero-prev cold paths
codecov/patch on PR 40 flagged two uncovered defensive branches:
- SMI returns self.current early when the smoothed range collapses to
zero (`r2 <= 0.0`) so the formula stays defined. Exercised by feeding
bars where high == low.
- ConnorsRsi skips the ROC ring-buffer update when the previous price
is exactly zero so the divide-by-zero in `(input - prev) / prev` is
impossible. Exercised by seeding the first bar at 0.0.
138 lines
5.9 KiB
Rust
138 lines
5.9 KiB
Rust
#![no_main]
|
|
//! Fuzz OHLCV-input indicator updates with arbitrary candle sequences.
|
|
//!
|
|
//! Every candle-input indicator must tolerate any sequence of validated OHLCV
|
|
//! candles — extreme magnitudes, micro-spreads, zero-volume bars, abrupt
|
|
//! reversals — without panicking. The fuzzer chunks the raw `f64` stream into
|
|
//! `[open, high, low, close, volume]` tuples and constructs each candle via
|
|
//! `Candle::new`; entries that fail OHLCV-invariant validation are skipped so
|
|
//! the indicator only ever sees structurally-valid candles. Each iteration
|
|
//! then drives that candle stream through every candle-input indicator twice
|
|
//! (streaming `update` + batch).
|
|
//!
|
|
//! Audit finding R9: the previous fuzz suite had no candle-input coverage at
|
|
//! all. This target now covers every candle-input indicator including the
|
|
//! ones the audit named explicitly (ATR, ADX, Stochastic, PSAR) plus the
|
|
//! complete catalogue: Keltner, Donchian, SuperTrend, Chandelier Exit, ATR
|
|
//! Trailing Stop, Aroon, AwesomeOscillator, CCI, WilliamsR, MFI, OBV, VWAP,
|
|
//! RollingVWAP, ADL, VPT, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex,
|
|
//! EaseOfMovement, NATR, AroonOscillator, ChandeKrollStop, Vortex, MassIndex,
|
|
//! ChoppinessIndex, TrueRange, ChaikinVolatility, AcceleratorOscillator,
|
|
//! BalanceOfPower, UltimateOscillator, VWMA, TypicalPrice, MedianPrice,
|
|
//! WeightedClose.
|
|
|
|
use libfuzzer_sys::fuzz_target;
|
|
use wickra_core::{
|
|
AcceleratorOscillator, Adl, Adx, Alligator, Aroon, AroonOscillator, Atr, AtrTrailingStop,
|
|
AwesomeOscillator, BalanceOfPower, BatchExt, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator,
|
|
ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, Donchian, EaseOfMovement,
|
|
Evwma, ForceIndex, Indicator, Inertia, Keltner, MassIndex, MedianPrice, Mfi, Natr, Obv, Pgo,
|
|
Psar, RollingVwap, Rvi, Smi,
|
|
Stochastic, SuperTrend, TrueRange, TypicalPrice, UltimateOscillator, VolumePriceTrend, Vortex,
|
|
Vwap, Vwma, WeightedClose, WilliamsR,
|
|
};
|
|
|
|
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
|
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
|
|
/// validation are dropped so the indicator under test only ever sees a
|
|
/// structurally-valid candle stream (the *parser* is fuzz-tested elsewhere;
|
|
/// this target focuses on indicator robustness).
|
|
fn candles_from(data: &[f64]) -> Vec<Candle> {
|
|
data.chunks_exact(5)
|
|
.enumerate()
|
|
.filter_map(|(i, ch)| {
|
|
// A monotonic timestamp avoids surprising any indicator that might
|
|
// care about ordering. The fuzz input drives OHLCV; time is just a
|
|
// tie-breaker.
|
|
Candle::new(ch[0], ch[1], ch[2], ch[3], ch[4], i as i64).ok()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Streaming + batch sweep through one candle-input indicator. `#[inline(never)]`
|
|
/// keeps each indicator on its own frame in any panic backtrace.
|
|
#[inline(never)]
|
|
fn drive<I, O>(make: impl Fn() -> I, candles: &[Candle])
|
|
where
|
|
I: Indicator<Input = Candle, Output = O> + BatchExt,
|
|
{
|
|
let mut streaming = make();
|
|
for c in candles {
|
|
let _ = streaming.update(*c);
|
|
}
|
|
let _ = make().batch(candles);
|
|
}
|
|
|
|
fuzz_target!(|data: Vec<f64>| {
|
|
let candles = candles_from(&data);
|
|
if candles.is_empty() {
|
|
return;
|
|
}
|
|
|
|
// --- Volatility & ATR family ---
|
|
drive(|| Atr::new(14).unwrap(), &candles);
|
|
drive(|| Natr::new(14).unwrap(), &candles);
|
|
drive(TrueRange::new, &candles);
|
|
drive(|| ChaikinVolatility::new(10, 10).unwrap(), &candles);
|
|
|
|
// --- Bands & Channels ---
|
|
drive(|| Keltner::new(20, 10, 2.0).unwrap(), &candles);
|
|
drive(|| Donchian::new(20).unwrap(), &candles);
|
|
|
|
// --- Trailing Stops ---
|
|
drive(|| Psar::new(0.02, 0.02, 0.20).unwrap(), &candles);
|
|
drive(|| SuperTrend::new(14, 3.0).unwrap(), &candles);
|
|
drive(|| ChandelierExit::new(22, 3.0).unwrap(), &candles);
|
|
drive(|| ChandeKrollStop::new(10, 1.0, 9).unwrap(), &candles);
|
|
drive(|| AtrTrailingStop::new(14, 3.0).unwrap(), &candles);
|
|
|
|
// --- Trend & Directional ---
|
|
drive(|| Adx::new(14).unwrap(), &candles);
|
|
drive(|| Aroon::new(14).unwrap(), &candles);
|
|
drive(|| Alligator::new(13, 8, 5).unwrap(), &candles);
|
|
drive(|| AroonOscillator::new(14).unwrap(), &candles);
|
|
drive(|| Vortex::new(14).unwrap(), &candles);
|
|
drive(|| MassIndex::new(9, 25).unwrap(), &candles);
|
|
drive(|| ChoppinessIndex::new(14).unwrap(), &candles);
|
|
|
|
// --- Momentum & Oscillators ---
|
|
drive(|| Cci::new(20).unwrap(), &candles);
|
|
drive(|| Rvi::new(10).unwrap(), &candles);
|
|
drive(|| Inertia::new(14, 20).unwrap(), &candles);
|
|
drive(|| Pgo::new(14).unwrap(), &candles);
|
|
drive(|| Smi::classic(), &candles);
|
|
drive(|| WilliamsR::new(14).unwrap(), &candles);
|
|
drive(|| AwesomeOscillator::new(5, 34).unwrap(), &candles);
|
|
drive(|| AcceleratorOscillator::new(5, 34, 5).unwrap(), &candles);
|
|
drive(|| UltimateOscillator::new(7, 14, 28).unwrap(), &candles);
|
|
drive(BalanceOfPower::new, &candles);
|
|
|
|
// --- Volume ---
|
|
drive(Obv::new, &candles);
|
|
drive(|| Mfi::new(14).unwrap(), &candles);
|
|
drive(Vwap::new, &candles);
|
|
drive(|| RollingVwap::new(20).unwrap(), &candles);
|
|
drive(|| Vwma::new(20).unwrap(), &candles);
|
|
drive(|| Evwma::new(20).unwrap(), &candles);
|
|
drive(Adl::new, &candles);
|
|
drive(VolumePriceTrend::new, &candles);
|
|
drive(|| ChaikinMoneyFlow::new(20).unwrap(), &candles);
|
|
drive(|| ChaikinOscillator::new(3, 10).unwrap(), &candles);
|
|
drive(|| ForceIndex::new(13).unwrap(), &candles);
|
|
drive(|| EaseOfMovement::with_divisor(14, 1e8).unwrap(), &candles);
|
|
|
|
// --- Price transformations ---
|
|
drive(TypicalPrice::new, &candles);
|
|
drive(MedianPrice::new, &candles);
|
|
drive(WeightedClose::new, &candles);
|
|
|
|
// --- Stochastic (multi-output) ---
|
|
{
|
|
let mut s = Stochastic::new(14, 3).unwrap();
|
|
for c in &candles {
|
|
let _ = s.update(*c);
|
|
}
|
|
let _ = Stochastic::new(14, 3).unwrap().batch(&candles);
|
|
}
|
|
});
|