mantis_ta/indicators/momentum/
cci.rs1use crate::indicators::Indicator;
2use crate::types::Candle;
3use crate::utils::ringbuf::RingBuf;
4
5#[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}