quantwave_core/indicators/incremental/
cmo.rs1use 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#[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}