Skip to main content

egui_charts/studies/builtin/
visible_average_price.rs

1//! Arithmetic mean of a price source over the bars currently visible on the chart.
2
3use crate::model::{Bar, PriceSource};
4use crate::studies::palette;
5use crate::studies::{Indicator, IndicatorValues};
6use egui::Color32;
7use std::ops::Range;
8
9/// Average of the visible bars' closes, recalculated when the chart pans or zooms.
10#[derive(Clone)]
11pub struct VisibleAveragePrice {
12    source: PriceSource,
13    range: Range<usize>,
14    values: IndicatorValues,
15    mean: Option<f64>,
16    color: Color32,
17    visible: bool,
18}
19
20impl VisibleAveragePrice {
21    /// A close-price average, over all loaded bars until a visible range is supplied.
22    pub fn new() -> Self {
23        Self {
24            source: PriceSource::Close,
25            range: 0..usize::MAX,
26            values: IndicatorValues::new(),
27            mean: None,
28            color: palette::GRAY,
29            visible: true,
30        }
31    }
32}
33
34impl Default for VisibleAveragePrice {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl Indicator for VisibleAveragePrice {
41    fn source(&self) -> Option<crate::studies::StudySource> {
42        Some(crate::studies::StudySource::Price(self.source))
43    }
44
45    crate::studies::inputs::study_inputs!(self;
46        source: choice("Source", PriceSource::all())
47    ; {});
48
49    fn name(&self) -> &str {
50        "Visible Average Price"
51    }
52    fn desc(&self) -> &str {
53        "Arithmetic mean of the visible bars' price source"
54    }
55    fn set_visible_range(&mut self, range: Range<usize>) -> bool {
56        let changed = self.range != range;
57        self.range = range;
58        changed
59    }
60    fn calculate(&mut self, data: &[Bar]) {
61        self.values = IndicatorValues::missing(data.len());
62        let end = self.range.end.min(data.len());
63        let start = self.range.start.min(end);
64        let price = |bar: &Bar| self.source.compute(bar.open, bar.high, bar.low, bar.close);
65        let (mut mean, mut count) = (0.0, 0.0);
66        for value in data[start..end].iter().map(price).filter(|v| v.is_finite()) {
67            count += 1.0;
68            mean = mean * ((count - 1.0) / count) + value / count;
69        }
70        self.mean = (count != 0.0).then_some(mean);
71        if count != 0.0 {
72            for (i, bar) in data.iter().enumerate() {
73                if price(bar).is_finite() {
74                    self.values.set_value(0, i, mean);
75                }
76            }
77        }
78    }
79    fn update(&mut self, data: &[Bar], from: usize) {
80        let start = from.min(self.values.len()).min(data.len());
81        if start < self.range.end || from == 0 {
82            // A changed visible price moves the average on every bar carrying that level.
83            self.calculate(data);
84            return;
85        }
86        self.values.truncate(start);
87        for bar in &data[start..] {
88            let price = self.source.compute(bar.open, bar.high, bar.low, bar.close);
89            self.values.push_lines(&[if price.is_finite() {
90                self.mean.unwrap_or(f64::NAN)
91            } else {
92                f64::NAN
93            }]);
94        }
95    }
96    fn values(&self) -> &IndicatorValues {
97        &self.values
98    }
99    fn colors(&self) -> Vec<Color32> {
100        vec![self.color]
101    }
102    fn set_colors(&mut self, colors: Vec<Color32>) {
103        if let Some(color) = colors.first() {
104            self.color = *color;
105        }
106    }
107    fn is_visible(&self) -> bool {
108        self.visible
109    }
110    fn set_visible(&mut self, visible: bool) {
111        self.visible = visible;
112    }
113    fn clone_box(&self) -> Box<dyn Indicator> {
114        Box::new(self.clone())
115    }
116    fn line_names(&self) -> Vec<String> {
117        vec!["Avg Price".into()]
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use crate::model::Bar;
124    use crate::studies::*;
125    use chrono::{TimeZone, Utc};
126
127    /// The visible mean uses exactly the supplied bars and changes with the viewport.
128    #[test]
129    fn visible_average_follows_the_visible_range() {
130        let bars: Vec<_> = [2.0, 4.0, 12.0, 20.0]
131            .into_iter()
132            .enumerate()
133            .map(|(i, c)| Bar::new(Utc.timestamp_opt(i as i64, 0).unwrap(), c, c, c, c, 1.0))
134            .collect();
135        let mut study = ConfiguredStudy::new(
136            IndicatorFactory::new()
137                .create("Visible Average Price")
138                .expect("Visible Average Price study"),
139        );
140        for (range, expected) in [(0..2, Some(3.0)), (1..4, Some(12.0)), (4..99, None)] {
141            study.set_visible_range(range.clone());
142            study.calculate(&bars);
143            for i in 0..bars.len() {
144                assert_eq!(study.values().value(0, i), expected);
145            }
146        }
147        #[cfg(feature = "widget")]
148        {
149            use crate::model::LogicalRange;
150            use crate::widget::{Chart, test_frames::frame};
151            let bars: Vec<_> = (0..100)
152                .map(|i| {
153                    let c = i as f64;
154                    Bar::new(Utc.timestamp_opt(i, 0).unwrap(), c, c, c, c, 1.0)
155                })
156                .collect();
157            let mut chart = Chart::new(crate::model::BarData::from_bars(bars));
158            use crate::studies::{InputValue, StudySource};
159            let source = chart.add_indicator(Box::new(SMA::new(1)));
160            let average = chart.add_indicator(Box::new(study));
161            chart
162                .set_indicator_source(
163                    average,
164                    StudySource::Study {
165                        id: source.0,
166                        plot: 0,
167                    },
168                )
169                .unwrap();
170            let consumer = chart.add_indicator(Box::new(SMA::new(1)));
171            chart
172                .set_indicator_source(
173                    consumer,
174                    StudySource::Study {
175                        id: average.0,
176                        plot: 0,
177                    },
178                )
179                .unwrap();
180            assert!(
181                chart
182                    .indicators()
183                    .find(|(id, _)| *id == average)
184                    .unwrap()
185                    .1
186                    .inputs()
187                    .iter()
188                    .any(|input| input.id == "source"
189                        && matches!(input.value, InputValue::Source(_)))
190            );
191            let ctx = egui::Context::default();
192            frame(&ctx, &mut chart, 0.0, Vec::new());
193            // Edges halfway through bars 10 and 30 show bars 10 to 30, whose closes average 20.
194            for (time, left, expected) in [(1.0, 10.5, 20.0), (2.0, 50.5, 60.0)] {
195                assert!(chart.set_visible_logical_range(LogicalRange::new(left, left + 19.0)));
196                frame(&ctx, &mut chart, time, Vec::new());
197                for id in [average, consumer] {
198                    let study = chart
199                        .indicators()
200                        .find(|(candidate, _)| *candidate == id)
201                        .unwrap()
202                        .1;
203                    assert!((study.values().value(0, 0).unwrap() - expected).abs() < 1e-10);
204                }
205            }
206        }
207    }
208}