Skip to main content

egui_charts/studies/builtin/
choppiness.rs

1use super::over_finite_bars;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::{update_window, usable_behind};
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Choppiness Index
9/// Measures market trendiness vs choppiness
10/// Values close to 100 = choppy market, values close to 0 = trending market
11#[derive(Clone)]
12pub struct ChoppinessIndex {
13    /// Lookback period (typically 14)
14    period: usize,
15    values: IndicatorValues,
16    color: Color32,
17    visible: bool,
18}
19
20impl ChoppinessIndex {
21    /// Create with a lookback of `period` bars, clamped to at least 1.
22    pub fn new(period: usize) -> Self {
23        Self {
24            period: period.max(1),
25            values: IndicatorValues::new(),
26            color: palette::BLUE,
27            visible: true,
28        }
29    }
30
31    /// Set the plot color.
32    pub fn with_color(mut self, color: Color32) -> Self {
33        self.color = color;
34        self
35    }
36}
37
38/// Construct with the conventional default parameters.
39impl Default for ChoppinessIndex {
40    fn default() -> Self {
41        Self::new(14)
42    }
43}
44
45impl ChoppinessIndex {
46    /// The values over `data`, every bar of which has finite inputs.
47    fn compute(&self, data: &[Bar]) -> IndicatorValues {
48        let mut values = IndicatorValues::new();
49        if data.len() < self.period.saturating_add(1) {
50            for _ in 0..data.len() {
51                values.push(IndicatorValue::None);
52            }
53            return values;
54        }
55
56        // Calculate True Range for each bar
57        let mut tr_values = Vec::with_capacity(data.len());
58        tr_values.push(data[0].high - data[0].low); // First bar TR is just range
59
60        for i in 1..data.len() {
61            let current = &data[i];
62            let prev = &data[i - 1];
63
64            let tr = (current.high - current.low)
65                .max((current.high - prev.close).abs())
66                .max((current.low - prev.close).abs());
67
68            tr_values.push(tr);
69        }
70
71        // Calculate Choppiness Index
72        for i in 0..data.len() {
73            if i < self.period {
74                values.push(IndicatorValue::None);
75            } else {
76                // Sum of TR over period
77                let atr_sum: f64 = tr_values[i + 1 - self.period..=i].iter().sum();
78
79                // Highest High and Lowest Low over period
80                let window = &data[i + 1 - self.period..=i];
81                let highest = window
82                    .iter()
83                    .map(|b| b.high)
84                    .fold(f64::NEG_INFINITY, f64::max);
85                let lowest = window.iter().map(|b| b.low).fold(f64::INFINITY, f64::min);
86
87                let range = highest - lowest;
88
89                if range == 0.0 {
90                    values.push(IndicatorValue::None);
91                } else {
92                    // CHOP = 100 * LOG10(SUM(TR, n) / (Highest High - Lowest Low)) / LOG10(n)
93                    let chop = 100.0 * (atr_sum / range).log10() / (self.period as f64).log10();
94                    values.push(IndicatorValue::Single(chop));
95                }
96            }
97        }
98
99        values
100    }
101}
102
103impl Indicator for ChoppinessIndex {
104    crate::studies::inputs::study_inputs!(self;
105        period: length("Period", 1, usize::MAX)
106    ; {});
107
108    fn name(&self) -> &str {
109        "CHOP"
110    }
111
112    fn desc(&self) -> &str {
113        "Choppiness Index - Measures market trendiness"
114    }
115
116    fn calculate(&mut self, data: &[Bar]) {
117        self.update(data, 0);
118    }
119
120    fn update(&mut self, data: &[Bar], from: usize) {
121        let mut values = std::mem::take(&mut self.values);
122        update_window(
123            &mut values,
124            data,
125            from,
126            usable_behind(self.period, |b| {
127                b.high.is_finite() && b.low.is_finite() && b.close.is_finite()
128            }),
129            |data| {
130                over_finite_bars(
131                    data,
132                    |b| b.high.is_finite() && b.low.is_finite() && b.close.is_finite(),
133                    |data| self.compute(data),
134                )
135            },
136        );
137        self.values = values;
138    }
139
140    fn is_incremental(&self) -> bool {
141        true
142    }
143
144    fn values(&self) -> &IndicatorValues {
145        &self.values
146    }
147
148    fn colors(&self) -> Vec<Color32> {
149        vec![self.color]
150    }
151
152    fn set_colors(&mut self, colors: Vec<Color32>) {
153        if !colors.is_empty() {
154            self.color = colors[0];
155        }
156    }
157
158    fn is_overlay(&self) -> bool {
159        false
160    }
161
162    fn is_visible(&self) -> bool {
163        self.visible
164    }
165
166    fn set_visible(&mut self, visible: bool) {
167        self.visible = visible;
168    }
169
170    fn clone_box(&self) -> Box<dyn Indicator> {
171        Box::new(self.clone())
172    }
173
174    fn line_names(&self) -> Vec<String> {
175        vec![format!("CHOP({})", self.period)]
176    }
177}