Skip to main content

egui_charts/studies/builtin/
woodies_cci.rs

1//! Woodies CCI, after Ken Wood's CCI trading method.
2//!
3//! Two Commodity Channel Index lines of the close, a fast "turbo" CCI and the trading CCI, with the
4//! trading CCI also drawn as columns coloured by how long it has held one side of zero.
5//!
6//! # Formula
7//!
8//! ```text
9//! CCI(n) = (close - SMA(close, n)) / (0.015 * mean absolute deviation of close over n)
10//! ```
11//!
12//! A window with no deviation gives 0. Lines, in order: the CCI 14 columns, the CCI Turbo (6) and
13//! the CCI 14 line. Levels at 100, 0 and -100.
14//!
15//! # Column stages
16//!
17//! A column's stage follows its run: the bar itself and the unbroken CCI 14 values just before it
18//! on the same side of zero. Bars without a value are not part of the series, and a value of
19//! exactly zero is on neither side, so it has no run and ends the run before it.
20//!
21//! | Run | Stage | Colour |
22//! |-----|-------|--------|
23//! | 6 or more | established | columns colour above zero, established down colour below |
24//! | 2 to 5 | transition | transition colour |
25//! | 1, or the value is zero | neutral | neutral colour |
26//!
27//! The columns colour is the first plot colour of the settings; the other three are study inputs.
28//!
29//! # Example
30//! ```
31//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
32//! use egui_charts::studies::{Indicator, WoodiesCCI};
33//!
34//! let mut woodies = WoodiesCCI::default();
35//! woodies.calculate(&bars);
36//! assert_eq!(woodies.values().value(0, 30), woodies.values().value(2, 30));
37//! assert!(woodies.values().value(1, 5).is_some());
38//! ```
39
40use 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/// Woodies CCI: CCI 14 columns, CCI Turbo and CCI 14 lines in a separate pane.
48#[derive(Clone)]
49pub struct WoodiesCCI {
50    turbo_length: usize,
51    cci_length: usize,
52    values: IndicatorValues,
53    /// Colors: [columns, CCI Turbo, CCI 14]; the columns colour is the established run above zero.
54    colors: Vec<Color32>,
55    /// Column colour of an established run below zero.
56    down_color: Color32,
57    /// Column colour of a run of two to five values.
58    transition_color: Color32,
59    transition_negative: Color32,
60    /// Column colour of a first value on a side of zero, or of zero.
61    neutral_color: Color32,
62    neutral_negative: Color32,
63    visible: bool,
64}
65
66impl WoodiesCCI {
67    /// Create the study with the turbo and trading CCI lengths (6 and 14 by convention).
68    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    /// CCI of the close at bar `i`, or NaN before its window is full.
84    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        // Correct the rounding of the sum, so equal closes have no deviation.
92        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    /// The values over `data`, every bar of which has a finite close.
102    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        // Bars without a value, skipped as non-finite, are not in the run.
165        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    /// A CCI of length 2 has the sign of the close change, and zero for equal closes, so the closes
276    /// set the runs. A run of one or a zero is neutral, two to five values are a transition and six
277    /// or more are established, above zero in the columns colour and below it in the down colour. A
278    /// non-finite bar is not part of the run, and a zero ends it.
279    #[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        // The columns colour of the plot and the three stage inputs replace the defaults.
347        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}