Skip to main content

quantwave_core/indicators/incremental/
ma_stream.rs

1//! Streaming moving average dispatched on [`MaType`] — TA-Lib `compute_ma` parity.
2//!
3//! Every one of the nine TA-Lib MA families is wired to its native QuantWave
4//! streaming implementation. Previously this enum carried only `Sma` and `Ema`
5//! variants and a catch-all arm collapsed Wma/Dema/Tema/Trima/Kama/Mama/T3 to
6//! SMA, so `APO::new(12, 26, MaType::Wma)` silently returned an SMA-based APO.
7//! There is no silent substitution left: the dispatch is total.
8
9use crate::indicators::incremental::dema::DEMA;
10use crate::indicators::incremental::hilbert_ta::TalibMama;
11use crate::indicators::incremental::overlap_ta::{KAMA, T3, TRIMA};
12use crate::indicators::incremental::talib_ema::TalibEma;
13use crate::indicators::incremental::talib_sma::TalibSma;
14use crate::indicators::incremental::talib_tema::TalibTema;
15use crate::indicators::incremental::talib_wma::TalibWma;
16use crate::indicators::ma_type::MaType;
17use crate::traits::Next;
18
19/// `vfactor` C TA-Lib's `ta_MA.c` uses when T3 is reached through the MA dispatcher.
20pub const MA_DISPATCH_T3_VFACTOR: f64 = 0.7;
21/// `fastlimit` C TA-Lib's `ta_MA.c` uses when MAMA is reached through the MA dispatcher.
22pub const MA_DISPATCH_MAMA_FASTLIMIT: f64 = 0.5;
23/// `slowlimit` C TA-Lib's `ta_MA.c` uses when MAMA is reached through the MA dispatcher.
24pub const MA_DISPATCH_MAMA_SLOWLIMIT: f64 = 0.05;
25
26/// Streaming MA matching `talib_rs`'s `compute_ma` for all nine `MaType`s.
27#[derive(Debug, Clone)]
28pub enum MaStream {
29    Sma(TalibSma),
30    Ema(TalibEma),
31    Wma(TalibWma),
32    Dema(DEMA),
33    Tema(TalibTema),
34    Trima(TRIMA),
35    Kama(KAMA),
36    /// Boxed: the Hilbert engine dwarfs the other variants.
37    Mama(Box<TalibMama>),
38    T3(T3),
39}
40
41impl MaStream {
42    /// Build the streaming MA for `ma_type`.
43    ///
44    /// `MaType::Mama` ignores `period` and uses `fastlimit = 0.5`,
45    /// `slowlimit = 0.05`; `MaType::T3` uses `vfactor = 0.7`. Both match what
46    /// C TA-Lib's `ta_MA.c` does when those families are selected through a
47    /// `matype` argument.
48    pub fn new(period: usize, ma_type: MaType) -> Self {
49        match ma_type {
50            MaType::Sma => Self::Sma(TalibSma::new(period)),
51            MaType::Ema => Self::Ema(TalibEma::new(period)),
52            MaType::Wma => Self::Wma(TalibWma::new(period)),
53            MaType::Dema => Self::Dema(DEMA::new(period)),
54            MaType::Tema => Self::Tema(TalibTema::new(period)),
55            MaType::Trima => Self::Trima(TRIMA::new(period)),
56            MaType::Kama => Self::Kama(KAMA::new(period)),
57            MaType::Mama => Self::Mama(Box::new(TalibMama::new(
58                MA_DISPATCH_MAMA_FASTLIMIT,
59                MA_DISPATCH_MAMA_SLOWLIMIT,
60            ))),
61            MaType::T3 => Self::T3(T3::new(period, MA_DISPATCH_T3_VFACTOR)),
62        }
63    }
64
65    /// The `MaType` this stream was built from.
66    pub fn ma_type(&self) -> MaType {
67        match self {
68            Self::Sma(_) => MaType::Sma,
69            Self::Ema(_) => MaType::Ema,
70            Self::Wma(_) => MaType::Wma,
71            Self::Dema(_) => MaType::Dema,
72            Self::Tema(_) => MaType::Tema,
73            Self::Trima(_) => MaType::Trima,
74            Self::Kama(_) => MaType::Kama,
75            Self::Mama(_) => MaType::Mama,
76            Self::T3(_) => MaType::T3,
77        }
78    }
79
80    pub fn next(&mut self, v: f64) -> f64 {
81        match self {
82            Self::Sma(s) => s.next(v),
83            Self::Ema(e) => e.next(v),
84            Self::Wma(w) => w.next(v),
85            Self::Dema(d) => d.next(v),
86            Self::Tema(t) => t.next(v),
87            Self::Trima(t) => t.next(v),
88            Self::Kama(k) => k.next(v),
89            Self::Mama(m) => m.next(v).0,
90            Self::T3(t) => t.next(v),
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use proptest::prelude::*;
99
100    /// Batch oracle: `talib_rs`'s `compute_ma` dispatch, spelled out.
101    pub(crate) fn ma_oracle(input: &[f64], period: usize, ma_type: MaType) -> Vec<f64> {
102        let batch = match ma_type {
103            MaType::Sma => talib_rs::overlap::sma(input, period),
104            MaType::Ema => talib_rs::overlap::ema(input, period),
105            MaType::Wma => talib_rs::overlap::wma(input, period),
106            MaType::Dema => talib_rs::overlap::dema(input, period),
107            MaType::Tema => talib_rs::overlap::tema(input, period),
108            MaType::Trima => talib_rs::overlap::trima(input, period),
109            MaType::Kama => talib_rs::overlap::kama(input, period),
110            MaType::Mama => talib_rs::overlap::mama(
111                input,
112                MA_DISPATCH_MAMA_FASTLIMIT,
113                MA_DISPATCH_MAMA_SLOWLIMIT,
114            )
115            .map(|(mama, _fama)| mama),
116            MaType::T3 => talib_rs::overlap::t3(input, period, MA_DISPATCH_T3_VFACTOR),
117        };
118        batch.unwrap_or_else(|_| vec![f64::NAN; input.len()])
119    }
120
121    proptest! {
122        /// Regression test for the silent-SMA-substitution bug: every `MaType`
123        /// must track its own batch oracle. Parameterised over all nine
124        /// variants — the old suite only ever constructed `MaType::Sma`, which
125        /// is exactly why the catch-all arm survived unnoticed.
126        #[test]
127        fn test_ma_stream_parity_all_matypes(
128            input in prop::collection::vec(0.1..100.0, 120..200),
129            idx in 0usize..9,
130        ) {
131            let ma_type = MaType::ALL[idx];
132            let period = 10;
133            let mut stream = MaStream::new(period, ma_type);
134            let streaming: Vec<f64> = input.iter().map(|&x| stream.next(x)).collect();
135            let batch = ma_oracle(&input, period, ma_type);
136            prop_assert_eq!(stream.ma_type(), ma_type);
137            for (i, (s, b)) in streaming.iter().zip(batch.iter()).enumerate() {
138                if s.is_nan() {
139                    prop_assert!(b.is_nan(), "{ma_type} bar {i}: streaming NaN, batch {b}");
140                } else {
141                    prop_assert!(!b.is_nan(), "{ma_type} bar {i}: streaming {s}, batch NaN");
142                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
143                }
144            }
145        }
146    }
147
148    /// Non-SMA `MaType`s used to silently collapse to SMA. They must not any more.
149    #[test]
150    fn non_sma_matypes_differ_from_sma() {
151        let input: Vec<f64> = (0..200)
152            .map(|i| 50.0 + 10.0 * (i as f64 * 0.3).sin() + i as f64 * 0.05)
153            .collect();
154        let sma: Vec<f64> = {
155            let mut s = MaStream::new(10, MaType::Sma);
156            input.iter().map(|&x| s.next(x)).collect()
157        };
158        for ma_type in MaType::ALL {
159            if ma_type == MaType::Sma {
160                continue;
161            }
162            let mut s = MaStream::new(10, ma_type);
163            let got: Vec<f64> = input.iter().map(|&x| s.next(x)).collect();
164            let identical = got
165                .iter()
166                .zip(sma.iter())
167                .all(|(a, b)| (a.is_nan() && b.is_nan()) || (a - b).abs() < 1e-12);
168            assert!(!identical, "{ma_type} still produces SMA values");
169        }
170    }
171}