Skip to main content

quantwave_core/indicators/incremental/
cci.rs

1//! Native streaming CCI — TA-Lib parity (`talib_rs::momentum::cci`).
2//!
3//! O(1) sliding sum for typical price SMA; O(period) mean deviation per bar (same as talib-rs).
4
5use crate::traits::Next;
6
7#[inline]
8fn cci_value(tp: f64, average: f64, circ_buf: &[f64], timeperiod: usize) -> f64 {
9    let tp_f = timeperiod as f64;
10    let mut mean_dev_sum = 0.0_f64;
11    for j in 0..timeperiod {
12        mean_dev_sum += (circ_buf[j] - average).abs();
13    }
14    let mean_dev = mean_dev_sum / tp_f;
15    if mean_dev > 0.0 {
16        (tp - average) / (0.015 * mean_dev)
17    } else {
18        0.0
19    }
20}
21
22/// Commodity Channel Index — input `(high, low, close)`.
23#[derive(Debug, Clone)]
24#[allow(non_camel_case_types)]
25pub struct CCI {
26    pub timeperiod: usize,
27    circ_buf: Vec<f64>,
28    circ_idx: usize,
29    running_sum: f64,
30    bars_seen: usize,
31}
32
33impl CCI {
34    pub fn new(timeperiod: usize) -> Self {
35        Self {
36            timeperiod,
37            circ_buf: vec![0.0; timeperiod.max(1)],
38            circ_idx: 0,
39            running_sum: 0.0,
40            bars_seen: 0,
41        }
42    }
43}
44
45impl Next<(f64, f64, f64)> for CCI {
46    type Output = f64;
47
48    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
49        let timeperiod = self.timeperiod;
50        if timeperiod < 2 {
51            return f64::NAN;
52        }
53        let lookback = timeperiod - 1;
54        let tp = (high + low + close) / 3.0;
55        self.bars_seen += 1;
56        let i = self.bars_seen - 1;
57
58        if i < timeperiod {
59            self.circ_buf[i] = tp;
60            self.running_sum += tp;
61            if i < lookback {
62                return f64::NAN;
63            }
64            let last_value = self.circ_buf[lookback];
65            let the_average = self.running_sum / timeperiod as f64;
66            return cci_value(last_value, the_average, &self.circ_buf[..timeperiod], timeperiod);
67        }
68
69        let new_tp = tp;
70        self.running_sum += new_tp - self.circ_buf[self.circ_idx];
71        self.circ_buf[self.circ_idx] = new_tp;
72        let the_average = self.running_sum / timeperiod as f64;
73        let out = cci_value(new_tp, the_average, &self.circ_buf[..timeperiod], timeperiod);
74        self.circ_idx += 1;
75        if self.circ_idx >= timeperiod {
76            self.circ_idx = 0;
77        }
78        out
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use proptest::prelude::*;
86
87    fn ordered_hlc(
88        h: &[f64],
89        l: &[f64],
90        c: &[f64],
91    ) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
92        let len = h.len().min(l.len()).min(c.len());
93        let mut high = Vec::with_capacity(len);
94        let mut low = Vec::with_capacity(len);
95        let mut close = Vec::with_capacity(len);
96        for i in 0..len {
97            let vh = h[i];
98            let vl = l[i];
99            let vc = c[i];
100            high.push(vh.max(vl).max(vc));
101            low.push(vh.min(vl).min(vc));
102            close.push(vc);
103        }
104        (high, low, close)
105    }
106
107    proptest! {
108        #[test]
109        fn test_cci_parity(
110            h in prop::collection::vec(1.0..100.0, 10..100),
111            l in prop::collection::vec(1.0..100.0, 10..100),
112            c in prop::collection::vec(1.0..100.0, 10..100),
113        ) {
114            let (high, low, close) = ordered_hlc(&h, &l, &c);
115            let len = high.len();
116            if len == 0 { return Ok(()); }
117            let period = 14;
118            let mut cci = CCI::new(period);
119            let streaming: Vec<f64> =
120                (0..len).map(|i| cci.next((high[i], low[i], close[i]))).collect();
121            let batch = talib_rs::momentum::cci(&high, &low, &close, period)
122                .unwrap_or_else(|_| vec![f64::NAN; len]);
123            for (s, b) in streaming.iter().zip(batch.iter()) {
124                if s.is_nan() {
125                    assert!(b.is_nan());
126                } else if !b.is_nan() {
127                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
128                }
129            }
130        }
131    }
132}