egui_charts/studies/builtin/
woodies_cci.rs1use super::over_finite_bars;
41use crate::model::Bar;
42use crate::studies::palette;
43use crate::studies::stepper::{update_window, usable_behind};
44use crate::studies::{Indicator, IndicatorValues};
45use egui::Color32;
46
47#[derive(Clone)]
49pub struct WoodiesCCI {
50 turbo_length: usize,
51 cci_length: usize,
52 values: IndicatorValues,
53 colors: Vec<Color32>,
55 down_color: Color32,
57 transition_color: Color32,
59 transition_negative: Color32,
60 neutral_color: Color32,
62 neutral_negative: Color32,
63 visible: bool,
64}
65
66impl WoodiesCCI {
67 pub fn new(turbo_length: usize, cci_length: usize) -> Self {
69 Self {
70 turbo_length: turbo_length.max(1),
71 cci_length: cci_length.max(1),
72 values: IndicatorValues::new(),
73 colors: vec![palette::DEEP_TEAL, palette::DEEP_TEAL, palette::BRIGHT_RED],
74 down_color: palette::BRIGHT_RED,
75 transition_color: palette::BRIGHT_RED,
76 transition_negative: palette::DEEP_TEAL,
77 neutral_color: palette::BRIGHT_RED,
78 neutral_negative: palette::DEEP_TEAL,
79 visible: true,
80 }
81 }
82
83 fn cci(closes: &[f64], i: usize, length: usize) -> f64 {
85 let Some(start) = (i + 1).checked_sub(length) else {
86 return f64::NAN;
87 };
88 let window = &closes[start..=i];
89 let n = length as f64;
90 let mean = window.iter().sum::<f64>() / n;
91 let mean = mean + window.iter().map(|c| c - mean).sum::<f64>() / n;
93 let deviation = window.iter().map(|c| (c - mean).abs()).sum::<f64>() / n;
94 if deviation == 0.0 {
95 0.0
96 } else {
97 (closes[i] - mean) / (0.015 * deviation)
98 }
99 }
100
101 fn compute(&self, data: &[Bar]) -> IndicatorValues {
103 let closes: Vec<f64> = data.iter().map(|b| b.close).collect();
104 let mut values = IndicatorValues::new();
105 for i in 0..closes.len() {
106 let cci = Self::cci(&closes, i, self.cci_length);
107 values.push_lines(&[cci, Self::cci(&closes, i, self.turbo_length), cci]);
108 }
109 values
110 }
111}
112
113impl Default for WoodiesCCI {
114 fn default() -> Self {
115 Self::new(6, 14)
116 }
117}
118
119impl Indicator for WoodiesCCI {
120 fn level_style(&self, value: f64) -> crate::model::LineStyle {
121 if value == 0.0 {
122 crate::model::LineStyle::Solid
123 } else {
124 crate::model::LineStyle::Dotted
125 }
126 }
127
128 fn source(&self) -> Option<crate::studies::StudySource> {
129 Some(crate::studies::StudySource::Price(
130 crate::model::PriceSource::Close,
131 ))
132 }
133
134 crate::studies::inputs::study_inputs!(self;
135 turbo_length: length("CCI Turbo length", 1, usize::MAX),
136 cci_length: length("CCI 14 length", 1, usize::MAX),
137 down_color: color("Established down colour"),
138 transition_color: color("Transition colour"),
139 neutral_color: color("Neutral colour")
140 ; changed => {
141 if changed == "transition_color" { self.transition_negative = self.transition_color; }
142 if changed == "neutral_color" { self.neutral_negative = self.neutral_color; }
143 });
144
145 fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
146 crate::studies::PlotStyle {
147 color: self.colors.get(line).copied().unwrap_or(Color32::WHITE),
148 width: 1.0,
149 pattern: crate::model::LineStyle::Solid,
150 plot_type: if line == 0 {
151 crate::studies::PlotType::Histogram
152 } else {
153 crate::studies::PlotType::Line
154 },
155 visible: true,
156 }
157 }
158
159 fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
160 if line != 0 {
161 return None;
162 }
163 let value = self.values.value(0, bar)?;
164 let previous = (0..bar).rev().filter_map(|bar| self.values.value(0, bar));
166 let run = 1 + previous.take_while(|p| p * value > 0.0).take(5).count();
167 Some(match run {
168 1 if value < 0.0 => self.neutral_negative,
169 1 => self.neutral_color,
170 2..=5 if value < 0.0 => self.transition_negative,
171 2..=5 => self.transition_color,
172 _ if value > 0.0 => self.colors[0],
173 _ => self.down_color,
174 })
175 }
176
177 fn name(&self) -> &str {
178 "Woodies CCI"
179 }
180
181 fn desc(&self) -> &str {
182 "Woodies CCI - Turbo and trading CCI with a trend-coloured histogram"
183 }
184
185 fn calculate(&mut self, data: &[Bar]) {
186 self.update(data, 0);
187 }
188
189 fn update(&mut self, data: &[Bar], from: usize) {
190 let mut values = std::mem::take(&mut self.values);
191 update_window(
192 &mut values,
193 data,
194 from,
195 usable_behind(self.turbo_length.max(self.cci_length) - 1, |b| {
196 b.close.is_finite()
197 }),
198 |data| over_finite_bars(data, |b| b.close.is_finite(), |data| self.compute(data)),
199 );
200 self.values = values;
201 }
202
203 fn is_incremental(&self) -> bool {
204 true
205 }
206
207 fn values(&self) -> &IndicatorValues {
208 &self.values
209 }
210
211 fn colors(&self) -> Vec<Color32> {
212 self.colors
213 .iter()
214 .copied()
215 .chain([self.transition_negative, self.neutral_negative])
216 .collect()
217 }
218
219 fn set_colors(&mut self, colors: Vec<Color32>) {
220 if colors.len() >= 3 {
221 self.colors = colors[..3].to_vec();
222 self.transition_negative = colors.get(3).copied().unwrap_or(self.transition_color);
223 self.neutral_negative = colors.get(4).copied().unwrap_or(self.neutral_color);
224 }
225 }
226
227 fn pane(&self) -> crate::studies::StudyPane {
228 let gray = palette::GRAY;
229 crate::studies::StudyPane {
230 height: None,
231 range: None,
232 levels: vec![
233 (100.0, gray, "Hundred Line"),
234 (0.0, gray, "Zero Line"),
235 (-100.0, gray, "Minus Line"),
236 ],
237 fill_between_levels: false,
238 }
239 }
240
241 fn is_overlay(&self) -> bool {
242 false
243 }
244
245 fn is_visible(&self) -> bool {
246 self.visible
247 }
248
249 fn set_visible(&mut self, visible: bool) {
250 self.visible = visible;
251 }
252
253 fn clone_box(&self) -> Box<dyn Indicator> {
254 Box::new(self.clone())
255 }
256
257 fn line_count(&self) -> usize {
258 3
259 }
260
261 fn line_names(&self) -> Vec<String> {
262 vec![
263 format!("Histogram({})", self.cci_length),
264 format!("CCI Turbo({})", self.turbo_length),
265 format!("CCI({})", self.cci_length),
266 ]
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use chrono::{TimeZone, Utc};
274
275 #[test]
280 fn columns_are_staged_by_the_run_on_one_side_of_zero() {
281 let closes = [
282 0.0,
283 1.0,
284 2.0,
285 f64::NAN,
286 3.0,
287 4.0,
288 5.0,
289 6.0,
290 5.0,
291 4.0,
292 5.0,
293 4.0,
294 3.0,
295 2.0,
296 1.0,
297 0.0,
298 -1.0,
299 -1.0,
300 -2.0,
301 ];
302 let bars: Vec<Bar> = closes
303 .iter()
304 .enumerate()
305 .map(|(i, &c)| {
306 Bar::new(
307 Utc.timestamp_opt(i as i64 * 60, 0).unwrap(),
308 c,
309 c,
310 c,
311 c,
312 1.0,
313 )
314 })
315 .collect();
316 let mut study = WoodiesCCI::new(2, 2);
317 study.calculate(&bars);
318 let (g, r) = (study.colors[0], study.down_color);
319 let expected = [
320 None,
321 Some(r),
322 Some(r),
323 None,
324 Some(r),
325 Some(r),
326 Some(r),
327 Some(g),
328 Some(g),
329 Some(g),
330 Some(r),
331 Some(g),
332 Some(g),
333 Some(g),
334 Some(g),
335 Some(g),
336 Some(r),
337 Some(r),
338 Some(g),
339 ];
340 let colours = |study: &WoodiesCCI| -> Vec<_> {
341 (0..bars.len())
342 .map(|bar| study.plot_color(0, bar))
343 .collect()
344 };
345 assert_eq!(colours(&study), expected);
346 study.set_color(0, Color32::BLUE);
348 for id in ["down_color", "transition_color", "neutral_color"] {
349 study
350 .set_input(id, crate::studies::InputValue::Color(Color32::GOLD))
351 .unwrap();
352 }
353 study.calculate(&bars);
354 let mut recoloured = expected.map(|c| c.map(|_| Color32::GOLD));
355 recoloured[7] = Some(Color32::BLUE);
356 assert_eq!(colours(&study), recoloured);
357 }
358}