Skip to main content

egui_charts/model/bar/
bar_data.rs

1//! Bar Data Container
2//!
3//! A collection of bars with utility methods for aggregation and analysis.
4
5use super::bar::Bar;
6
7/// Container for multiple bars with utility methods
8///
9/// This container provides common operations over bar collections.
10///
11/// # Example
12///
13/// ```
14/// use egui_charts::model::BarData;
15///
16/// let data = BarData::new();
17/// assert!(data.is_empty());
18/// ```
19#[derive(Debug, Clone, Default)]
20pub struct BarData {
21    /// Bars in caller-supplied order; construction does not sort or validate them.
22    pub bars: Vec<Bar>,
23}
24
25impl BarData {
26    /// Creates a new empty BarData
27    pub fn new() -> Self {
28        Self { bars: Vec::new() }
29    }
30
31    /// Creates BarData with pre-allocated capacity
32    pub fn with_capacity(capacity: usize) -> Self {
33        Self {
34            bars: Vec::with_capacity(capacity),
35        }
36    }
37
38    /// Creates BarData from a vector of bars
39    pub fn from_bars(bars: Vec<Bar>) -> Self {
40        Self { bars }
41    }
42
43    /// Adds a bar to the dataset
44    pub fn push(&mut self, bar: Bar) {
45        self.bars.push(bar);
46    }
47
48    /// Returns the number of bars
49    pub fn len(&self) -> usize {
50        self.bars.len()
51    }
52
53    /// Returns true if there are no bars
54    pub fn is_empty(&self) -> bool {
55        self.bars.is_empty()
56    }
57
58    /// Clear all bars
59    pub fn clear(&mut self) {
60        self.bars.clear();
61    }
62
63    /// Get the first bar
64    pub fn first(&self) -> Option<&Bar> {
65        self.bars.first()
66    }
67
68    /// Get the last bar
69    pub fn last(&self) -> Option<&Bar> {
70        self.bars.last()
71    }
72
73    /// Get a bar by index
74    pub fn get(&self, index: usize) -> Option<&Bar> {
75        self.bars.get(index)
76    }
77
78    /// Get a mutable reference to a bar by index
79    pub fn get_mut(&mut self, index: usize) -> Option<&mut Bar> {
80        self.bars.get_mut(index)
81    }
82
83    /// Iterate over bars
84    pub fn iter(&self) -> impl Iterator<Item = &Bar> {
85        self.bars.iter()
86    }
87
88    // ============= Aggregation Methods =============
89
90    /// Returns the min price (low) across all bars.
91    ///
92    /// Non-finite values are skipped; `None` when no finite value remains.
93    pub fn min_price(&self) -> Option<f64> {
94        self.bars
95            .iter()
96            .map(|c| c.low)
97            .filter(|v| v.is_finite())
98            .min_by(f64::total_cmp)
99    }
100
101    /// Returns the max price (high) across all bars.
102    ///
103    /// Non-finite values are skipped; `None` when no finite value remains.
104    pub fn max_price(&self) -> Option<f64> {
105        self.bars
106            .iter()
107            .map(|c| c.high)
108            .filter(|v| v.is_finite())
109            .max_by(f64::total_cmp)
110    }
111
112    /// Returns the price range (max_high - min_low)
113    pub fn price_range(&self) -> Option<(f64, f64)> {
114        let min = self.min_price()?;
115        let max = self.max_price()?;
116        Some((min, max))
117    }
118
119    /// Returns the max volume across all bars.
120    ///
121    /// Non-finite values are skipped; `None` when no finite value remains.
122    pub fn max_volume(&self) -> Option<f64> {
123        self.bars
124            .iter()
125            .map(|c| c.volume)
126            .filter(|v| v.is_finite())
127            .max_by(f64::total_cmp)
128    }
129
130    /// Returns the total volume across all bars
131    pub fn total_volume(&self) -> f64 {
132        self.bars.iter().map(|c| c.volume).sum()
133    }
134
135    /// Returns the average volume across all bars
136    pub fn avg_volume(&self) -> Option<f64> {
137        if self.bars.is_empty() {
138            return None;
139        }
140        Some(self.total_volume() / self.bars.len() as f64)
141    }
142
143    // ============= Transformation Methods =============
144
145    /// Converts regular OHLC bars to Heikin-Ashi bars
146    ///
147    /// Heikin-Ashi is a Japanese candlestick technique that filters out market noise
148    /// and highlights trends more clearly than traditional candlesticks.
149    ///
150    /// # Formula
151    /// - HA Close = (Open + High + Low + Close) / 4
152    /// - HA Open = (Previous HA Open + Previous HA Close) / 2
153    /// - HA High = Max(High, HA Open, HA Close)
154    /// - HA Low = Min(Low, HA Open, HA Close)
155    pub fn to_heikin_ashi(&self) -> Self {
156        if self.bars.is_empty() {
157            return Self::new();
158        }
159
160        let mut ha_bars = Vec::with_capacity(self.bars.len());
161
162        // First bar: use modified OHLC values
163        let first = &self.bars[0];
164        let mut prev_ha_open = (first.open + first.close) / 2.0;
165        let mut prev_ha_close = (first.open + first.high + first.low + first.close) / 4.0;
166
167        ha_bars.push(Bar::new(
168            first.time,
169            prev_ha_open,
170            first.high,
171            first.low,
172            prev_ha_close,
173            first.volume,
174        ));
175
176        // Process remaining bars
177        for bar in self.bars.iter().skip(1) {
178            let ha_close = (bar.open + bar.high + bar.low + bar.close) / 4.0;
179            let ha_open = (prev_ha_open + prev_ha_close) / 2.0;
180            let ha_high = bar.high.max(ha_open).max(ha_close);
181            let ha_low = bar.low.min(ha_open).min(ha_close);
182
183            ha_bars.push(Bar::new(
184                bar.time, ha_open, ha_high, ha_low, ha_close, bar.volume,
185            ));
186
187            prev_ha_open = ha_open;
188            prev_ha_close = ha_close;
189        }
190
191        Self::from_bars(ha_bars)
192    }
193
194    /// Returns a clone of the bar data (for regular candlesticks)
195    ///
196    /// This method exists for API consistency when switching between chart types.
197    pub fn to_regular(&self) -> Self {
198        self.clone()
199    }
200
201    /// Get a slice of bars within an index range
202    pub fn slice(&self, start: usize, end: usize) -> &[Bar] {
203        let end = end.min(self.bars.len());
204        let start = start.min(end);
205        &self.bars[start..end]
206    }
207}
208
209impl IntoIterator for BarData {
210    type Item = Bar;
211    type IntoIter = std::vec::IntoIter<Bar>;
212
213    fn into_iter(self) -> Self::IntoIter {
214        self.bars.into_iter()
215    }
216}
217
218impl<'a> IntoIterator for &'a BarData {
219    type Item = &'a Bar;
220    type IntoIter = std::slice::Iter<'a, Bar>;
221
222    fn into_iter(self) -> Self::IntoIter {
223        self.bars.iter()
224    }
225}
226
227impl FromIterator<Bar> for BarData {
228    fn from_iter<I: IntoIterator<Item = Bar>>(iter: I) -> Self {
229        Self {
230            bars: iter.into_iter().collect(),
231        }
232    }
233}
234
235impl std::ops::Index<usize> for BarData {
236    type Output = Bar;
237
238    fn index(&self, index: usize) -> &Self::Output {
239        &self.bars[index]
240    }
241}
242
243impl std::ops::IndexMut<usize> for BarData {
244    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
245        &mut self.bars[index]
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use chrono::Utc;
253
254    fn create_test_bars() -> BarData {
255        let now = Utc::now();
256        BarData::from_bars(vec![
257            Bar::new(now, 100.0, 110.0, 95.0, 105.0, 1000.0),
258            Bar::new(now, 105.0, 115.0, 100.0, 110.0, 1500.0),
259            Bar::new(now, 110.0, 120.0, 105.0, 115.0, 1200.0),
260        ])
261    }
262
263    #[test]
264    fn test_aggregations() {
265        let (nan, inf) = (f64::NAN, f64::INFINITY);
266        // (case, rows of (low, high, volume), expected (min_price, max_price, max_volume))
267        let cases = [
268            (
269                "finite only",
270                vec![
271                    (95.0, 110.0, 1000.0),
272                    (100.0, 115.0, 1500.0),
273                    (105.0, 120.0, 1200.0),
274                ],
275                (Some(95.0), Some(120.0), Some(1500.0)),
276            ),
277            (
278                "NaN first",
279                vec![(nan, nan, nan), (95.0, 110.0, 1000.0)],
280                (Some(95.0), Some(110.0), Some(1000.0)),
281            ),
282            (
283                "NaN last",
284                vec![(95.0, 110.0, 1000.0), (nan, nan, nan)],
285                (Some(95.0), Some(110.0), Some(1000.0)),
286            ),
287            (
288                "infinite high",
289                vec![(95.0, inf, 1000.0), (100.0, 110.0, 1500.0)],
290                (Some(95.0), Some(110.0), Some(1500.0)),
291            ),
292            ("all NaN", vec![(nan, nan, nan)], (None, None, None)),
293        ];
294        let now = Utc::now();
295        for (case, rows, expected) in cases {
296            let data = BarData::from_bars(
297                rows.iter()
298                    .map(|&(low, high, volume)| Bar::new(now, low, high, low, low, volume))
299                    .collect(),
300            );
301            let actual = (data.min_price(), data.max_price(), data.max_volume());
302            assert_eq!(actual, expected, "{case}");
303        }
304        assert_eq!(create_test_bars().total_volume(), 3700.0);
305    }
306
307    #[test]
308    fn test_heikin_ashi() {
309        let data = create_test_bars();
310        let ha = data.to_heikin_ashi();
311
312        // Closes average each OHLC row; opens average the preceding transformed open and close.
313        let actual: Vec<_> = ha.iter().map(|bar| (bar.open, bar.close)).collect();
314        assert_eq!(actual, [(102.5, 102.5), (102.5, 107.5), (105.0, 112.5)]);
315    }
316}