Skip to main content

egui_charts/studies/builtin/
week52_highlow.rs

1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
4use chrono::Duration;
5use egui::Color32;
6use std::collections::VecDeque;
7
8/// Close position in the high-low range over the inclusive last 364 days.
9///
10/// `(close - lowest_low) / (highest_high - lowest_low) * 100`, or 50 for a flat range.
11/// Bars before 52 weeks of history have no value, regardless of the bar interval.
12#[derive(Clone)]
13pub struct Week52HighLow {
14    values: IndicatorValues,
15    state: State,
16    colors: Vec<Color32>,
17    visible: bool,
18}
19
20impl Week52HighLow {
21    /// Create a 52-week high/low percentage study.
22    pub fn new() -> Self {
23        Self {
24            values: IndicatorValues::new(),
25            state: State::default(),
26            colors: vec![palette::PINK],
27            visible: true,
28        }
29    }
30}
31
32impl Default for Week52HighLow {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl Indicator for Week52HighLow {
39    fn value_format(&self) -> crate::scales::StudyFormat {
40        crate::scales::StudyFormat::Percent
41    }
42
43    fn name(&self) -> &str {
44        "52W HL%"
45    }
46
47    fn desc(&self) -> &str {
48        "52-Week High/Low Percentage - Close position within the last 364 days"
49    }
50
51    fn calculate(&mut self, data: &[Bar]) {
52        self.update(data, 0);
53    }
54
55    fn update(&mut self, data: &[Bar], from: usize) {
56        self.state.run(
57            data,
58            from,
59            &mut self.values,
60            |bar| [bar.high, bar.low, bar.close].iter().all(|x| x.is_finite()),
61            |bar, high, low| {
62                IndicatorValue::Single(if high == low {
63                    50.0
64                } else {
65                    (bar.close - low) / (high - low) * 100.0
66                })
67            },
68        );
69    }
70
71    fn is_incremental(&self) -> bool {
72        true
73    }
74
75    fn values(&self) -> &IndicatorValues {
76        &self.values
77    }
78
79    fn colors(&self) -> Vec<Color32> {
80        self.colors.clone()
81    }
82
83    fn set_colors(&mut self, colors: Vec<Color32>) {
84        if !colors.is_empty() {
85            self.colors = colors;
86        }
87    }
88
89    fn is_overlay(&self) -> bool {
90        false
91    }
92
93    fn is_visible(&self) -> bool {
94        self.visible
95    }
96
97    fn set_visible(&mut self, visible: bool) {
98        self.visible = visible;
99    }
100
101    fn clone_box(&self) -> Box<dyn Indicator> {
102        Box::new(self.clone())
103    }
104
105    fn line_names(&self) -> Vec<String> {
106        vec!["52W HL%".to_string()]
107    }
108}
109
110/// Highest high and lowest low over the inclusive last 364 days, drawn on the price pane.
111/// Bars before 52 weeks of history have no value, regardless of the bar interval.
112#[derive(Clone)]
113pub struct Week52HighLowLevels {
114    values: IndicatorValues,
115    state: State,
116    colors: Vec<Color32>,
117    visible: bool,
118}
119
120impl Week52HighLowLevels {
121    /// Create a 52-week high and low study.
122    pub fn new() -> Self {
123        Self {
124            values: IndicatorValues::new(),
125            state: State::default(),
126            colors: vec![palette::BLUE, palette::PINK],
127            visible: true,
128        }
129    }
130}
131
132impl Default for Week52HighLowLevels {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138impl Indicator for Week52HighLowLevels {
139    fn name(&self) -> &str {
140        "52W High/Low"
141    }
142
143    fn desc(&self) -> &str {
144        "52-Week High/Low - Highest high and lowest low within the last 364 days"
145    }
146
147    fn calculate(&mut self, data: &[Bar]) {
148        self.update(data, 0);
149    }
150    fn update(&mut self, data: &[Bar], from: usize) {
151        self.state.run(
152            data,
153            from,
154            &mut self.values,
155            |bar| bar.high.is_finite() && bar.low.is_finite(),
156            |_, high, low| IndicatorValue::Multiple(vec![high, low]),
157        );
158    }
159    fn is_incremental(&self) -> bool {
160        true
161    }
162
163    fn values(&self) -> &IndicatorValues {
164        &self.values
165    }
166
167    fn colors(&self) -> Vec<Color32> {
168        self.colors.clone()
169    }
170
171    fn set_colors(&mut self, colors: Vec<Color32>) {
172        for (kept, color) in self.colors.iter_mut().zip(colors) {
173            *kept = color;
174        }
175    }
176
177    fn is_overlay(&self) -> bool {
178        true
179    }
180
181    fn is_visible(&self) -> bool {
182        self.visible
183    }
184
185    fn set_visible(&mut self, visible: bool) {
186        self.visible = visible;
187    }
188
189    fn clone_box(&self) -> Box<dyn Indicator> {
190        Box::new(self.clone())
191    }
192
193    fn line_names(&self) -> Vec<String> {
194        vec!["52W High".to_string(), "52W Low".to_string()]
195    }
196}
197
198#[derive(Clone, Default)]
199struct State {
200    processed: usize,
201    first: Option<chrono::DateTime<chrono::Utc>>,
202    extremes: [VecDeque<usize>; 2],
203}
204
205impl State {
206    fn run(
207        &mut self,
208        data: &[Bar],
209        from: usize,
210        values: &mut IndicatorValues,
211        usable: impl Fn(&Bar) -> bool,
212        output: impl Fn(&Bar, f64, f64) -> IndicatorValue,
213    ) {
214        let len = values.len();
215        let from = if len > 0 && from >= len - 1 && data.len() >= len {
216            from.min(len)
217        } else {
218            *self = Self::default();
219            0
220        };
221        values.truncate(from);
222        // Commit completed bars only. Revisions of the developing bar leave the queues intact.
223        for at in self.processed..data.len() {
224            let bar = &data[at];
225            let last = at + 1 == data.len();
226            let value = if usable(bar) {
227                let first = self.first.unwrap_or(bar.time);
228                let mut range = [bar.high, bar.low];
229                for (line, queue) in self.extremes.iter_mut().enumerate() {
230                    let expired = |i: usize| bar.time - data[i].time > Duration::weeks(52);
231                    let price = |i: usize| if line == 0 { data[i].high } else { data[i].low };
232                    let better = |a: f64, b: f64| if line == 0 { a > b } else { a < b };
233                    if !last {
234                        while queue.front().is_some_and(|&i| expired(i)) {
235                            queue.pop_front();
236                        }
237                        while queue.back().is_some_and(|&i| !better(price(i), price(at))) {
238                            queue.pop_back();
239                        }
240                        queue.push_back(at);
241                    }
242                    // Include the bar exactly 52 weeks before this one.
243                    let start = queue.partition_point(|&i| expired(i));
244                    if let Some(&i) = queue.get(start)
245                        && better(price(i), range[line])
246                    {
247                        range[line] = price(i);
248                    }
249                }
250                if !last {
251                    self.first = Some(first);
252                }
253                if bar.time - first >= Duration::weeks(52) {
254                    output(bar, range[0], range[1])
255                } else {
256                    IndicatorValue::None
257                }
258            } else {
259                IndicatorValue::None
260            };
261            if at >= from {
262                values.push(value);
263            }
264            if !last {
265                self.processed = at + 1;
266            }
267        }
268    }
269}