Skip to main content

quantwave_core/indicators/incremental/
cmo.rs

1//! Native O(1) CMO — TA-Lib Wilder smoothing parity (`talib_rs::momentum::cmo`).
2
3use crate::traits::Next;
4
5#[inline]
6fn cmo_from_sums(sum_up: f64, sum_down: f64) -> f64 {
7    let total = sum_up + sum_down;
8    if total > 0.0 {
9        100.0 * (sum_up - sum_down) / total
10    } else {
11        0.0
12    }
13}
14
15/// Chande Momentum Oscillator — matches `talib_rs::momentum::cmo`.
16#[derive(Debug, Clone)]
17#[allow(non_camel_case_types)]
18pub struct CMO {
19    pub timeperiod: usize,
20    period_f: f64,
21    prev_close: Option<f64>,
22    sum_up: f64,
23    sum_down: f64,
24    warmup_changes: usize,
25}
26
27impl CMO {
28    pub fn new(timeperiod: usize) -> Self {
29        Self {
30            timeperiod,
31            period_f: timeperiod as f64,
32            prev_close: None,
33            sum_up: 0.0,
34            sum_down: 0.0,
35            warmup_changes: 0,
36        }
37    }
38}
39
40impl Next<f64> for CMO {
41    type Output = f64;
42
43    fn next(&mut self, input: f64) -> Self::Output {
44        let period = self.timeperiod;
45        if period < 2 {
46            return f64::NAN;
47        }
48
49        let Some(prev) = self.prev_close else {
50            self.prev_close = Some(input);
51            return f64::NAN;
52        };
53        self.prev_close = Some(input);
54
55        let change = input - prev;
56        let (cur_up, cur_down) = if change > 0.0 {
57            (change, 0.0)
58        } else {
59            (0.0, -change)
60        };
61
62        if self.warmup_changes < period {
63            self.warmup_changes += 1;
64            self.sum_up += cur_up;
65            self.sum_down += cur_down;
66            if self.warmup_changes < period {
67                return f64::NAN;
68            }
69            return cmo_from_sums(self.sum_up, self.sum_down);
70        }
71
72        self.sum_up = self.sum_up - (self.sum_up / self.period_f) + cur_up;
73        self.sum_down = self.sum_down - (self.sum_down / self.period_f) + cur_down;
74        cmo_from_sums(self.sum_up, self.sum_down)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use proptest::prelude::*;
82
83    proptest! {
84        #[test]
85        fn test_cmo_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
86            let period = 14;
87            let mut cmo = CMO::new(period);
88            let streaming: Vec<f64> = input.iter().map(|&x| cmo.next(x)).collect();
89            let batch = talib_rs::momentum::cmo(&input, period)
90                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
91            for (s, b) in streaming.iter().zip(batch.iter()) {
92                if s.is_nan() {
93                    assert!(b.is_nan());
94                } else {
95                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
96                }
97            }
98        }
99    }
100}