Skip to main content

egui_charts/studies/builtin/
chop_zone.rs

1//! Chop Zone
2//!
3//! The Chop Zone colours a column on every bar by the angle of a 34-bar EMA of the close, so
4//! steep slopes (trends) and flat stretches (chop) stand out at a glance.
5//!
6//! # Formula
7//! - span = 25 / (highest high - lowest low over 30 bars) * lowest low
8//! - y = (EMA(close, 34)[1] - EMA(close, 34)) / hlc3 * span
9//! - angle = atan(|y|) in degrees, rounded to a whole degree, negative when the EMA fell (y > 0)
10//!
11//! The EMA starts with the simple mean of its first 34 closes. Every column has the value 1;
12//! the colour carries the reading:
13//!
14//! | angle | colour |
15//! |---|---|
16//! | 5 or more | turquoise |
17//! | 3.57 to 5 | dark green |
18//! | 2.14 to 3.57 | pale green |
19//! | 0.71 to 2.14 | lime |
20//! | between -0.71 and 0.71 | yellow |
21//! | -2.14 to -0.71 | light orange |
22//! | -3.57 to -2.14 | orange |
23//! | -5 to -3.57 | red |
24//! | -5 or less | dark red |
25//!
26//! A bar whose 30-bar range is zero has no value.
27//!
28//! # Example
29//! ```
30//! # 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();
31//! use egui_charts::studies::{ChopZone, Indicator};
32//!
33//! let mut zone = ChopZone::new();
34//! zone.calculate(&bars);
35//! assert!(zone.plot_color(0, 59).is_some());
36//! ```
37
38use super::smoothing::Smoothing;
39use crate::model::Bar;
40use crate::studies::palette;
41use crate::studies::stepper::Stepper;
42use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
43use egui::Color32;
44
45const EMA_LENGTH: usize = 34;
46const RANGE_LENGTH: usize = 30;
47
48/// Chop Zone: unit columns coloured by the angle of the 34-bar EMA.
49#[derive(Clone)]
50pub struct ChopZone {
51    values: IndicatorValues,
52    stepper: Stepper<State>,
53    /// The angle in whole degrees at each source bar.
54    angles: IndicatorValues,
55    /// Band colours from the steepest rise to the steepest fall.
56    colors: [Color32; 9],
57    visible: bool,
58}
59
60#[derive(Clone, Default)]
61struct State {
62    count: usize,
63    prices: std::collections::VecDeque<[f64; 2]>,
64    ema: Smoothing,
65    previous: f64,
66}
67
68impl ChopZone {
69    /// Create the Chop Zone; its lengths are fixed by the definition.
70    pub fn new() -> Self {
71        Self {
72            values: IndicatorValues::new(),
73            stepper: Stepper::default(),
74            angles: IndicatorValues::new(),
75            colors: [
76                palette::CYAN,
77                palette::DARK_GREEN,
78                palette::LIGHT_GREEN,
79                palette::DEEP_TEAL,
80                palette::YELLOW,
81                palette::LIGHT_ORANGE,
82                palette::ORANGE,
83                palette::PINK,
84                palette::DEEP_RED,
85            ],
86            visible: true,
87        }
88    }
89}
90
91/// Construct with the conventional definition.
92impl Default for ChopZone {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl Indicator for ChopZone {
99    fn plot_style(&self, _line: usize) -> crate::studies::PlotStyle {
100        crate::studies::PlotStyle {
101            color: self.colors[0],
102            width: 1.0,
103            pattern: crate::model::LineStyle::Solid,
104            plot_type: crate::studies::PlotType::Columns,
105            visible: true,
106        }
107    }
108
109    fn plot_color(&self, _line: usize, bar: usize) -> Option<Color32> {
110        let angle = self.angles.value(0, bar)?;
111        let band = match angle {
112            a if a >= 5.0 => 0,
113            a if a >= 3.57 => 1,
114            a if a >= 2.14 => 2,
115            a if a >= 0.71 => 3,
116            a if a <= -5.0 => 8,
117            a if a <= -3.57 => 7,
118            a if a <= -2.14 => 6,
119            a if a <= -0.71 => 5,
120            _ => 4,
121        };
122        Some(self.colors[band])
123    }
124
125    fn value_format(&self) -> crate::scales::StudyFormat {
126        crate::scales::StudyFormat::Fixed(0)
127    }
128
129    fn name(&self) -> &str {
130        "Chop Zone"
131    }
132
133    fn desc(&self) -> &str {
134        "Chop Zone - Trend and chop by the angle of an EMA"
135    }
136
137    fn calculate(&mut self, data: &[Bar]) {
138        self.update(data, 0);
139    }
140
141    fn update(&mut self, data: &[Bar], from: usize) {
142        self.stepper
143            .run(data, from, &mut self.angles, |state, bar| {
144                if ![bar.high, bar.low, bar.close].iter().all(|x| x.is_finite()) {
145                    return IndicatorValue::None;
146                }
147                state.count += 1;
148                state.prices.push_back([bar.high, bar.low]);
149                if state.prices.len() > RANGE_LENGTH {
150                    state.prices.pop_front();
151                }
152                let ema = state
153                    .ema
154                    .next(bar.close, EMA_LENGTH, 2.0 / (EMA_LENGTH as f64 + 1.0));
155                let previous = std::mem::replace(&mut state.previous, ema);
156                if state.count <= RANGE_LENGTH.max(EMA_LENGTH) {
157                    return IndicatorValue::None;
158                }
159                let high = state
160                    .prices
161                    .iter()
162                    .map(|v| v[0])
163                    .fold(f64::NEG_INFINITY, f64::max);
164                let low = state
165                    .prices
166                    .iter()
167                    .map(|v| v[1])
168                    .fold(f64::INFINITY, f64::min);
169                let range = high - low;
170                if range == 0.0 {
171                    return IndicatorValue::None;
172                }
173                let span = 25.0 / range * low;
174                let hlc3 = (bar.high + bar.low + bar.close) / 3.0;
175                let y = (previous - ema) / hlc3 * span;
176                let angle = y.abs().atan().to_degrees().round();
177                IndicatorValue::Single(if y > 0.0 { -angle } else { angle })
178            });
179        let start = from.min(self.values.len()).min(data.len());
180        self.values.truncate(start);
181        for i in start..data.len() {
182            self.values.push(if self.angles.value(0, i).is_some() {
183                IndicatorValue::Single(1.0)
184            } else {
185                IndicatorValue::None
186            });
187        }
188    }
189
190    fn is_incremental(&self) -> bool {
191        true
192    }
193
194    fn values(&self) -> &IndicatorValues {
195        &self.values
196    }
197
198    fn colors(&self) -> Vec<Color32> {
199        self.colors.to_vec()
200    }
201
202    fn set_colors(&mut self, colors: Vec<Color32>) {
203        for (old, new) in self.colors.iter_mut().zip(colors) {
204            *old = new;
205        }
206    }
207
208    fn is_overlay(&self) -> bool {
209        false
210    }
211
212    fn is_visible(&self) -> bool {
213        self.visible
214    }
215
216    fn set_visible(&mut self, visible: bool) {
217        self.visible = visible;
218    }
219
220    fn clone_box(&self) -> Box<dyn Indicator> {
221        Box::new(self.clone())
222    }
223
224    fn line_names(&self) -> Vec<String> {
225        vec!["Chop Zone".into()]
226    }
227}