perf: bit-exact batch fast paths + streaming-first benchmark docs (#202)
## Summary - Dedicated batch fast paths for **EMA, RSI, Bollinger, MACD and ATR** (used by the Python bindings): one allocation filled in a single pass, warmup encoded as `NaN`, no per-element `Option` or input re-validation. Each is **bit-for-bit equal** to replaying `update` — SMA/Bollinger keep the drift-reseed cadence, the EMA-family keep the seed division and `mul_add` recurrences. Adds the `BatchNanExt` extension trait. - **Cross-library benchmark refresh**: `compare_libraries.py` reports the median across timing rounds (`--rounds` / `--streaming-rounds`), gains `--skip-batch` / `--skip-streaming`, and runs every peer through the streaming arena (recompute for batch-only libraries). `wickra-bench` drives the batch fast paths against `kand`. - **README** benchmark section reordered streaming-first (the order-of-magnitude result), with measured TA-Lib/tulipy/pandas-ta numbers in place of the CI-only placeholders. ## Impact - Python batch ~2× faster on EMA/RSI/MACD/ATR; streaming path unchanged. - The `batch == streaming` equivalence stays bit-exact. ## Verification - `cargo fmt` · `cargo clippy --workspace --all-targets --all-features -- -D warnings` (clean) - `cargo test --workspace --all-features` — 3782 unit + 420 doc tests pass - Python `pytest` — streaming-vs-batch, known-values, input-validation, smoke pass ## Notes - Node/WASM bindings keep their existing batch; the fast paths are Python-only for now.
This commit is contained in:
@@ -90,6 +90,29 @@ pub trait BatchExt: Indicator {
|
||||
|
||||
impl<T: Indicator> BatchExt for T {}
|
||||
|
||||
/// Fast batch for scalar `f64 -> f64` indicators.
|
||||
///
|
||||
/// The generic [`BatchExt::batch`] returns `Vec<Option<f64>>` — 16 bytes per
|
||||
/// element (no niche fits an arbitrary `f64`), which a caller wanting a dense
|
||||
/// `f64` series then has to walk a second time to map warmup `None`s to `NaN`.
|
||||
/// This skips both the wide intermediate and the second pass: one allocation,
|
||||
/// one pass, warmup encoded as `NaN`. The default body is bit-identical to
|
||||
/// replaying `update`; indicators with a vectorizable closed form override it
|
||||
/// with an inherent `batch_nan` of the same name, which wins method resolution
|
||||
/// over this trait default.
|
||||
pub trait BatchNanExt: Indicator<Input = f64, Output = f64> {
|
||||
/// One `f64` per input, warmup positions filled with `NaN`.
|
||||
fn batch_nan(&mut self, inputs: &[f64]) -> Vec<f64> {
|
||||
let mut out = Vec::with_capacity(inputs.len());
|
||||
for &x in inputs {
|
||||
out.push(self.update(x).unwrap_or(f64::NAN));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Indicator<Input = f64, Output = f64>> BatchNanExt for T {}
|
||||
|
||||
/// A streaming *bar builder* — an alternative-chart constructor (Renko, Kagi,
|
||||
/// Point-and-Figure) that turns a candle stream into a stream of price-driven
|
||||
/// bars.
|
||||
@@ -297,6 +320,17 @@ mod tests {
|
||||
assert_eq!(out, vec![Some(1.0), Some(2.0), Some(3.0)]);
|
||||
}
|
||||
|
||||
/// The blanket [`BatchNanExt::batch_nan`] default (used by every scalar
|
||||
/// indicator without an inherent fast path) maps `update` outputs to a dense
|
||||
/// `f64` series, warmup `None` becoming `NaN`. `Identity` is always ready, so
|
||||
/// the result is just the inputs back.
|
||||
#[test]
|
||||
fn batch_nan_default_maps_none_to_nan() {
|
||||
let mut id = Identity::default();
|
||||
let out = id.batch_nan(&[1.0, 2.0, 3.0]);
|
||||
assert_eq!(out, vec![1.0, 2.0, 3.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_pipes_first_into_second() {
|
||||
let mut c = Chain::new(Doubler::default(), Doubler::default());
|
||||
|
||||
Reference in New Issue
Block a user