kestrel_chartkit/indicator/
cci.rs1use std::collections::{HashMap, VecDeque};
2
3use crate::model::Bar;
4
5use super::divergence::SlopeDivergence;
6use super::{Indicator, IndicatorAlert, IndicatorOutput};
7
8pub struct Cci {
18 cci_len: usize,
19 avg_len: usize,
20 sig_len: usize,
21 lookback_extreme: usize,
22 oversold: f64,
23 overbought: f64,
24 require_extreme_zone: bool,
25 ctx_len: usize,
26
27 source_window: VecDeque<f64>,
28 cci_ema: Option<f64>,
29 signal_ema: Option<f64>,
30 cci_line_window: VecDeque<f64>,
31 prev_cci_line: Option<f64>,
32 prev_signal: Option<f64>,
33
34 ctx_window: VecDeque<f64>,
35 ctx_ema: Option<f64>,
36 divergence: SlopeDivergence,
37
38 alerts: CciAlerts,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Default)]
42pub struct CciAlerts {
43 pub bull_extreme: bool,
44 pub bear_extreme: bool,
45 pub bull_zero_cross: bool,
46 pub bear_zero_cross: bool,
47 pub bull_divergence: bool,
48 pub bear_divergence: bool,
49 pub extreme_strength: f64,
50 pub divergence_strength: f64,
51}
52
53impl Cci {
54 #[allow(clippy::too_many_arguments)]
55 pub fn new(
56 cci_len: usize,
57 avg_len: usize,
58 sig_len: usize,
59 lookback_extreme: usize,
60 oversold: f64,
61 overbought: f64,
62 require_extreme_zone: bool,
63 ctx_len: usize,
64 div_len: usize,
65 div_min: f64,
66 ) -> Self {
67 Self {
68 cci_len,
69 avg_len,
70 sig_len,
71 lookback_extreme,
72 oversold,
73 overbought,
74 require_extreme_zone,
75 ctx_len,
76 source_window: VecDeque::with_capacity(cci_len),
77 cci_ema: None,
78 signal_ema: None,
79 cci_line_window: VecDeque::with_capacity(lookback_extreme),
80 prev_cci_line: None,
81 prev_signal: None,
82 ctx_window: VecDeque::with_capacity(ctx_len),
83 ctx_ema: None,
84 divergence: SlopeDivergence::new(div_len, div_min),
85 alerts: CciAlerts::default(),
86 }
87 }
88
89 pub fn with_defaults() -> Self {
90 Self::new(20, 3, 3, 5, -100.0, 100.0, true, 100, 4, 25.0)
91 }
92
93 fn ema_step(state: &mut Option<f64>, src: f64, len: usize) -> f64 {
94 let alpha = 2.0 / (len as f64 + 1.0);
95 let next = match *state {
96 None => src,
97 Some(prev) => alpha * src + (1.0 - alpha) * prev,
98 };
99 *state = Some(next);
100 next
101 }
102}
103
104impl Indicator for Cci {
105 fn name(&self) -> &str {
106 "cci"
107 }
108
109 fn warmup_period(&self) -> usize {
110 self.cci_len.max(self.ctx_len)
111 }
112
113 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
114 self.alerts = CciAlerts::default();
115
116 let source = bar.typical_price();
117
118 if self.ctx_window.len() == self.ctx_len {
119 self.ctx_window.pop_front();
120 }
121 self.ctx_window.push_back(source);
122 let ctx_line = if self.ctx_window.len() == self.ctx_len {
123 let ctx_sma: f64 = self.ctx_window.iter().sum::<f64>() / self.ctx_len as f64;
124 let ctx_mean_dev: f64 = self
125 .ctx_window
126 .iter()
127 .map(|v| (v - ctx_sma).abs())
128 .sum::<f64>()
129 / self.ctx_len as f64;
130 let ctx_raw = if ctx_mean_dev != 0.0 {
131 (source - ctx_sma) / (0.015 * ctx_mean_dev)
132 } else {
133 0.0
134 };
135 Some(Self::ema_step(&mut self.ctx_ema, ctx_raw, self.avg_len))
136 } else {
137 None
138 };
139
140 if self.source_window.len() == self.cci_len {
141 self.source_window.pop_front();
142 }
143 self.source_window.push_back(source);
144 if self.source_window.len() < self.cci_len {
145 return None;
146 }
147
148 let sma: f64 = self.source_window.iter().sum::<f64>() / self.cci_len as f64;
149 let mean_dev: f64 = self
150 .source_window
151 .iter()
152 .map(|v| (v - sma).abs())
153 .sum::<f64>()
154 / self.cci_len as f64;
155 let raw_cci = if mean_dev != 0.0 {
156 (source - sma) / (0.015 * mean_dev)
157 } else {
158 0.0
159 };
160
161 let cci_line = Self::ema_step(&mut self.cci_ema, raw_cci, self.avg_len);
162 let signal = Self::ema_step(&mut self.signal_ema, cci_line, self.sig_len);
163
164 if self.cci_line_window.len() == self.lookback_extreme {
165 self.cci_line_window.pop_front();
166 }
167 self.cci_line_window.push_back(cci_line);
168 let was_oversold = self.cci_line_window.len() == self.lookback_extreme
169 && self
170 .cci_line_window
171 .iter()
172 .cloned()
173 .fold(f64::INFINITY, f64::min)
174 <= self.oversold;
175 let was_overbought = self.cci_line_window.len() == self.lookback_extreme
176 && self
177 .cci_line_window
178 .iter()
179 .cloned()
180 .fold(f64::NEG_INFINITY, f64::max)
181 >= self.overbought;
182
183 if let (Some(prev_cci), Some(prev_sig)) = (self.prev_cci_line, self.prev_signal) {
184 let bull_cross = prev_cci <= prev_sig && cci_line > signal;
185 let bear_cross = prev_cci >= prev_sig && cci_line < signal;
186 self.alerts.bull_extreme = bull_cross && (!self.require_extreme_zone || was_oversold);
187 self.alerts.bear_extreme = bear_cross && (!self.require_extreme_zone || was_overbought);
188 self.alerts.bull_zero_cross = prev_cci <= 0.0 && cci_line > 0.0;
189 self.alerts.bear_zero_cross = prev_cci >= 0.0 && cci_line < 0.0;
190
191 let lowest = self
192 .cci_line_window
193 .iter()
194 .cloned()
195 .fold(f64::INFINITY, f64::min);
196 let highest = self
197 .cci_line_window
198 .iter()
199 .cloned()
200 .fold(f64::NEG_INFINITY, f64::max);
201 self.alerts.extreme_strength = if self.alerts.bull_extreme {
202 ((self.oversold - lowest) / self.oversold.abs()).clamp(0.0, 1.0)
203 } else if self.alerts.bear_extreme {
204 ((highest - self.overbought) / self.overbought.abs()).clamp(0.0, 1.0)
205 } else {
206 0.0
207 };
208 }
209 self.prev_cci_line = Some(cci_line);
210 self.prev_signal = Some(signal);
211
212 let mut extra = HashMap::new();
213 extra.insert("signal".to_string(), signal);
214 if let Some(ctx_line) = ctx_line {
215 let div = self.divergence.update(cci_line, ctx_line);
216 self.alerts.bull_divergence = div.bull;
217 self.alerts.bear_divergence = div.bear;
218 self.alerts.divergence_strength = if div.bull || div.bear {
219 ((div.fast_dir.abs() - self.divergence.div_min()) / self.divergence.div_min())
220 .clamp(0.0, 1.0)
221 } else {
222 0.0
223 };
224 extra.insert("ctx".to_string(), ctx_line);
225 }
226
227 Some(IndicatorOutput::with_extra(cci_line, extra))
228 }
229
230 fn reset(&mut self) {
231 self.source_window.clear();
232 self.cci_ema = None;
233 self.signal_ema = None;
234 self.cci_line_window.clear();
235 self.prev_cci_line = None;
236 self.prev_signal = None;
237 self.ctx_window.clear();
238 self.ctx_ema = None;
239 self.divergence.reset();
240 self.alerts = CciAlerts::default();
241 }
242
243 fn alerts(&self) -> Vec<IndicatorAlert> {
244 let a = self.alerts;
245 let mut out = Vec::new();
246 if a.bull_extreme {
247 out.push(IndicatorAlert {
248 kind: "bull_extreme".to_string(),
249 note: "CCI · BULL CROSS OVERSOLD".to_string(),
250 strength: a.extreme_strength,
251 });
252 }
253 if a.bear_extreme {
254 out.push(IndicatorAlert {
255 kind: "bear_extreme".to_string(),
256 note: "CCI · BEAR CROSS OVERBOUGHT".to_string(),
257 strength: a.extreme_strength,
258 });
259 }
260 if a.bull_zero_cross {
261 out.push(IndicatorAlert {
262 kind: "bull_zero_cross".to_string(),
263 note: "CCI · CROSS ABOVE ZERO".to_string(),
264 strength: 1.0,
265 });
266 }
267 if a.bear_zero_cross {
268 out.push(IndicatorAlert {
269 kind: "bear_zero_cross".to_string(),
270 note: "CCI · CROSS BELOW ZERO".to_string(),
271 strength: 1.0,
272 });
273 }
274 if a.bull_divergence {
275 out.push(IndicatorAlert {
276 kind: "bull_divergence".to_string(),
277 note: "CCI · BULL DIVERGENCE".to_string(),
278 strength: a.divergence_strength,
279 });
280 }
281 if a.bear_divergence {
282 out.push(IndicatorAlert {
283 kind: "bear_divergence".to_string(),
284 note: "CCI · BEAR DIVERGENCE".to_string(),
285 strength: a.divergence_strength,
286 });
287 }
288 out
289 }
290}