Skip to main content

egui_charts/studies/
metric.rs

1//! Pane study for a scalar metric published by the host's data source.
2
3use super::{Indicator, IndicatorValue, IndicatorValues, StudyData, StudyDataRequest};
4
5use crate::data::MetricSeries;
6use crate::model::Bar;
7use crate::scales::StudyFormat;
8use crate::studies::palette;
9use egui::Color32;
10
11/// A host metric, held by time on the chart's bars and drawn in an independent pane.
12/// Its output can be selected as another study's [`super::StudySource::Study`].
13///
14/// Connected charts request its history and live updates through the datafeed. Hosts
15/// supplying study data directly convert [`crate::data::MetricPoint`] into [`Bar`]
16/// and pass them to `Chart::set_study_data` with this study's request.
17#[derive(Clone)]
18pub struct MetricStudy {
19    metric: MetricSeries,
20    values: IndicatorValues,
21    color: Color32,
22    visible: bool,
23}
24
25impl MetricStudy {
26    /// Display this metric of the chart's current instrument.
27    pub fn new(metric: MetricSeries) -> Self {
28        Self {
29            metric,
30            values: IndicatorValues::new(),
31            color: palette::ACCENT,
32            visible: true,
33        }
34    }
35}
36
37impl Indicator for MetricStudy {
38    fn name(&self) -> &str {
39        &self.metric.name
40    }
41    fn desc(&self) -> &str {
42        &self.metric.unit
43    }
44    fn is_overlay(&self) -> bool {
45        false
46    }
47    fn value_format(&self) -> StudyFormat {
48        self.metric.format
49    }
50    fn line_names(&self) -> Vec<String> {
51        vec![if self.metric.unit.is_empty() {
52            self.metric.name.clone()
53        } else {
54            format!("{} ({})", self.metric.name, self.metric.unit)
55        }]
56    }
57    fn plot_style(&self, _: usize) -> super::PlotStyle {
58        super::PlotStyle {
59            color: self.color,
60            width: 1.0,
61            pattern: crate::model::LineStyle::Solid,
62            plot_type: super::PlotType::StepLine,
63            visible: true,
64        }
65    }
66    fn data_requests(&self) -> Vec<StudyDataRequest> {
67        vec![StudyDataRequest {
68            symbol: String::new(),
69            timeframe: None,
70            metric: Some(self.metric.id.clone()),
71        }]
72    }
73    fn calculate(&mut self, data: &[Bar]) {
74        self.update_data(data, 0, &[]);
75    }
76    fn update_data(&mut self, data: &[Bar], _: usize, sources: &[StudyData<'_>]) {
77        let request = &self.data_requests()[0];
78        let source = sources.iter().find(|source| &source.request == request);
79        self.values = data
80            .iter()
81            .map(|bar| {
82                source
83                    .and_then(|source| source.bar_at(bar.time).map(|i| source.bars[i].close))
84                    .map_or(IndicatorValue::None, IndicatorValue::Single)
85            })
86            .collect();
87    }
88    fn values(&self) -> &IndicatorValues {
89        &self.values
90    }
91    fn colors(&self) -> Vec<Color32> {
92        vec![self.color]
93    }
94    fn set_colors(&mut self, colors: Vec<Color32>) {
95        if let Some(&color) = colors.first() {
96            self.color = color;
97        }
98    }
99    fn is_visible(&self) -> bool {
100        self.visible
101    }
102    fn set_visible(&mut self, visible: bool) {
103        self.visible = visible;
104    }
105    fn clone_box(&self) -> Box<dyn Indicator> {
106        Box::new(self.clone())
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::data::MetricPoint;
114    use crate::model::{BarClock, Timeframe};
115    use chrono::DateTime;
116
117    /// Publication instants are exact: earlier chart bars have no value, later bars
118    /// carry the last point, and a correction replaces every bar that held that point.
119    #[test]
120    fn irregular_metric_points_hold_until_the_next_publication() {
121        let time = |seconds| DateTime::from_timestamp(seconds, 0).unwrap();
122        let points = [(30, 10.0), (150, 20.0), (301, 5.0)];
123        let mut bars: Vec<Bar> = points
124            .into_iter()
125            .map(|(t, value)| {
126                MetricPoint {
127                    time: time(t),
128                    value,
129                }
130                .into()
131            })
132            .collect();
133        let chart: Vec<_> = [0, 30, 60, 120, 150, 180, 240, 300, 360]
134            .into_iter()
135            .map(|t| Bar::new(time(t), 1.0, 1.0, 1.0, 1.0, 1.0))
136            .collect();
137        let mut study = MetricStudy::new(MetricSeries {
138            id: "oi".into(),
139            name: "Open interest".into(),
140            unit: "contracts".into(),
141            format: StudyFormat::Volume,
142        });
143        let request = study.data_requests().pop().unwrap();
144        // 10 applies from second 30, 20 from second 150, and 5 from second 301.
145        for (edit, expected) in [
146            (
147                None,
148                [
149                    None,
150                    Some(10.0),
151                    Some(10.0),
152                    Some(10.0),
153                    Some(20.0),
154                    Some(20.0),
155                    Some(20.0),
156                    Some(20.0),
157                    Some(5.0),
158                ],
159            ),
160            (
161                Some(12.0),
162                [
163                    None,
164                    Some(12.0),
165                    Some(12.0),
166                    Some(12.0),
167                    Some(20.0),
168                    Some(20.0),
169                    Some(20.0),
170                    Some(20.0),
171                    Some(5.0),
172                ],
173            ),
174        ] {
175            if let Some(value) = edit {
176                bars[0] = MetricPoint {
177                    time: time(30),
178                    value,
179                }
180                .into();
181            }
182            let source = StudyData {
183                request: request.clone(),
184                bars: &bars,
185                clock: BarClock::new(Timeframe::Ticks(1.try_into().unwrap())),
186                instrument: None,
187                timeframe: Timeframe::Min1,
188            };
189            study.update_data(&chart, 0, &[source]);
190            for (i, want) in expected.into_iter().enumerate() {
191                assert_eq!(
192                    study.values().value(0, i),
193                    want,
194                    "bar {i}, correction {edit:?}"
195                );
196            }
197        }
198        study.calculate(&chart);
199        assert!(
200            study.values().columns().is_empty(),
201            "missing histories stay unavailable"
202        );
203    }
204}