Skip to main content

mantis_ta/indicators/momentum/
cci.rs

1use crate::indicators::Indicator;
2use crate::types::Candle;
3use crate::utils::ringbuf::RingBuf;
4
5/// Commodity Channel Index measuring deviation from typical price.
6///
7/// CCI = (Typical Price - SMA of Typical Price) / (0.015 * Mean Deviation)
8/// where Typical Price = (High + Low + Close) / 3
9///
10/// # Examples
11/// ```rust
12/// use mantis_ta::indicators::{Indicator, CCI};
13/// use mantis_ta::types::Candle;
14///
15/// let candles: Vec<Candle> = [
16///     (100.0, 102.0, 99.0),
17///     (101.0, 103.0, 100.0),
18///     (102.0, 104.0, 101.0),
19///     (103.0, 105.0, 102.0),
20/// ]
21/// .iter()
22/// .enumerate()
23/// .map(|(i, (c, h, l))| Candle {
24///     timestamp: i as i64,
25///     open: *c,
26///     high: *h,
27///     low: *l,
28///     close: *c,
29///     volume: 0.0,
30/// })
31/// .collect();
32///
33/// let values = CCI::new(3).calculate(&candles);
34/// assert_eq!(values[0], None);
35/// assert_eq!(values[1], None);
36/// assert!(values[2].is_some());
37/// assert!(values[3].is_some());
38/// ```
39#[derive(Debug, Clone)]
40pub struct CCI {
41    period: usize,
42    tp_window: RingBuf<f64>,
43}
44
45impl CCI {
46    pub fn new(period: usize) -> Self {
47        assert!(period > 0, "period must be > 0");
48        Self {
49            period,
50            tp_window: RingBuf::new(period, 0.0),
51        }
52    }
53
54    #[inline]
55    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
56        let typical_price = (high + low + close) / 3.0;
57        self.tp_window.push(typical_price);
58
59        if self.tp_window.len() < self.period {
60            return None;
61        }
62
63        let sma_tp = self.tp_window.iter().sum::<f64>() / self.period as f64;
64
65        let mean_deviation = self
66            .tp_window
67            .iter()
68            .map(|v| (v - sma_tp).abs())
69            .sum::<f64>()
70            / self.period as f64;
71
72        if mean_deviation.abs() < 1e-10 {
73            None
74        } else {
75            Some((typical_price - sma_tp) / (0.015 * mean_deviation))
76        }
77    }
78}
79
80impl Indicator for CCI {
81    type Output = f64;
82
83    fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
84        self.update(candle.high, candle.low, candle.close)
85    }
86
87    fn reset(&mut self) {
88        self.tp_window = RingBuf::new(self.period, 0.0);
89    }
90
91    fn warmup_period(&self) -> usize {
92        self.period
93    }
94
95    fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
96        Box::new(self.clone())
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn computes_cci_after_warmup() {
106        let mut cci = CCI::new(3);
107        let candles = vec![
108            Candle {
109                timestamp: 0,
110                open: 100.0,
111                high: 102.0,
112                low: 99.0,
113                close: 100.0,
114                volume: 0.0,
115            },
116            Candle {
117                timestamp: 1,
118                open: 101.0,
119                high: 103.0,
120                low: 100.0,
121                close: 101.0,
122                volume: 0.0,
123            },
124            Candle {
125                timestamp: 2,
126                open: 102.0,
127                high: 104.0,
128                low: 101.0,
129                close: 102.0,
130                volume: 0.0,
131            },
132            Candle {
133                timestamp: 3,
134                open: 103.0,
135                high: 105.0,
136                low: 102.0,
137                close: 103.0,
138                volume: 0.0,
139            },
140        ];
141
142        let mut outputs = Vec::new();
143        for c in &candles {
144            outputs.push(cci.next(c));
145        }
146
147        assert_eq!(outputs[0], None);
148        assert_eq!(outputs[1], None);
149        assert!(outputs[2].is_some());
150        assert!(outputs[3].is_some());
151    }
152
153    #[test]
154    fn cci_reset_clears_state() {
155        let mut cci = CCI::new(3);
156        let candles = vec![
157            Candle {
158                timestamp: 0,
159                open: 100.0,
160                high: 102.0,
161                low: 99.0,
162                close: 100.0,
163                volume: 0.0,
164            },
165            Candle {
166                timestamp: 1,
167                open: 101.0,
168                high: 103.0,
169                low: 100.0,
170                close: 101.0,
171                volume: 0.0,
172            },
173            Candle {
174                timestamp: 2,
175                open: 102.0,
176                high: 104.0,
177                low: 101.0,
178                close: 102.0,
179                volume: 0.0,
180            },
181        ];
182
183        for c in &candles {
184            cci.next(c);
185        }
186        assert!(cci.next(&candles[0]).is_some());
187
188        cci.reset();
189        assert_eq!(cci.next(&candles[0]), None);
190    }
191
192    #[test]
193    fn cci_with_flat_market() {
194        let mut cci = CCI::new(3);
195        let candle = Candle {
196            timestamp: 0,
197            open: 100.0,
198            high: 100.0,
199            low: 100.0,
200            close: 100.0,
201            volume: 0.0,
202        };
203
204        cci.next(&candle);
205        cci.next(&candle);
206        assert_eq!(cci.next(&candle), None);
207    }
208
209    #[test]
210    fn cci_warmup_period() {
211        let cci = CCI::new(5);
212        assert_eq!(cci.warmup_period(), 5);
213    }
214}