egui_charts/studies/builtin/
cci.rs1use crate::model::Bar;
28use crate::studies::palette;
29use crate::studies::stepper::update_window;
30use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
31use egui::Color32;
32
33#[derive(Clone)]
35pub struct CCI {
36 period: usize,
37 values: IndicatorValues,
38 color: Color32,
39 visible: bool,
40}
41
42impl CCI {
43 pub fn new(period: usize) -> Self {
48 Self {
49 period: period.max(1),
50 values: IndicatorValues::new(),
51 color: palette::BLUE,
52 visible: true,
53 }
54 }
55
56 pub fn with_color(mut self, color: Color32) -> Self {
58 self.color = color;
59 self
60 }
61}
62
63impl Default for CCI {
65 fn default() -> Self {
66 Self::new(20)
67 }
68}
69
70impl CCI {
71 fn compute(&self, data: &[Bar]) -> IndicatorValues {
72 let mut values = IndicatorValues::new();
73
74 if data.len() < self.period {
75 for _ in 0..data.len() {
76 values.push(IndicatorValue::None);
77 }
78 return values;
79 }
80
81 let tp: Vec<f64> = data.iter().map(Bar::typical_price).collect();
83
84 for i in 0..data.len() {
85 if i + 1 < self.period {
86 values.push(IndicatorValue::None);
87 } else {
88 let start = i + 1 - self.period;
90 let sma: f64 = tp[start..=i].iter().sum::<f64>() / self.period as f64;
91 let sma =
93 sma + tp[start..=i].iter().map(|p| p - sma).sum::<f64>() / self.period as f64;
94
95 let mean_dev: f64 =
97 tp[start..=i].iter().map(|p| (p - sma).abs()).sum::<f64>() / self.period as f64;
98
99 if mean_dev == 0.0 {
101 values.push(IndicatorValue::Single(0.0));
102 } else {
103 let cci = (tp[i] - sma) / (0.015 * mean_dev);
105 values.push(IndicatorValue::Single(cci));
106 }
107 }
108 }
109 values
110 }
111}
112
113impl Indicator for CCI {
114 fn source(&self) -> Option<crate::studies::StudySource> {
115 Some(crate::studies::StudySource::Price(
116 crate::model::PriceSource::HLC3,
117 ))
118 }
119
120 crate::studies::inputs::study_inputs!(self;
121 period: length("Period", 1, usize::MAX)
122 ; {});
123
124 fn name(&self) -> &str {
125 "CCI"
126 }
127
128 fn desc(&self) -> &str {
129 "Commodity Channel Index - Cyclical trend indicator"
130 }
131
132 fn calculate(&mut self, data: &[Bar]) {
133 self.update(data, 0);
134 }
135
136 fn update(&mut self, data: &[Bar], from: usize) {
137 let mut values = std::mem::take(&mut self.values);
138 update_window(
139 &mut values,
140 data,
141 from,
142 |_data, from| from.saturating_sub(self.period - 1),
143 |data| self.compute(data),
144 );
145 self.values = values;
146 }
147
148 fn is_incremental(&self) -> bool {
149 true
150 }
151
152 fn values(&self) -> &IndicatorValues {
153 &self.values
154 }
155
156 fn colors(&self) -> Vec<Color32> {
157 vec![self.color]
158 }
159
160 fn set_colors(&mut self, colors: Vec<Color32>) {
161 if !colors.is_empty() {
162 self.color = colors[0];
163 }
164 }
165
166 fn is_overlay(&self) -> bool {
167 false }
169
170 fn is_visible(&self) -> bool {
171 self.visible
172 }
173
174 fn set_visible(&mut self, visible: bool) {
175 self.visible = visible;
176 }
177
178 fn clone_box(&self) -> Box<dyn Indicator> {
179 Box::new(self.clone())
180 }
181
182 fn line_names(&self) -> Vec<String> {
183 vec![format!("CCI({})", self.period)]
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use chrono::{Duration, Utc};
191
192 #[test]
193 fn test_cci_flat_prices() {
194 let start = Utc::now();
196 let mut bars: Vec<Bar> = (0..30)
197 .map(|i| Bar {
198 time: start + Duration::minutes(i * 5),
199 open: 100.0,
200 high: 100.0,
201 low: 100.0,
202 close: 100.0,
203 volume: 1000.0,
204 colors: None,
205 payload: None,
206 })
207 .collect();
208
209 for price in [100.0, 0.1, 1e-17] {
210 for bar in &mut bars {
211 bar.open = price;
212 bar.high = price;
213 bar.low = price;
214 bar.close = price;
215 }
216 let mut cci = CCI::new(20);
217 cci.calculate(&bars);
218 for value in cci.values().to_rows().iter().skip(19) {
219 assert!(
220 matches!(value, IndicatorValue::Single(v) if *v == 0.0),
221 "a window without deviation reads 0, got {value:?} at {price}"
222 );
223 }
224 }
225 }
226}