Honest tiered cross-library benchmark + streaming/batch perf (#186)
## Summary An honest, tiered cross-library benchmark — and the optimization pass it triggered. ### Performance (wickra-core, outputs unchanged) Profiling against the other Rust TA crates exposed real inefficiencies. Each benchmarked indicator is now **5–79% faster** in both streaming and batch: - **SMA, Bollinger**: flat `Box<[f64]>` ring buffers replace `VecDeque` (−69…79%). - **RSI**: `100·ag/(ag+al)` collapses three divisions into one; Wilder smoothing hoists `1/period` out of the hot path (−46%). - **ATR**: reciprocal hoisted (−42%). - **EMA/RSI/ATR**: per-tick `Option<f64>` hot state → bare `f64` + ready flag. Net result vs `kand`: Wickra now wins **RSI, Bollinger and ATR** (streaming), and ties `ta-rs` on SMA — up from losing every indicator 1.5–6× before. ### Benchmark harness New `crates/wickra-bench` (publish=false): a Criterion benchmark comparing Wickra against `kand`, `ta-rs` and `yata` on an identical BTCUSDT candle series, in streaming and batch modes. Peer APIs were verified against their source, not guessed. Wired into the nightly `cross-library-bench` workflow as a separate job. ### Honest README The benchmark section is rewritten into three layered tables (Rust core vs Rust crates; Python vs the Python ecosystem) that **show the losses as well as the wins**. The "only library that combines…" claim is gone; the new framing is breadth + multi-language reach + the deliberate safety trade-off that costs raw speed. Added an origin/why-slower rationale and a star CTA. ### Python benchmark Added `tulipy` runners and expanded per-tick streaming coverage to SMA/EMA/RSI/ MACD/Bollinger. `bench.in`/`bench.txt` now lock `TA-Lib` + `tulipy` (hash-pinned); `pandas-ta` stays out (it requires Python ≥ 3.12, the bench runs on 3.11). ### Notes - TA-Lib/tulipy numbers in the README Python table are marked ⧗ — they are produced by the CI Linux job (C extensions don't build cleanly on every desktop), not measured locally. - The matching `wickra-docs` prose update is committed separately and will be pushed with the release, per the docs-don't-lead-the-registries rule. Verified locally: `cargo fmt`, `cargo test --workspace --all-features` (3413 core + bindings), `cargo clippy --workspace --all-targets --all-features -D warnings`, Node build + 498 tests, and pytest all green.
This commit is contained in:
@@ -28,9 +28,17 @@ use crate::traits::Indicator;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Atr {
|
||||
period: usize,
|
||||
/// `period - 1` as `f64`, precomputed for the Wilder smoothing step.
|
||||
n_minus_1: f64,
|
||||
/// `1 / period`, precomputed so the per-tick smoothing multiplies instead of
|
||||
/// divides.
|
||||
inv_period: f64,
|
||||
prev_close: Option<f64>,
|
||||
seed_buf: Vec<f64>,
|
||||
avg: Option<f64>,
|
||||
/// Smoothed ATR, valid once `seeded` is set. Bare `f64` + flag rather than
|
||||
/// `Option<f64>` so the hot recurrence avoids an enum-tag read per tick.
|
||||
avg: f64,
|
||||
seeded: bool,
|
||||
}
|
||||
|
||||
impl Atr {
|
||||
@@ -45,9 +53,12 @@ impl Atr {
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
n_minus_1: (period - 1) as f64,
|
||||
inv_period: 1.0 / period as f64,
|
||||
prev_close: None,
|
||||
seed_buf: Vec::with_capacity(period),
|
||||
avg: None,
|
||||
avg: 0.0,
|
||||
seeded: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,7 +69,11 @@ impl Atr {
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.avg
|
||||
if self.seeded {
|
||||
Some(self.avg)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,17 +85,18 @@ impl Indicator for Atr {
|
||||
let tr = candle.true_range(self.prev_close);
|
||||
self.prev_close = Some(candle.close);
|
||||
|
||||
if let Some(avg) = self.avg {
|
||||
let n = self.period as f64;
|
||||
let new_avg = avg.mul_add(n - 1.0, tr) / n;
|
||||
self.avg = Some(new_avg);
|
||||
if self.seeded {
|
||||
// Wilder smoothing with the reciprocal hoisted out of the hot path.
|
||||
let new_avg = self.avg.mul_add(self.n_minus_1, tr) * self.inv_period;
|
||||
self.avg = new_avg;
|
||||
return Some(new_avg);
|
||||
}
|
||||
|
||||
self.seed_buf.push(tr);
|
||||
if self.seed_buf.len() == self.period {
|
||||
let seed = self.seed_buf.iter().copied().sum::<f64>() / self.period as f64;
|
||||
self.avg = Some(seed);
|
||||
self.avg = seed;
|
||||
self.seeded = true;
|
||||
return Some(seed);
|
||||
}
|
||||
None
|
||||
@@ -89,7 +105,8 @@ impl Indicator for Atr {
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.seed_buf.clear();
|
||||
self.avg = None;
|
||||
self.avg = 0.0;
|
||||
self.seeded = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
@@ -97,7 +114,7 @@ impl Indicator for Atr {
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.avg.is_some()
|
||||
self.seeded
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Bollinger Bands.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
@@ -49,7 +47,13 @@ pub struct BollingerOutput {
|
||||
pub struct BollingerBands {
|
||||
period: usize,
|
||||
multiplier: f64,
|
||||
window: VecDeque<f64>,
|
||||
/// Fixed-capacity ring buffer of the last `period` finite inputs. A flat
|
||||
/// `Box<[f64]>` with a manual write cursor beats `VecDeque` on this hot path.
|
||||
buf: Box<[f64]>,
|
||||
/// Index of the next slot to write — also the oldest element once full.
|
||||
head: usize,
|
||||
/// Number of slots filled, saturating at `period`.
|
||||
count: usize,
|
||||
sum: f64,
|
||||
sum_sq: f64,
|
||||
/// Number of finite updates since the running sums were last reseeded
|
||||
@@ -80,7 +84,9 @@ impl BollingerBands {
|
||||
Ok(Self {
|
||||
period,
|
||||
multiplier,
|
||||
window: VecDeque::with_capacity(period),
|
||||
buf: vec![0.0; period].into_boxed_slice(),
|
||||
head: 0,
|
||||
count: 0,
|
||||
sum: 0.0,
|
||||
sum_sq: 0.0,
|
||||
updates_since_recompute: 0,
|
||||
@@ -103,7 +109,7 @@ impl BollingerBands {
|
||||
}
|
||||
|
||||
fn current(&self) -> Option<BollingerOutput> {
|
||||
if self.window.len() != self.period {
|
||||
if self.count != self.period {
|
||||
return None;
|
||||
}
|
||||
let n = self.period as f64;
|
||||
@@ -129,25 +135,38 @@ impl Indicator for BollingerBands {
|
||||
if !input.is_finite() {
|
||||
return self.current();
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let old = self.window.pop_front().expect("non-empty");
|
||||
if self.count == self.period {
|
||||
let old = self.buf[self.head];
|
||||
self.sum -= old;
|
||||
self.sum_sq -= old * old;
|
||||
self.buf[self.head] = input;
|
||||
self.sum += input;
|
||||
self.sum_sq += input * input;
|
||||
} else {
|
||||
self.buf[self.head] = input;
|
||||
self.sum += input;
|
||||
self.sum_sq += input * input;
|
||||
self.count += 1;
|
||||
}
|
||||
self.head += 1;
|
||||
if self.head == self.period {
|
||||
self.head = 0;
|
||||
}
|
||||
self.window.push_back(input);
|
||||
self.sum += input;
|
||||
self.sum_sq += input * input;
|
||||
self.updates_since_recompute += 1;
|
||||
if self.updates_since_recompute >= RECOMPUTE_EVERY * self.period {
|
||||
self.sum = self.window.iter().copied().sum();
|
||||
self.sum_sq = self.window.iter().copied().map(|x| x * x).sum();
|
||||
// Reseed in chronological order (oldest at `head`) to keep the running
|
||||
// sums bit-equivalent to a fresh from-scratch pass on stable inputs.
|
||||
let chronological = self.buf[self.head..].iter().chain(&self.buf[..self.head]);
|
||||
self.sum = chronological.clone().copied().sum();
|
||||
self.sum_sq = chronological.map(|&x| x * x).sum();
|
||||
self.updates_since_recompute = 0;
|
||||
}
|
||||
self.current()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.head = 0;
|
||||
self.count = 0;
|
||||
self.sum = 0.0;
|
||||
self.sum_sq = 0.0;
|
||||
self.updates_since_recompute = 0;
|
||||
@@ -158,7 +177,7 @@ impl Indicator for BollingerBands {
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
self.count == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
@@ -171,6 +190,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
fn naive(prices: &[f64], period: usize, mult: f64) -> BollingerOutput {
|
||||
assert!(
|
||||
|
||||
@@ -25,7 +25,15 @@ use crate::traits::Indicator;
|
||||
pub struct Ema {
|
||||
period: usize,
|
||||
alpha: f64,
|
||||
state: Option<f64>,
|
||||
/// `1 - alpha`, precomputed so the recurrence avoids a subtraction per tick.
|
||||
/// Cached value, so the steady-state output is bit-for-bit unchanged.
|
||||
one_minus_alpha: f64,
|
||||
/// Latest EMA value, valid only once `seeded` is true. Stored as a bare `f64`
|
||||
/// (plus the `seeded` flag) rather than `Option<f64>` so the steady-state
|
||||
/// recurrence reads and writes 8 bytes with no enum-tag handling per tick.
|
||||
current: f64,
|
||||
/// Whether `current` holds a real value yet (warmup complete).
|
||||
seeded: bool,
|
||||
warmup_buf: Vec<f64>,
|
||||
}
|
||||
|
||||
@@ -43,7 +51,9 @@ impl Ema {
|
||||
Ok(Self {
|
||||
period,
|
||||
alpha,
|
||||
state: None,
|
||||
one_minus_alpha: 1.0 - alpha,
|
||||
current: 0.0,
|
||||
seeded: false,
|
||||
warmup_buf: Vec::with_capacity(period),
|
||||
})
|
||||
}
|
||||
@@ -66,7 +76,9 @@ impl Ema {
|
||||
Ok(Self {
|
||||
period: 1,
|
||||
alpha,
|
||||
state: None,
|
||||
one_minus_alpha: 1.0 - alpha,
|
||||
current: 0.0,
|
||||
seeded: false,
|
||||
warmup_buf: Vec::with_capacity(1),
|
||||
})
|
||||
}
|
||||
@@ -83,21 +95,28 @@ impl Ema {
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.state
|
||||
if self.seeded {
|
||||
Some(self.current)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper that feeds a value without finiteness validation. The caller
|
||||
/// guarantees `input.is_finite()`. Used by MACD which has already validated.
|
||||
pub(crate) fn step_unchecked(&mut self, input: f64) -> Option<f64> {
|
||||
if let Some(prev) = self.state {
|
||||
let new = self.alpha.mul_add(input, (1.0 - self.alpha) * prev);
|
||||
self.state = Some(new);
|
||||
if self.seeded {
|
||||
let new = self
|
||||
.alpha
|
||||
.mul_add(input, self.one_minus_alpha * self.current);
|
||||
self.current = new;
|
||||
return Some(new);
|
||||
}
|
||||
self.warmup_buf.push(input);
|
||||
if self.warmup_buf.len() == self.period {
|
||||
let seed = self.warmup_buf.iter().copied().sum::<f64>() / self.period as f64;
|
||||
self.state = Some(seed);
|
||||
self.current = seed;
|
||||
self.seeded = true;
|
||||
return Some(seed);
|
||||
}
|
||||
None
|
||||
@@ -110,13 +129,14 @@ impl Indicator for Ema {
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
if !input.is_finite() {
|
||||
return self.state;
|
||||
return self.value();
|
||||
}
|
||||
self.step_unchecked(input)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.state = None;
|
||||
self.current = 0.0;
|
||||
self.seeded = false;
|
||||
self.warmup_buf.clear();
|
||||
}
|
||||
|
||||
@@ -125,7 +145,7 @@ impl Indicator for Ema {
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.state.is_some()
|
||||
self.seeded
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
|
||||
@@ -25,13 +25,24 @@ use crate::traits::Indicator;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rsi {
|
||||
period: usize,
|
||||
prev_close: Option<f64>,
|
||||
/// `period - 1` as `f64`, precomputed for the Wilder smoothing step.
|
||||
n_minus_1: f64,
|
||||
/// `1 / period`, precomputed so the per-tick smoothing multiplies instead of
|
||||
/// divides (a reciprocal is hoisted out of the hot path).
|
||||
inv_period: f64,
|
||||
/// Previous close, valid once `has_prev` is set. Bare `f64` + flag instead of
|
||||
/// `Option<f64>` to avoid an enum-tag read on every tick.
|
||||
prev_close: f64,
|
||||
has_prev: bool,
|
||||
// Wilder seeds with the simple average of the first `period` gains/losses,
|
||||
// then transitions to recursive smoothing.
|
||||
seed_buf_gains: Vec<f64>,
|
||||
seed_buf_losses: Vec<f64>,
|
||||
avg_gain: Option<f64>,
|
||||
avg_loss: Option<f64>,
|
||||
/// Smoothed average gain / loss, valid once `avgs_seeded` is set. Bare `f64`s
|
||||
/// + flag so the hot recurrence avoids reading two `Option<f64>` tags per tick.
|
||||
avg_gain: f64,
|
||||
avg_loss: f64,
|
||||
avgs_seeded: bool,
|
||||
last_value: Option<f64>,
|
||||
}
|
||||
|
||||
@@ -47,11 +58,15 @@ impl Rsi {
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
prev_close: None,
|
||||
n_minus_1: (period - 1) as f64,
|
||||
inv_period: 1.0 / period as f64,
|
||||
prev_close: 0.0,
|
||||
has_prev: false,
|
||||
seed_buf_gains: Vec::with_capacity(period),
|
||||
seed_buf_losses: Vec::with_capacity(period),
|
||||
avg_gain: None,
|
||||
avg_loss: None,
|
||||
avg_gain: 0.0,
|
||||
avg_loss: 0.0,
|
||||
avgs_seeded: false,
|
||||
last_value: None,
|
||||
})
|
||||
}
|
||||
@@ -67,16 +82,16 @@ impl Rsi {
|
||||
}
|
||||
|
||||
fn rsi_from_avgs(avg_gain: f64, avg_loss: f64) -> f64 {
|
||||
if avg_loss == 0.0 {
|
||||
if avg_gain == 0.0 {
|
||||
// No movement at all -> RSI undefined; standard convention returns 50.
|
||||
50.0
|
||||
} else {
|
||||
100.0
|
||||
}
|
||||
// Algebraically `100 - 100/(1 + ag/al)` collapses to `100·ag/(ag+al)`,
|
||||
// which needs a single division instead of two and removes the separate
|
||||
// `rs` step. Edge cases stay exact: `al == 0, ag > 0` gives `100·ag/ag =
|
||||
// 100`; `ag == 0, al > 0` gives `0`; both zero (no movement) is the
|
||||
// undefined case and returns the neutral 50.
|
||||
let denom = avg_gain + avg_loss;
|
||||
if denom == 0.0 {
|
||||
50.0
|
||||
} else {
|
||||
let rs = avg_gain / avg_loss;
|
||||
100.0 - 100.0 / (1.0 + rs)
|
||||
100.0 * avg_gain / denom
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,22 +105,25 @@ impl Indicator for Rsi {
|
||||
return self.last_value;
|
||||
}
|
||||
|
||||
let Some(prev) = self.prev_close else {
|
||||
self.prev_close = Some(input);
|
||||
if !self.has_prev {
|
||||
self.prev_close = input;
|
||||
self.has_prev = true;
|
||||
return None;
|
||||
};
|
||||
self.prev_close = Some(input);
|
||||
}
|
||||
let prev = self.prev_close;
|
||||
self.prev_close = input;
|
||||
|
||||
let diff = input - prev;
|
||||
let gain = if diff > 0.0 { diff } else { 0.0 };
|
||||
let loss = if diff < 0.0 { -diff } else { 0.0 };
|
||||
|
||||
if let (Some(ag), Some(al)) = (self.avg_gain, self.avg_loss) {
|
||||
let n = self.period as f64;
|
||||
let new_ag = (ag * (n - 1.0) + gain) / n;
|
||||
let new_al = (al * (n - 1.0) + loss) / n;
|
||||
self.avg_gain = Some(new_ag);
|
||||
self.avg_loss = Some(new_al);
|
||||
if self.avgs_seeded {
|
||||
// Wilder smoothing `(prev·(n-1) + x) / n` with the reciprocal hoisted:
|
||||
// a fused multiply-add then a multiply by `1/n`, no per-tick division.
|
||||
let new_ag = self.avg_gain.mul_add(self.n_minus_1, gain) * self.inv_period;
|
||||
let new_al = self.avg_loss.mul_add(self.n_minus_1, loss) * self.inv_period;
|
||||
self.avg_gain = new_ag;
|
||||
self.avg_loss = new_al;
|
||||
let v = Self::rsi_from_avgs(new_ag, new_al);
|
||||
self.last_value = Some(v);
|
||||
return Some(v);
|
||||
@@ -116,8 +134,9 @@ impl Indicator for Rsi {
|
||||
if self.seed_buf_gains.len() == self.period {
|
||||
let ag = self.seed_buf_gains.iter().sum::<f64>() / self.period as f64;
|
||||
let al = self.seed_buf_losses.iter().sum::<f64>() / self.period as f64;
|
||||
self.avg_gain = Some(ag);
|
||||
self.avg_loss = Some(al);
|
||||
self.avg_gain = ag;
|
||||
self.avg_loss = al;
|
||||
self.avgs_seeded = true;
|
||||
let v = Self::rsi_from_avgs(ag, al);
|
||||
self.last_value = Some(v);
|
||||
return Some(v);
|
||||
@@ -126,11 +145,13 @@ impl Indicator for Rsi {
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.prev_close = 0.0;
|
||||
self.has_prev = false;
|
||||
self.seed_buf_gains.clear();
|
||||
self.seed_buf_losses.clear();
|
||||
self.avg_gain = None;
|
||||
self.avg_loss = None;
|
||||
self.avg_gain = 0.0;
|
||||
self.avg_loss = 0.0;
|
||||
self.avgs_seeded = false;
|
||||
self.last_value = None;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Simple Moving Average.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
@@ -33,7 +31,14 @@ use crate::traits::Indicator;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sma {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
/// Fixed-capacity ring buffer of the last `period` finite inputs. A flat
|
||||
/// `Box<[f64]>` with a manual write cursor beats `VecDeque` on this hot path:
|
||||
/// sequential storage, branchless wraparound, no per-call bookkeeping.
|
||||
buf: Box<[f64]>,
|
||||
/// Index of the next slot to write — also the oldest element once full.
|
||||
head: usize,
|
||||
/// Number of slots filled, saturating at `period`.
|
||||
count: usize,
|
||||
sum: f64,
|
||||
/// Number of finite updates since the running `sum` was last reseeded from
|
||||
/// the live window. Caps accumulated floating-point drift on long streams.
|
||||
@@ -60,7 +65,9 @@ impl Sma {
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
buf: vec![0.0; period].into_boxed_slice(),
|
||||
head: 0,
|
||||
count: 0,
|
||||
sum: 0.0,
|
||||
updates_since_recompute: 0,
|
||||
})
|
||||
@@ -73,7 +80,7 @@ impl Sma {
|
||||
|
||||
/// Current value if available.
|
||||
pub fn value(&self) -> Option<f64> {
|
||||
if self.window.len() == self.period {
|
||||
if self.count == self.period {
|
||||
Some(self.sum / self.period as f64)
|
||||
} else {
|
||||
None
|
||||
@@ -89,25 +96,40 @@ impl Indicator for Sma {
|
||||
if !input.is_finite() {
|
||||
return self.value();
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
// Slide: drop the oldest, then add the new. Each step is a single
|
||||
// f64 add/subtract — O(1) but introduces ~1 ULP of rounding noise.
|
||||
// The periodic reseed below caps the accumulated drift.
|
||||
let old = self.window.pop_front().expect("window non-empty");
|
||||
self.sum -= old;
|
||||
if self.count == self.period {
|
||||
// Window full: overwrite the oldest slot (at `head`). Each step is a
|
||||
// single f64 add/subtract — O(1) but introduces ~1 ULP of rounding
|
||||
// noise. The periodic reseed below caps the accumulated drift.
|
||||
self.sum -= self.buf[self.head];
|
||||
self.buf[self.head] = input;
|
||||
self.sum += input;
|
||||
} else {
|
||||
self.buf[self.head] = input;
|
||||
self.sum += input;
|
||||
self.count += 1;
|
||||
}
|
||||
// Branchless-ish wraparound, cheaper than `% period`.
|
||||
self.head += 1;
|
||||
if self.head == self.period {
|
||||
self.head = 0;
|
||||
}
|
||||
self.window.push_back(input);
|
||||
self.sum += input;
|
||||
self.updates_since_recompute += 1;
|
||||
if self.updates_since_recompute >= RECOMPUTE_EVERY * self.period {
|
||||
self.sum = self.window.iter().copied().sum();
|
||||
// Reseed in chronological order (oldest at `head`) so the running sum
|
||||
// tracks a fresh from-scratch mean to the bit on stable inputs.
|
||||
self.sum = self.buf[self.head..]
|
||||
.iter()
|
||||
.chain(&self.buf[..self.head])
|
||||
.copied()
|
||||
.sum();
|
||||
self.updates_since_recompute = 0;
|
||||
}
|
||||
self.value()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.head = 0;
|
||||
self.count = 0;
|
||||
self.sum = 0.0;
|
||||
self.updates_since_recompute = 0;
|
||||
}
|
||||
@@ -117,7 +139,7 @@ impl Indicator for Sma {
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
self.count == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
@@ -130,6 +152,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_period() {
|
||||
|
||||
Reference in New Issue
Block a user