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