egui_charts/studies/builtin/
donchian.rs1use 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#[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 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 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 pub fn with_offset(mut self, offset: i32) -> Self {
72 self.offset = offset;
73 self
74 }
75}
76
77impl Default for DonchianChannels {
79 fn default() -> Self {
80 Self::new(20)
81 }
82}
83
84impl DonchianChannels {
85 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 }
179
180 fn line_count(&self) -> usize {
181 3 }
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 #[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 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 assert_eq!(study.plot_value(upper, 5), None);
246 }
247 }
248 }
249 }
250}