Skip to main content

quantwave_core/indicators/incremental/
talib_tema.rs

1//! TA-Lib compatible streaming TEMA — O(1) per bar.
2//!
3//! `TEMA = 3*EMA1 - 3*EMA2 + EMA3`, with each layer seeded by the SMA of the
4//! previous layer's first `period` outputs (exactly what [`TalibEma`] does), so
5//! the lookback is `3 * (period - 1)` as in `talib_rs::overlap::tema`.
6//!
7//! Distinct from [`crate::indicators::tema::TEMA`], which uses the general
8//! `EMA` (seeded from the first sample, no NaN warmup) and therefore does not
9//! reproduce TA-Lib's values.
10
11use crate::indicators::incremental::talib_ema::TalibEma;
12use crate::traits::Next;
13
14/// Triple exponential moving average matching `talib_rs::overlap::tema`.
15#[derive(Debug, Clone)]
16pub struct TalibTema {
17    ema1: TalibEma,
18    ema2: TalibEma,
19    ema3: TalibEma,
20}
21
22impl TalibTema {
23    pub fn new(period: usize) -> Self {
24        Self {
25            ema1: TalibEma::new(period),
26            ema2: TalibEma::new(period),
27            ema3: TalibEma::new(period),
28        }
29    }
30}
31
32impl Next<f64> for TalibTema {
33    type Output = f64;
34
35    fn next(&mut self, input: f64) -> Self::Output {
36        let e1 = self.ema1.next(input);
37        if e1.is_nan() {
38            return f64::NAN;
39        }
40        let e2 = self.ema2.next(e1);
41        if e2.is_nan() {
42            return f64::NAN;
43        }
44        let e3 = self.ema3.next(e2);
45        if e3.is_nan() {
46            return f64::NAN;
47        }
48        3.0 * e1 - 3.0 * e2 + e3
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use proptest::prelude::*;
56
57    proptest! {
58        #[test]
59        fn test_talib_tema_parity(input in prop::collection::vec(0.1..100.0, 1..120)) {
60            let period = 10;
61            let mut tema = TalibTema::new(period);
62            let streaming: Vec<f64> = input.iter().map(|&x| tema.next(x)).collect();
63            let batch = talib_rs::overlap::tema(&input, period)
64                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
65            for (s, b) in streaming.iter().zip(batch.iter()) {
66                if s.is_nan() { assert!(b.is_nan()); }
67                else { approx::assert_relative_eq!(s, b, epsilon = 1e-9); }
68            }
69        }
70    }
71}