Skip to main content

finance_solution/stocks/ta/
state.rs

1//! Incremental **state machines** for live / streaming bar updates.
2//!
3//! # What this is
4//!
5//! Pure math state: [`push`](SmaState::push) one bar at a time, [`push_bars`](SmaState::push_bars)
6//! for multi-bar payloads, or seed with [`from_history`](SmaState::from_history).
7//! **Not** a market-data engine — your quant system owns the feed, symbols, and calendars.
8//!
9//! # Constructor naming: `new` → `FinanceResult` (not `try_new`)
10//!
11//! Fallible construction uses **`new`**, matching this crate’s Result-only style
12//! (`Schedule::new_repeating`, `File::open` in the standard library — fallibility lives in the
13//! return type, not a `try_` prefix). There is no panicking twin.
14//!
15//! # What this is not
16//!
17//! - No sockets, no multi-symbol registry, no auto “session open”
18//! - Day reset of VWAP/RVOL is **your** call to [`VwapState::reset`] / rebuild
19//!
20//! # Quant engine sketch (per symbol)
21//!
22//! ```
23//! use finance_solution::stocks::ta::{
24//!     StochasticParams, StochState, VwapParams, VwapState, EmaState,
25//! };
26//!
27//! const FAST: StochasticParams = StochasticParams::fast(9, 3);
28//!
29//! struct SymbolPipeline {
30//!     stoch: StochState,
31//!     vwap: VwapState,
32//!     ema20: EmaState,
33//! }
34//!
35//! impl SymbolPipeline {
36//!     fn new() -> finance_solution::FinanceResult<Self> {
37//!         Ok(Self {
38//!             stoch: StochState::new(FAST)?,
39//!             vwap: VwapState::new(VwapParams::cumulative_typical())?,
40//!             ema20: EmaState::new(20)?,
41//!         })
42//!     }
43//!
44//!     /// Seed from historical bars, then only push live bars.
45//!     fn seed_history(
46//!         &mut self,
47//!         high: &[f64],
48//!         low: &[f64],
49//!         close: &[f64],
50//!         volume: &[f64],
51//!     ) -> finance_solution::FinanceResult<()> {
52//!         self.stoch = StochState::from_history(FAST, high, low, close)?;
53//!         self.vwap = VwapState::from_history(
54//!             VwapParams::cumulative_typical(),
55//!             high, low, close, volume,
56//!         )?;
57//!         self.ema20 = EmaState::from_history(20, close)?;
58//!         Ok(())
59//!     }
60//!
61//!     fn on_bar(
62//!         &mut self,
63//!         high: f64,
64//!         low: f64,
65//!         close: f64,
66//!         volume: f64,
67//!     ) -> finance_solution::FinanceResult<()> {
68//!         let _kd = self.stoch.push(high, low, close)?;
69//!         let _vw = self.vwap.push(high, low, close, volume)?;
70//!         let _e = self.ema20.push(close)?;
71//!         Ok(())
72//!     }
73//!
74//!     /// Caller owns the calendar — e.g. regular-session open.
75//!     fn on_session_open_reset_vwap(&mut self) {
76//!         self.vwap.reset();
77//!         // stoch/ema often continue; reset only if *your* strategy wants it
78//!     }
79//! }
80//!
81//! # let mut p = SymbolPipeline::new().unwrap();
82//! # p.on_bar(10.0, 9.0, 9.5, 1000.0).unwrap();
83//! ```
84//!
85//! # Parity with batch
86//!
87//! Streaming state is defined to match batch series functions on the same path
88//! (within floating-point tolerance). Prefer batch [`compute`](super::ValidatedStochastic::compute)
89//! for research; prefer state for live multi-symbol updates.
90//!
91//! | Batch | Incremental |
92//! |-------|-------------|
93//! | [`sma`](super::sma) | [`SmaState`] |
94//! | [`ema`](super::ema) | [`EmaState`] |
95//! | [`stochastics`](super::stochastics) | [`StochState`] |
96//! | [`macd`](super::macd) | [`MacdState`] |
97//! | [`bollinger`](super::bollinger) | [`BollingerState`] |
98//! | [`keltner`](super::keltner) | [`KeltnerState`] |
99//! | [`vwap`](super::vwap) | [`VwapState`] |
100//! | [`rvol`](super::rvol) | [`RvolState`] |
101//! | [`rsi`](super::rsi) | [`RsiState`] |
102//! | [`atr`](super::atr) | [`AtrState`] |
103
104use crate::stocks::ta::bollinger::{BollingerParams, ValidatedBollinger};
105use crate::stocks::ta::common::{require_hlc, validate_positive_volume, window_stdev};
106use crate::stocks::ta::keltner::{KeltnerParams, ValidatedKeltner};
107use crate::stocks::ta::macd::{MacdParams, ValidatedMacd};
108use crate::stocks::ta::ring::{RingF64, RingPv};
109use crate::stocks::ta::rvol::{RvolParams, ValidatedRvol};
110use crate::stocks::ta::stochastic::{StochasticParams, ValidatedStochastic};
111use crate::stocks::ta::vwap::{ValidatedVwap, VwapMode, VwapParams, VwapPriceSource};
112use crate::util::error::{require_finite, FinanceError, FinanceResult};
113
114/// SMA/EMA state live in [`moving_average`](super::moving_average); re-exported here for the
115/// incremental-API index.
116pub use crate::stocks::ta::moving_average::{EmaState, SmaState};
117
118// ---------------------------------------------------------------------------
119// Stochastic
120// ---------------------------------------------------------------------------
121
122/// Incremental stochastic (fast/full via [`StochasticParams`]).
123///
124/// # Examples
125/// ```
126/// use finance_solution::stocks::ta::{StochasticParams, StochState, stochastics};
127/// const P: StochasticParams = StochasticParams::fast(5, 3);
128/// let h: Vec<_> = (0..20).map(|i| 10.0 + i as f64).collect();
129/// let l: Vec<_> = (0..20).map(|i| 9.0 + i as f64).collect();
130/// let c: Vec<_> = (0..20).map(|i| 9.5 + i as f64).collect();
131/// let batch = stochastics(&h, &l, &c, P).unwrap();
132/// let mut st = StochState::new(P).unwrap();
133/// let mut last = None;
134/// for i in 0..20 {
135///     last = st.push(h[i], l[i], c[i]).unwrap();
136/// }
137/// let (bk, bd) = batch.last_kd().unwrap();
138/// let (sk, sd) = last.unwrap();
139/// assert!((bk - sk).abs() < 1e-9 && (bd - sd).abs() < 1e-9);
140/// ```
141#[derive(Clone, Debug)]
142pub struct StochState {
143    params: StochasticParams,
144    high: RingF64,
145    low: RingF64,
146    close: RingF64,
147    raw_k: RingF64,
148    smooth_k: RingF64,
149    last_k: Option<f64>,
150    last_d: Option<f64>,
151    /// Last raw %K before smoothing (for flat-window carry-forward).
152    prev_raw_k: Option<f64>,
153}
154
155impl StochState {
156    pub fn new(params: StochasticParams) -> FinanceResult<Self> {
157        let _ = ValidatedStochastic::new(params)?;
158        Ok(Self {
159            params,
160            high: RingF64::with_capacity(params.k_period),
161            low: RingF64::with_capacity(params.k_period),
162            close: RingF64::with_capacity(params.k_period),
163            raw_k: RingF64::with_capacity(params.k_smooth),
164            smooth_k: RingF64::with_capacity(params.d_period),
165            last_k: None,
166            last_d: None,
167            prev_raw_k: None,
168        })
169    }
170
171    pub fn from_history(
172        params: StochasticParams,
173        high: &[f64],
174        low: &[f64],
175        close: &[f64],
176    ) -> FinanceResult<Self> {
177        let mut s = Self::new(params)?;
178        require_hlc(high, low, close)?;
179        for i in 0..close.len() {
180            s.push(high[i], low[i], close[i])?;
181        }
182        Ok(s)
183    }
184
185    pub fn params(&self) -> StochasticParams {
186        self.params
187    }
188
189    pub fn reset(&mut self) {
190        self.high.clear();
191        self.low.clear();
192        self.close.clear();
193        self.raw_k.clear();
194        self.smooth_k.clear();
195        self.last_k = None;
196        self.last_d = None;
197        self.prev_raw_k = None;
198    }
199
200    /// Returns `Some((%K, %D))` when both are defined; `%K`-only warm-up yields `None`
201    /// until `%D` is ready (same as batch last_kd semantics for the pair).
202    ///
203    /// For finer control use [`StochState::push_detail`].
204    pub fn push(&mut self, high: f64, low: f64, close: f64) -> FinanceResult<Option<(f64, f64)>> {
205        let d = self.push_detail(high, low, close)?;
206        match (d.k, d.d) {
207            (Some(k), Some(dd)) => Ok(Some((k, dd))),
208            _ => Ok(None),
209        }
210    }
211
212    /// Full detail for this bar (either line may still be warming up).
213    pub fn push_detail(
214        &mut self,
215        high: f64,
216        low: f64,
217        close: f64,
218    ) -> FinanceResult<StochBarOutput> {
219        require_finite("high", high)?;
220        require_finite("low", low)?;
221        require_finite("close", close)?;
222        if high < low {
223            return Err(FinanceError::InvalidCashflow {
224                message: "high must be >= low for each bar",
225            });
226        }
227        self.high.push(high);
228        self.low.push(low);
229        self.close.push(close);
230
231        let mut k_out = None;
232        let mut d_out = None;
233
234        if self.high.is_full() {
235            let hh = self.high.max().unwrap();
236            let ll = self.low.min().unwrap();
237            let range = hh - ll;
238            // Flat window: carry previous raw %K, else 50 (matches batch stochastics).
239            let raw = if range == 0.0 {
240                self.prev_raw_k.unwrap_or(50.0)
241            } else {
242                100.0 * (close - ll) / range
243            };
244            self.prev_raw_k = Some(raw);
245            self.raw_k.push(raw);
246            if self.raw_k.is_full() {
247                let sk = self.raw_k.sum() / self.params.k_smooth as f64;
248                self.last_k = Some(sk);
249                k_out = Some(sk);
250                self.smooth_k.push(sk);
251                if self.smooth_k.is_full() {
252                    let d = self.smooth_k.sum() / self.params.d_period as f64;
253                    self.last_d = Some(d);
254                    d_out = Some(d);
255                }
256            }
257        }
258        Ok(StochBarOutput { k: k_out, d: d_out })
259    }
260
261    pub fn last_kd(&self) -> Option<(f64, f64)> {
262        Some((self.last_k?, self.last_d?))
263    }
264
265    /// Push aligned H/L/C slices (same length) from one streaming payload.
266    pub fn push_bars(
267        &mut self,
268        high: &[f64],
269        low: &[f64],
270        close: &[f64],
271    ) -> FinanceResult<Vec<StochBarOutput>> {
272        require_hlc(high, low, close)?;
273        let mut out = Vec::with_capacity(close.len());
274        for i in 0..close.len() {
275            out.push(self.push_detail(high[i], low[i], close[i])?);
276        }
277        Ok(out)
278    }
279}
280
281/// One-bar stochastic output (warm-up allowed as `None`).
282#[derive(Clone, Copy, Debug, PartialEq)]
283pub struct StochBarOutput {
284    pub k: Option<f64>,
285    pub d: Option<f64>,
286}
287
288// ---------------------------------------------------------------------------
289// MACD
290// ---------------------------------------------------------------------------
291
292/// Incremental MACD (fast/slow/signal EMAs).
293#[derive(Clone, Debug)]
294pub struct MacdState {
295    params: MacdParams,
296    fast: EmaState,
297    slow: EmaState,
298    signal: EmaState,
299    last: Option<(f64, f64, f64)>,
300}
301
302impl MacdState {
303    pub fn new(params: MacdParams) -> FinanceResult<Self> {
304        let _ = ValidatedMacd::new(params)?;
305        Ok(Self {
306            params,
307            fast: EmaState::new(params.fast)?,
308            slow: EmaState::new(params.slow)?,
309            signal: EmaState::new(params.signal)?,
310            last: None,
311        })
312    }
313
314    pub fn from_history(params: MacdParams, closes: &[f64]) -> FinanceResult<Self> {
315        let mut s = Self::new(params)?;
316        for &c in closes {
317            s.push(c)?;
318        }
319        Ok(s)
320    }
321
322    pub fn params(&self) -> MacdParams {
323        self.params
324    }
325
326    pub fn reset(&mut self) {
327        self.fast.reset();
328        self.slow.reset();
329        self.signal.reset();
330        self.last = None;
331    }
332
333    /// `Some((macd, signal, hist))` when all three are defined.
334    pub fn push(&mut self, close: f64) -> FinanceResult<Option<(f64, f64, f64)>> {
335        let f = self.fast.push(close)?;
336        let s = self.slow.push(close)?;
337        let macd_line = match (f, s) {
338            (Some(a), Some(b)) => a - b,
339            _ => return Ok(None),
340        };
341        let sig = self.signal.push(macd_line)?;
342        match sig {
343            Some(signal) => {
344                let hist = macd_line - signal;
345                self.last = Some((macd_line, signal, hist));
346                Ok(Some((macd_line, signal, hist)))
347            }
348            None => {
349                self.last = None;
350                Ok(None)
351            }
352        }
353    }
354
355    pub fn last(&self) -> Option<(f64, f64, f64)> {
356        self.last
357    }
358
359    /// Push many closes from one payload.
360    pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<(f64, f64, f64)>>> {
361        let mut out = Vec::with_capacity(closes.len());
362        for &c in closes {
363            out.push(self.push(c)?);
364        }
365        Ok(out)
366    }
367}
368
369// ---------------------------------------------------------------------------
370// Bollinger
371// ---------------------------------------------------------------------------
372
373/// Incremental Bollinger Bands (sample stdev on the window).
374#[derive(Clone, Debug)]
375pub struct BollingerState {
376    params: BollingerParams,
377    ring: RingF64,
378    scratch: Vec<f64>,
379    last: Option<BollingerBarOutput>,
380}
381
382/// One-bar Bollinger output.
383#[derive(Clone, Copy, Debug, PartialEq)]
384pub struct BollingerBarOutput {
385    pub middle: f64,
386    pub upper: f64,
387    pub lower: f64,
388    pub pct_b: Option<f64>,
389}
390
391impl BollingerState {
392    pub fn new(params: BollingerParams) -> FinanceResult<Self> {
393        let _ = ValidatedBollinger::new(params)?;
394        Ok(Self {
395            params,
396            ring: RingF64::with_capacity(params.period),
397            scratch: Vec::with_capacity(params.period),
398            last: None,
399        })
400    }
401
402    pub fn from_history(params: BollingerParams, closes: &[f64]) -> FinanceResult<Self> {
403        let mut s = Self::new(params)?;
404        for &c in closes {
405            s.push(c)?;
406        }
407        Ok(s)
408    }
409
410    pub fn params(&self) -> BollingerParams {
411        self.params
412    }
413
414    pub fn reset(&mut self) {
415        self.ring.clear();
416        self.last = None;
417    }
418
419    pub fn push(&mut self, close: f64) -> FinanceResult<Option<BollingerBarOutput>> {
420        require_finite("close", close)?;
421        self.ring.push(close);
422        if !self.ring.is_full() {
423            self.last = None;
424            return Ok(None);
425        }
426        self.ring.copy_ordered(&mut self.scratch);
427        let mid = self.ring.sum() / self.params.period as f64;
428        let sd = window_stdev(&self.scratch, self.params.stdev).unwrap_or(0.0);
429        let band = self.params.num_std * sd;
430        let upper = mid + band;
431        let lower = mid - band;
432        let width = upper - lower;
433        let pct_b = if width > 0.0 {
434            Some((close - lower) / width)
435        } else {
436            None
437        };
438        let out = BollingerBarOutput {
439            middle: mid,
440            upper,
441            lower,
442            pct_b,
443        };
444        self.last = Some(out);
445        Ok(Some(out))
446    }
447
448    pub fn last(&self) -> Option<BollingerBarOutput> {
449        self.last
450    }
451
452    /// Push many closes from one payload.
453    pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<BollingerBarOutput>>> {
454        let mut out = Vec::with_capacity(closes.len());
455        for &c in closes {
456            out.push(self.push(c)?);
457        }
458        Ok(out)
459    }
460}
461
462// ---------------------------------------------------------------------------
463// Keltner
464// ---------------------------------------------------------------------------
465
466/// Incremental Keltner (EMA mid + Wilder ATR).
467#[derive(Clone, Debug)]
468pub struct KeltnerState {
469    params: KeltnerParams,
470    mid: EmaState,
471    atr: crate::stocks::ta::atr::AtrState,
472    last: Option<KeltnerBarOutput>,
473}
474
475#[derive(Clone, Copy, Debug, PartialEq)]
476pub struct KeltnerBarOutput {
477    pub middle: f64,
478    pub upper: f64,
479    pub lower: f64,
480    pub atr: f64,
481}
482
483impl KeltnerState {
484    pub fn new(params: KeltnerParams) -> FinanceResult<Self> {
485        let _ = ValidatedKeltner::new(params)?;
486        Ok(Self {
487            params,
488            mid: EmaState::new(params.ema_period)?,
489            atr: crate::stocks::ta::atr::AtrState::new(crate::stocks::ta::atr::AtrParams::new(
490                params.atr_period,
491            ))?,
492            last: None,
493        })
494    }
495
496    pub fn from_history(
497        params: KeltnerParams,
498        high: &[f64],
499        low: &[f64],
500        close: &[f64],
501    ) -> FinanceResult<Self> {
502        let mut s = Self::new(params)?;
503        require_hlc(high, low, close)?;
504        for i in 0..close.len() {
505            s.push(high[i], low[i], close[i])?;
506        }
507        Ok(s)
508    }
509
510    pub fn params(&self) -> KeltnerParams {
511        self.params
512    }
513
514    pub fn reset(&mut self) {
515        self.mid.reset();
516        self.atr.reset();
517        self.last = None;
518    }
519
520    pub fn push(
521        &mut self,
522        high: f64,
523        low: f64,
524        close: f64,
525    ) -> FinanceResult<Option<KeltnerBarOutput>> {
526        let atr_val = self.atr.push(high, low, close)?;
527        let mid = self.mid.push(close)?;
528        match (mid, atr_val) {
529            (Some(m), Some(a)) => {
530                let out = KeltnerBarOutput {
531                    middle: m,
532                    upper: m + self.params.atr_mult * a,
533                    lower: m - self.params.atr_mult * a,
534                    atr: a,
535                };
536                self.last = Some(out);
537                Ok(Some(out))
538            }
539            _ => {
540                self.last = None;
541                Ok(None)
542            }
543        }
544    }
545
546    pub fn last(&self) -> Option<KeltnerBarOutput> {
547        self.last
548    }
549
550    /// Push aligned H/L/C bars from one payload.
551    pub fn push_bars(
552        &mut self,
553        high: &[f64],
554        low: &[f64],
555        close: &[f64],
556    ) -> FinanceResult<Vec<Option<KeltnerBarOutput>>> {
557        require_hlc(high, low, close)?;
558        let mut out = Vec::with_capacity(close.len());
559        for i in 0..close.len() {
560            out.push(self.push(high[i], low[i], close[i])?);
561        }
562        Ok(out)
563    }
564}
565
566// ---------------------------------------------------------------------------
567// VWAP
568// ---------------------------------------------------------------------------
569
570/// Incremental VWAP (cumulative or rolling). Call [`VwapState::reset`] at session open if desired.
571///
572/// # Examples
573/// ```
574/// use finance_solution::stocks::ta::{VwapParams, VwapState};
575/// let mut v = VwapState::new(VwapParams::cumulative_typical()).unwrap();
576/// let a = v.push(10.0, 9.0, 9.5, 100.0).unwrap().unwrap();
577/// let b = v.push(11.0, 10.0, 10.5, 200.0).unwrap().unwrap();
578/// assert!(b > a || (b - a).abs() < 1.0); // volume-weighted drift
579/// v.reset(); // e.g. new regular trading day — caller decides
580/// assert!(v.push(10.0, 10.0, 10.0, 50.0).unwrap().unwrap() - 10.0 < 1e-12);
581/// ```
582#[derive(Clone, Debug)]
583pub struct VwapState {
584    params: VwapParams,
585    // cumulative
586    cum_pv: f64,
587    cum_v: f64,
588    // rolling
589    rolling: Option<RingPv>,
590    last: Option<f64>,
591}
592
593impl VwapState {
594    pub fn new(params: VwapParams) -> FinanceResult<Self> {
595        let _ = ValidatedVwap::new(params)?;
596        let rolling = match params.mode {
597            VwapMode::Cumulative => None,
598            VwapMode::Rolling { period } => Some(RingPv::with_capacity(period)),
599        };
600        Ok(Self {
601            params,
602            cum_pv: 0.0,
603            cum_v: 0.0,
604            rolling,
605            last: None,
606        })
607    }
608
609    pub fn from_history(
610        params: VwapParams,
611        high: &[f64],
612        low: &[f64],
613        close: &[f64],
614        volume: &[f64],
615    ) -> FinanceResult<Self> {
616        let mut s = Self::new(params)?;
617        require_hlc(high, low, close)?;
618        validate_positive_volume(volume)?;
619        if close.len() != volume.len() {
620            return Err(FinanceError::LengthMismatch {
621                left: close.len(),
622                right: volume.len(),
623                context: "close/volume",
624            });
625        }
626        for i in 0..close.len() {
627            s.push(high[i], low[i], close[i], volume[i])?;
628        }
629        Ok(s)
630    }
631
632    pub fn params(&self) -> VwapParams {
633        self.params
634    }
635
636    /// Clear cumulative / rolling window (typical: start of session).
637    pub fn reset(&mut self) {
638        self.cum_pv = 0.0;
639        self.cum_v = 0.0;
640        if let Some(r) = self.rolling.as_mut() {
641            r.clear();
642        }
643        self.last = None;
644    }
645
646    pub fn push(
647        &mut self,
648        high: f64,
649        low: f64,
650        close: f64,
651        volume: f64,
652    ) -> FinanceResult<Option<f64>> {
653        require_finite("high", high)?;
654        require_finite("low", low)?;
655        require_finite("close", close)?;
656        require_finite("volume", volume)?;
657        if high < low {
658            return Err(FinanceError::InvalidCashflow {
659                message: "high must be >= low for each bar",
660            });
661        }
662        if volume < 0.0 {
663            return Err(FinanceError::InvalidCashflow {
664                message: "volume must be non-negative",
665            });
666        }
667        let price = match self.params.price_source {
668            VwapPriceSource::Typical => (high + low + close) / 3.0,
669            VwapPriceSource::Close => close,
670        };
671        let out = match self.params.mode {
672            VwapMode::Cumulative => {
673                self.cum_pv += price * volume;
674                self.cum_v += volume;
675                if self.cum_v > 0.0 {
676                    Some(self.cum_pv / self.cum_v)
677                } else {
678                    None
679                }
680            }
681            VwapMode::Rolling { period } => {
682                let ring = self.rolling.as_mut().unwrap();
683                ring.push(price, volume);
684                if ring.len() >= period {
685                    ring.vwap()
686                } else {
687                    None
688                }
689            }
690        };
691        self.last = out;
692        Ok(out)
693    }
694
695    pub fn last(&self) -> Option<f64> {
696        self.last
697    }
698
699    /// Push aligned OHLC+V bars from one payload.
700    pub fn push_bars(
701        &mut self,
702        high: &[f64],
703        low: &[f64],
704        close: &[f64],
705        volume: &[f64],
706    ) -> FinanceResult<Vec<Option<f64>>> {
707        require_hlc(high, low, close)?;
708        validate_positive_volume(volume)?;
709        if close.len() != volume.len() {
710            return Err(FinanceError::LengthMismatch {
711                left: close.len(),
712                right: volume.len(),
713                context: "close/volume",
714            });
715        }
716        let mut out = Vec::with_capacity(close.len());
717        for i in 0..close.len() {
718            out.push(self.push(high[i], low[i], close[i], volume[i])?);
719        }
720        Ok(out)
721    }
722}
723
724// ---------------------------------------------------------------------------
725// RVOL
726// ---------------------------------------------------------------------------
727
728/// Incremental relative volume.
729#[derive(Clone, Debug)]
730pub struct RvolState {
731    params: RvolParams,
732    ring: RingF64,
733    last: Option<f64>,
734}
735
736impl RvolState {
737    pub fn new(params: RvolParams) -> FinanceResult<Self> {
738        let _ = ValidatedRvol::new(params)?;
739        Ok(Self {
740            params,
741            ring: RingF64::with_capacity(params.lookback),
742            last: None,
743        })
744    }
745
746    pub fn from_history(params: RvolParams, volume: &[f64]) -> FinanceResult<Self> {
747        let mut s = Self::new(params)?;
748        for &v in volume {
749            s.push(v)?;
750        }
751        Ok(s)
752    }
753
754    pub fn params(&self) -> RvolParams {
755        self.params
756    }
757
758    pub fn reset(&mut self) {
759        self.ring.clear();
760        self.last = None;
761    }
762
763    pub fn push(&mut self, volume: f64) -> FinanceResult<Option<f64>> {
764        require_finite("volume", volume)?;
765        if volume < 0.0 {
766            return Err(FinanceError::InvalidCashflow {
767                message: "volume must be non-negative",
768            });
769        }
770        self.ring.push(volume);
771        if !self.ring.is_full() {
772            self.last = None;
773            return Ok(None);
774        }
775        let mean = self.ring.sum() / self.params.lookback as f64;
776        let out = if mean > 0.0 {
777            Some(volume / mean)
778        } else {
779            None
780        };
781        self.last = out;
782        Ok(out)
783    }
784
785    pub fn last(&self) -> Option<f64> {
786        self.last
787    }
788
789    /// Push many volume samples from one payload.
790    pub fn push_bars(&mut self, volume: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
791        let mut out = Vec::with_capacity(volume.len());
792        for &v in volume {
793            out.push(self.push(v)?);
794        }
795        Ok(out)
796    }
797}
798
799// ---------------------------------------------------------------------------
800// Parity tests
801// ---------------------------------------------------------------------------
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806    use crate::stocks::ta::bollinger::bollinger;
807    use crate::stocks::ta::keltner::keltner;
808    use crate::stocks::ta::macd::macd;
809    use crate::stocks::ta::moving_average::{ema, sma};
810    use crate::stocks::ta::rvol::rvol;
811    use crate::stocks::ta::stochastic::stochastics;
812    use crate::stocks::ta::vwap::vwap;
813
814    fn path(n: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
815        let close: Vec<_> = (0..n)
816            .map(|i| 100.0 + i as f64 * 0.13 + ((i % 7) as f64) * 0.04)
817            .collect();
818        let high: Vec<_> = close.iter().map(|c| c + 0.35).collect();
819        let low: Vec<_> = close.iter().map(|c| c - 0.35).collect();
820        let vol: Vec<_> = (0..n).map(|i| 800.0 + i as f64 * 3.0).collect();
821        (high, low, close, vol)
822    }
823
824    fn approx_opt(a: Option<f64>, b: Option<f64>) {
825        match (a, b) {
826            (None, None) => {}
827            (Some(x), Some(y)) => assert!((x - y).abs() < 1e-9, "{x} vs {y}"),
828            _ => panic!("Option mismatch {a:?} vs {b:?}"),
829        }
830    }
831
832    #[test]
833    fn sma_parity() {
834        let (_, _, c, _) = path(40);
835        let batch = sma(&c, 10).unwrap();
836        let mut st = SmaState::new(10).unwrap();
837        for i in 0..c.len() {
838            approx_opt(st.push(c[i]).unwrap(), batch[i]);
839        }
840    }
841
842    #[test]
843    fn ema_parity() {
844        let (_, _, c, _) = path(40);
845        let batch = ema(&c, 10).unwrap();
846        let mut st = EmaState::new(10).unwrap();
847        for i in 0..c.len() {
848            approx_opt(st.push(c[i]).unwrap(), batch[i]);
849        }
850    }
851
852    #[test]
853    fn stoch_parity() {
854        let (h, l, c, _) = path(50);
855        let p = StochasticParams::full(14, 3, 3);
856        let batch = stochastics(&h, &l, &c, p).unwrap();
857        let mut st = StochState::new(p).unwrap();
858        for i in 0..c.len() {
859            let d = st.push_detail(h[i], l[i], c[i]).unwrap();
860            approx_opt(d.k, batch.k[i]);
861            approx_opt(d.d, batch.d[i]);
862        }
863    }
864
865    #[test]
866    fn macd_parity() {
867        let (_, _, c, _) = path(60);
868        let p = MacdParams::standard();
869        let batch = macd(&c, p).unwrap();
870        let mut st = MacdState::new(p).unwrap();
871        for i in 0..c.len() {
872            let o = st.push(c[i]).unwrap();
873            match (o, batch.signal[i], batch.histogram[i], batch.macd[i]) {
874                (Some((m, s, h)), Some(bs), Some(bh), Some(bm)) => {
875                    assert!((m - bm).abs() < 1e-8, "macd {i}");
876                    assert!((s - bs).abs() < 1e-8, "signal {i}");
877                    assert!((h - bh).abs() < 1e-8, "hist {i}");
878                }
879                (None, None, None, _) => {} // warm-up or macd-only in batch
880                other => panic!("macd parity at {i}: {other:?}"),
881            }
882        }
883        let bl = batch.last().unwrap();
884        let sl = st.last().unwrap();
885        assert!((bl.0 - sl.0).abs() < 1e-8);
886        assert!((bl.1 - sl.1).abs() < 1e-8);
887        assert!((bl.2 - sl.2).abs() < 1e-8);
888    }
889
890    #[test]
891    fn bollinger_parity() {
892        let (_, _, c, _) = path(40);
893        let p = BollingerParams::standard();
894        let batch = bollinger(&c, p).unwrap();
895        let mut st = BollingerState::new(p).unwrap();
896        for i in 0..c.len() {
897            let o = st.push(c[i]).unwrap();
898            match (o, batch.middle[i]) {
899                (None, None) => {}
900                (Some(bo), Some(m)) => {
901                    assert!((bo.middle - m).abs() < 1e-9);
902                    assert!((bo.upper - batch.upper[i].unwrap()).abs() < 1e-9);
903                    assert!((bo.lower - batch.lower[i].unwrap()).abs() < 1e-9);
904                }
905                other => panic!("{other:?}"),
906            }
907        }
908    }
909
910    #[test]
911    fn keltner_parity() {
912        let (h, l, c, _) = path(45);
913        let p = KeltnerParams::standard();
914        let batch = keltner(&h, &l, &c, p).unwrap();
915        let mut st = KeltnerState::new(p).unwrap();
916        for i in 0..c.len() {
917            let o = st.push(h[i], l[i], c[i]).unwrap();
918            match (o, batch.middle[i], batch.upper[i], batch.atr[i]) {
919                (Some(ko), Some(m), Some(u), Some(a)) => {
920                    assert!((ko.middle - m).abs() < 1e-8, "mid {i}");
921                    assert!((ko.upper - u).abs() < 1e-8, "upper {i}");
922                    assert!((ko.atr - a).abs() < 1e-8, "atr {i}");
923                }
924                (None, _, None, _) | (None, None, _, _) => {} // mid and/or atr still warming
925                other => panic!("keltner parity {i}: {other:?}"),
926            }
927        }
928        let sl = st.last().unwrap();
929        let bl_m = batch.middle.iter().rev().find_map(|x| *x).unwrap();
930        let bl_a = batch.atr.iter().rev().find_map(|x| *x).unwrap();
931        assert!((bl_m - sl.middle).abs() < 1e-8);
932        assert!((bl_a - sl.atr).abs() < 1e-8);
933    }
934
935    #[test]
936    fn vwap_cum_parity() {
937        let (h, l, c, v) = path(30);
938        let p = VwapParams::cumulative_typical();
939        let batch = vwap(&h, &l, &c, &v, p).unwrap();
940        let mut st = VwapState::new(p).unwrap();
941        for i in 0..c.len() {
942            approx_opt(st.push(h[i], l[i], c[i], v[i]).unwrap(), batch.vwap[i]);
943        }
944    }
945
946    #[test]
947    fn vwap_reset() {
948        let mut st = VwapState::new(VwapParams::cumulative_typical()).unwrap();
949        st.push(10.0, 10.0, 10.0, 100.0).unwrap();
950        st.reset();
951        let x = st.push(20.0, 20.0, 20.0, 50.0).unwrap().unwrap();
952        assert!((x - 20.0).abs() < 1e-12);
953    }
954
955    #[test]
956    fn rvol_parity() {
957        let (_, _, _, v) = path(40);
958        let p = RvolParams::days_20();
959        let batch = rvol(&v, p).unwrap();
960        let mut st = RvolState::new(p).unwrap();
961        for i in 0..v.len() {
962            approx_opt(st.push(v[i]).unwrap(), batch.rvol[i]);
963        }
964    }
965
966    #[test]
967    fn rsi_atr_parity_via_from_history() {
968        let (h, l, c, _) = path(60);
969        let rsi_b = crate::stocks::ta::rsi::rsi(&c, crate::stocks::ta::rsi::RsiParams::period_14())
970            .unwrap();
971        let rsi_s = crate::stocks::ta::rsi::RsiState::from_history(
972            crate::stocks::ta::rsi::RsiParams::period_14(),
973            &c,
974        )
975        .unwrap();
976        approx_opt(rsi_b.last(), rsi_s.last());
977
978        let atr_b =
979            crate::stocks::ta::atr::atr(&h, &l, &c, crate::stocks::ta::atr::AtrParams::period_14())
980                .unwrap();
981        let atr_s = crate::stocks::ta::atr::AtrState::from_history(
982            crate::stocks::ta::atr::AtrParams::period_14(),
983            &h,
984            &l,
985            &c,
986        )
987        .unwrap();
988        approx_opt(atr_b.last(), atr_s.last());
989    }
990
991    #[test]
992    fn from_history_matches_push() {
993        let (h, l, c, _) = path(25);
994        let p = StochasticParams::fast(9, 3);
995        let a = StochState::from_history(p, &h, &l, &c).unwrap();
996        let mut b = StochState::new(p).unwrap();
997        for i in 0..c.len() {
998            b.push(h[i], l[i], c[i]).unwrap();
999        }
1000        assert_eq!(a.last_kd(), b.last_kd());
1001    }
1002}