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 {
79        cur / prev
80    } else {
81        0.0
82    }
83}
84
85#[inline]
86fn rocr100_compute(cur: f64, prev: f64) -> f64 {
87    if prev != 0.0 {
88        (cur / prev) * 100.0
89    } else {
90        0.0
91    }
92}
93
94impl_lookback_momentum!(MOM, mom_compute);
95impl_lookback_momentum!(ROC, roc_compute);
96impl_lookback_momentum!(ROCP, rocp_compute);
97impl_lookback_momentum!(ROCR, rocr_compute);
98impl_lookback_momentum!(ROCR100, rocr100_compute);
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use proptest::prelude::*;
104
105    fn assert_momentum_parity<I, F>(mut indicator: I, input: &[f64], batch: F)
106    where
107        I: Next<f64, Output = f64>,
108        F: Fn(&[f64], usize) -> Result<Vec<f64>, talib_rs::error::TaError>,
109    {
110        let timeperiod = 14;
111        let streaming: Vec<f64> = input.iter().map(|&x| indicator.next(x)).collect();
112        let batch = batch(input, timeperiod).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
113        for (s, b) in streaming.iter().zip(batch.iter()) {
114            if s.is_nan() {
115                assert!(b.is_nan());
116            } else if !b.is_nan() {
117                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
118            }
119        }
120    }
121
122    proptest! {
123        #[test]
124        fn mom_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
125            let period = 14;
126            assert_momentum_parity(MOM::new(period), &input, talib_rs::momentum::mom);
127        }
128
129        #[test]
130        fn roc_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
131            let period = 14;
132            assert_momentum_parity(ROC::new(period), &input, talib_rs::momentum::roc);
133        }
134
135        #[test]
136        fn rocp_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
137            let period = 14;
138            assert_momentum_parity(ROCP::new(period), &input, talib_rs::momentum::rocp);
139        }
140
141        #[test]
142        fn rocr_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
143            let period = 14;
144            assert_momentum_parity(ROCR::new(period), &input, talib_rs::momentum::rocr);
145        }
146
147        #[test]
148        fn rocr100_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
149            let period = 14;
150            assert_momentum_parity(ROCR100::new(period), &input, talib_rs::momentum::rocr100);
151        }
152    }
153}