Skip to main content

quantwave_core/indicators/incremental/
mom.rs

1//! Native O(1) streaming momentum indicators (TA-Lib parity).
2
3use crate::indicators::incremental::utils::RingBuffer;
4use crate::traits::Next;
5
6macro_rules! impl_lookback_momentum {
7    ($name:ident, $compute:ident) => {
8        #[derive(Debug, Clone)]
9        #[allow(non_camel_case_types)]
10        pub struct $name {
11            pub timeperiod: usize,
12            history: RingBuffer<f64>,
13            bar_count: usize,
14        }
15
16        impl $name {
17            pub fn new(timeperiod: usize) -> Self {
18                let cap = timeperiod.saturating_add(1).max(1);
19                Self {
20                    timeperiod,
21                    history: RingBuffer::with_capacity(cap),
22                    bar_count: 0,
23                }
24            }
25
26            #[inline]
27            fn lagged(&self) -> Option<f64> {
28                let n = self.bar_count;
29                if n <= self.timeperiod {
30                    return None;
31                }
32                let idx = n - 1 - self.timeperiod;
33                self.history.get(idx).copied()
34            }
35        }
36
37        impl Next<f64> for $name {
38            type Output = f64;
39
40            fn next(&mut self, input: f64) -> Self::Output {
41                self.history.push_back(input);
42                self.bar_count += 1;
43
44                let Some(prev) = self.lagged() else {
45                    return f64::NAN;
46                };
47                $compute(input, prev)
48            }
49        }
50    };
51}
52
53#[inline]
54fn mom_compute(cur: f64, prev: f64) -> f64 {
55    cur - prev
56}
57
58#[inline]
59fn roc_compute(cur: f64, prev: f64) -> f64 {
60    if prev != 0.0 {
61        ((cur - prev) / prev) * 100.0
62    } else {
63        0.0
64    }
65}
66
67#[inline]
68fn rocp_compute(cur: f64, prev: f64) -> f64 {
69    if prev != 0.0 {
70        (cur - prev) / prev
71    } else {
72        0.0
73    }
74}
75
76#[inline]
77fn rocr_compute(cur: f64, prev: f64) -> f64 {
78    if prev != 0.0 { cur / prev } else { 0.0 }
79}
80
81#[inline]
82fn rocr100_compute(cur: f64, prev: f64) -> f64 {
83    if prev != 0.0 {
84        (cur / prev) * 100.0
85    } else {
86        0.0
87    }
88}
89
90impl_lookback_momentum!(MOM, mom_compute);
91impl_lookback_momentum!(ROC, roc_compute);
92impl_lookback_momentum!(ROCP, rocp_compute);
93impl_lookback_momentum!(ROCR, rocr_compute);
94impl_lookback_momentum!(ROCR100, rocr100_compute);
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use proptest::prelude::*;
100
101    fn assert_momentum_parity<I, F>(mut indicator: I, input: &[f64], batch: F)
102    where
103        I: Next<f64, Output = f64>,
104        F: Fn(&[f64], usize) -> Result<Vec<f64>, talib_rs::error::TaError>,
105    {
106        let timeperiod = 14;
107        let streaming: Vec<f64> = input.iter().map(|&x| indicator.next(x)).collect();
108        let batch = batch(input, timeperiod).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
109        for (s, b) in streaming.iter().zip(batch.iter()) {
110            if s.is_nan() {
111                assert!(b.is_nan());
112            } else if !b.is_nan() {
113                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
114            }
115        }
116    }
117
118    proptest! {
119        #[test]
120        fn mom_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
121            let period = 14;
122            assert_momentum_parity(MOM::new(period), &input, talib_rs::momentum::mom);
123        }
124
125        #[test]
126        fn roc_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
127            let period = 14;
128            assert_momentum_parity(ROC::new(period), &input, talib_rs::momentum::roc);
129        }
130
131        #[test]
132        fn rocp_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
133            let period = 14;
134            assert_momentum_parity(ROCP::new(period), &input, talib_rs::momentum::rocp);
135        }
136
137        #[test]
138        fn rocr_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
139            let period = 14;
140            assert_momentum_parity(ROCR::new(period), &input, talib_rs::momentum::rocr);
141        }
142
143        #[test]
144        fn rocr100_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
145            let period = 14;
146            assert_momentum_parity(ROCR100::new(period), &input, talib_rs::momentum::rocr100);
147        }
148    }
149}