Skip to main content

finance_query/indicators/
cmo.rs

1//! Chande Momentum Oscillator (CMO) indicator.
2
3use super::{IndicatorError, Result};
4
5/// Calculate Chande Momentum Oscillator (CMO).
6///
7/// Similar to RSI but uses sum of gains - sum of losses.
8/// Returns value between -100 and 100.
9///
10/// # Arguments
11///
12/// * `data` - Price data (typically close prices)
13/// * `period` - Number of periods
14///
15/// # Example
16///
17/// ```
18/// use finance_query::indicators::cmo;
19///
20/// let prices = vec![10.0, 11.0, 12.0, 11.0, 10.0];
21/// let result = cmo(&prices, 3).unwrap();
22/// ```
23pub fn cmo(data: &[f64], period: usize) -> Result<Vec<Option<f64>>> {
24    if period == 0 {
25        return Err(IndicatorError::InvalidPeriod(
26            "Period must be greater than 0".to_string(),
27        ));
28    }
29    if data.len() < period + 1 {
30        return Err(IndicatorError::InsufficientData {
31            need: period + 1,
32            got: data.len(),
33        });
34    }
35
36    let mut result = vec![None; data.len()];
37
38    // Pre-calculate changes
39    let mut changes = Vec::with_capacity(data.len());
40    changes.push(0.0); // Dummy for index 0
41    for i in 1..data.len() {
42        changes.push(data[i] - data[i - 1]);
43    }
44
45    // Calculate initial window
46    let mut gains_sum = 0.0;
47    let mut losses_sum = 0.0;
48
49    for &change in changes.iter().skip(1).take(period) {
50        if change > 0.0 {
51            gains_sum += change;
52        } else {
53            losses_sum += -change;
54        }
55    }
56
57    // First value at index period
58    let total = gains_sum + losses_sum;
59    if total != 0.0 {
60        result[period] = Some(((gains_sum - losses_sum) / total) * 100.0);
61    } else {
62        result[period] = Some(0.0);
63    }
64
65    // Slide window
66    for i in (period + 1)..data.len() {
67        // Remove old change (at index i - period)
68        let old_change = changes[i - period];
69        if old_change > 0.0 {
70            gains_sum -= old_change;
71        } else {
72            losses_sum -= -old_change;
73        }
74
75        // Add new change (at index i)
76        let new_change = changes[i];
77        if new_change > 0.0 {
78            gains_sum += new_change;
79        } else {
80            losses_sum += -new_change;
81        }
82
83        let total = gains_sum + losses_sum;
84        if total != 0.0 {
85            result[i] = Some(((gains_sum - losses_sum) / total) * 100.0);
86        } else {
87            result[i] = Some(0.0);
88        }
89    }
90
91    Ok(result)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_cmo() {
100        let prices = vec![10.0, 11.0, 12.0, 11.0, 10.0];
101        let result = cmo(&prices, 3).unwrap();
102
103        assert_eq!(result.len(), 5);
104        assert!(result[0].is_none());
105        assert!(result[1].is_none());
106        assert!(result[2].is_none());
107
108        // i=3: changes: 1, 1, -1. gains=2, losses=1. total=3. cmo=(1/3)*100 = 33.33
109        assert!(result[3].is_some());
110        let val = result[3].unwrap();
111        assert!((val - 33.333).abs() < 0.01);
112    }
113}