Skip to main content

egui_charts/studies/builtin/
donchian.rs

1//! Donchian Channels Indicator
2//!
3//! Donchian Channels consist of three lines based on moving highest high
4//! and lowest low calculations. They're used to identify breakouts and trends.
5//!
6//! # Components
7//! - Upper Band: Highest high over N periods
8//! - Lower Band: Lowest low over N periods
9//! - Middle Band: Avg of upper and lower
10//!
11//! # Interpretation
12//! - Price breaking above upper band: Bullish breakout
13//! - Price breaking below lower band: Bearish breakout
14//! - Channel width indicates volatility
15//!
16//! # Example
17//! ```
18//! # 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();
19//! use egui_charts::studies::{DonchianChannels, Indicator};
20//!
21//! let mut donchian = DonchianChannels::new(20);
22//! donchian.calculate(&bars);
23//! ```
24
25use super::over_finite_bars;
26use crate::model::Bar;
27use crate::studies::palette;
28use crate::studies::stepper::{rolling_max, rolling_min};
29use crate::studies::stepper::{update_window, usable_behind};
30use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
31use egui::Color32;
32
33/// Donchian Channels indicator
34#[derive(Clone)]
35pub struct DonchianChannels {
36    period: usize,
37    offset: i32,
38    values: IndicatorValues,
39    upper_color: Color32,
40    middle_color: Color32,
41    lower_color: Color32,
42    visible: bool,
43}
44
45impl DonchianChannels {
46    /// Create a new Donchian Channels indicator
47    ///
48    /// # Arguments
49    /// * `period` - Lookback period (typically 20)
50    pub fn new(period: usize) -> Self {
51        Self {
52            period: period.max(1),
53            offset: 0,
54            values: IndicatorValues::new(),
55            upper_color: palette::BLUE,
56            middle_color: palette::ORANGE,
57            lower_color: palette::BLUE,
58            visible: true,
59        }
60    }
61
62    /// Set colors for upper, middle, and lower bands
63    pub fn with_colors(mut self, upper: Color32, middle: Color32, lower: Color32) -> Self {
64        self.upper_color = upper;
65        self.middle_color = middle;
66        self.lower_color = lower;
67        self
68    }
69
70    /// Draw every line `offset` bars later, earlier when negative.
71    pub fn with_offset(mut self, offset: i32) -> Self {
72        self.offset = offset;
73        self
74    }
75}
76
77/// Construct with the conventional default parameters.
78impl Default for DonchianChannels {
79    fn default() -> Self {
80        Self::new(20)
81    }
82}
83
84impl DonchianChannels {
85    /// The values over `data`, every bar of which has finite inputs.
86    fn compute(&self, data: &[Bar]) -> IndicatorValues {
87        let mut values = IndicatorValues::new();
88        if data.len() < self.period {
89            for _ in 0..data.len() {
90                values.push(IndicatorValue::None);
91            }
92            return values;
93        }
94
95        let highest = rolling_max(data.iter().map(|b| b.high), self.period);
96        let lowest = rolling_min(data.iter().map(|b| b.low), self.period);
97        for i in 0..data.len() {
98            if i < self.period - 1 {
99                values.push(IndicatorValue::None);
100            } else {
101                let (upper, lower) = (highest[i], lowest[i]);
102                let middle = (upper + lower) / 2.0;
103
104                values.push_lines(&[middle, upper, lower]);
105            }
106        }
107
108        values
109    }
110}
111
112impl Indicator for DonchianChannels {
113    crate::studies::inputs::study_inputs!(self;
114        period: length("Period", 1, usize::MAX),
115        offset: integer("Offset", i32::MIN, i32::MAX)
116    ; {});
117
118    fn plot_offset(&self, _line: usize) -> isize {
119        self.offset as isize
120    }
121
122    fn name(&self) -> &str {
123        "Donchian"
124    }
125
126    fn desc(&self) -> &str {
127        "Donchian Channels - Breakout indicator"
128    }
129
130    fn calculate(&mut self, data: &[Bar]) {
131        self.update(data, 0);
132    }
133
134    fn update(&mut self, data: &[Bar], from: usize) {
135        let mut values = std::mem::take(&mut self.values);
136        update_window(
137            &mut values,
138            data,
139            from,
140            usable_behind(self.period - 1, |b| b.high.is_finite() && b.low.is_finite()),
141            |data| {
142                over_finite_bars(
143                    data,
144                    |b| b.high.is_finite() && b.low.is_finite(),
145                    |data| self.compute(data),
146                )
147            },
148        );
149        self.values = values;
150    }
151
152    fn is_incremental(&self) -> bool {
153        true
154    }
155
156    fn values(&self) -> &IndicatorValues {
157        &self.values
158    }
159
160    fn colors(&self) -> Vec<Color32> {
161        vec![self.middle_color, self.upper_color, self.lower_color]
162    }
163
164    fn set_colors(&mut self, colors: Vec<Color32>) {
165        if !colors.is_empty() {
166            self.middle_color = colors[0];
167        }
168        if colors.len() > 1 {
169            self.upper_color = colors[1];
170        }
171        if colors.len() > 2 {
172            self.lower_color = colors[2];
173        }
174    }
175
176    fn is_overlay(&self) -> bool {
177        true // Donchian is drawn on main chart
178    }
179
180    fn line_count(&self) -> usize {
181        3 // Middle, Upper, Lower
182    }
183
184    fn is_visible(&self) -> bool {
185        self.visible
186    }
187
188    fn set_visible(&mut self, visible: bool) {
189        self.visible = visible;
190    }
191
192    fn clone_box(&self) -> Box<dyn Indicator> {
193        Box::new(self.clone())
194    }
195
196    fn line_names(&self) -> Vec<String> {
197        vec![
198            format!("Middle({})", self.period),
199            "Upper".to_string(),
200            "Lower".to_string(),
201        ]
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::studies::test_support::make_range_bar as make_bar;
209    use crate::studies::{InputValue, PriceChannel};
210
211    /// Period 2 over highs 1, 3, 2, 5 and lows 0, 1, 1, 2: from bar 1 the upper line is 3, 3, 5
212    /// and the middle line 1.5, 2, 3.
213    #[test]
214    fn channels_draw_their_lines_offset_bars_later() {
215        let bars = [
216            make_bar(1.0, 0.0, 0.5),
217            make_bar(3.0, 1.0, 2.0),
218            make_bar(2.0, 1.0, 1.5),
219            make_bar(5.0, 2.0, 3.0),
220        ];
221        type Build = fn(i32) -> Box<dyn Indicator>;
222        let rows: [(usize, usize, Build); 2] = [
223            (1, 0, |offset| {
224                Box::new(DonchianChannels::new(2).with_offset(offset))
225            }),
226            (0, 2, |offset| {
227                Box::new(PriceChannel::new(2).with_offset(offset))
228            }),
229        ];
230        for (upper, middle, build) in rows {
231            // Bar 1 at the upper line's first 3, bar 3 at the middle line's 3.
232            for (offset, first, last) in [(5, 6, 8), (-1, 0, 2)] {
233                let mut typed = build(0);
234                typed
235                    .set_input("offset", InputValue::Integer(offset))
236                    .unwrap();
237                for mut study in [build(offset), typed] {
238                    study.calculate(&bars);
239                    for line in 0..3 {
240                        assert_eq!(study.plot_offset(line), offset as isize);
241                    }
242                    assert_eq!(study.plot_value(upper, first), Some(3.0));
243                    assert_eq!(study.plot_value(middle, last), Some(3.0));
244                    // Bar 0 has no value at offset 5, and no bar 6 exists at offset -1.
245                    assert_eq!(study.plot_value(upper, 5), None);
246                }
247            }
248        }
249    }
250}