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(
67                last_value,
68                the_average,
69                &self.circ_buf[..timeperiod],
70                timeperiod,
71            );
72        }
73
74        let new_tp = tp;
75        self.running_sum += new_tp - self.circ_buf[self.circ_idx];
76        self.circ_buf[self.circ_idx] = new_tp;
77        let the_average = self.running_sum / timeperiod as f64;
78        let out = cci_value(
79            new_tp,
80            the_average,
81            &self.circ_buf[..timeperiod],
82            timeperiod,
83        );
84        self.circ_idx += 1;
85        if self.circ_idx >= timeperiod {
86            self.circ_idx = 0;
87        }
88        out
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use proptest::prelude::*;
96
97    fn ordered_hlc(h: &[f64], l: &[f64], c: &[f64]) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
98        let len = h.len().min(l.len()).min(c.len());
99        let mut high = Vec::with_capacity(len);
100        let mut low = Vec::with_capacity(len);
101        let mut close = Vec::with_capacity(len);
102        for i in 0..len {
103            let vh = h[i];
104            let vl = l[i];
105            let vc = c[i];
106            high.push(vh.max(vl).max(vc));
107            low.push(vh.min(vl).min(vc));
108            close.push(vc);
109        }
110        (high, low, close)
111    }
112
113    proptest! {
114        #[test]
115        fn test_cci_parity(
116            h in prop::collection::vec(1.0..100.0, 10..100),
117            l in prop::collection::vec(1.0..100.0, 10..100),
118            c in prop::collection::vec(1.0..100.0, 10..100),
119        ) {
120            let (high, low, close) = ordered_hlc(&h, &l, &c);
121            let len = high.len();
122            if len == 0 { return Ok(()); }
123            let period = 14;
124            let mut cci = CCI::new(period);
125            let streaming: Vec<f64> =
126                (0..len).map(|i| cci.next((high[i], low[i], close[i]))).collect();
127            let batch = talib_rs::momentum::cci(&high, &low, &close, period)
128                .unwrap_or_else(|_| vec![f64::NAN; len]);
129            for (s, b) in streaming.iter().zip(batch.iter()) {
130                if s.is_nan() {
131                    assert!(b.is_nan());
132                } else if !b.is_nan() {
133                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
134                }
135            }
136        }
137    }
138}