Skip to main content

egui_charts/studies/builtin/
up_down_volume.rs

1//! Volume classified by the direction of host-supplied lower-timeframe candles.
2//!
3//! A rising candle contributes to up volume; a falling candle to down volume. When open
4//! equals close, a close at or above the previous intrabar close counts as up, otherwise
5//! down. Up volume is positive, down volume negative and delta their sum. Missing intrabar
6//! history has no value; the parent candle's volume is never substituted for it. The Lower
7//! interval input chooses the candles' interval; one that is not shorter than the chart's or
8//! does not divide it reads the automatic one.
9
10use super::lower_interval;
11use crate::model::LineStyle;
12use crate::model::{Bar, BarClock, Timeframe};
13use crate::scales::StudyFormat;
14use crate::studies::palette;
15use crate::studies::{
16    Indicator, IndicatorValues, InputValue, PlotStyle, PlotType, StudyData, StudyDataRequest,
17    StudyInput,
18};
19use egui::Color32;
20
21/// Up and down volume from intrabars supplied by the host, in a separate pane.
22#[derive(Clone)]
23pub struct UpDownVolume {
24    clock: BarClock,
25    /// The interval chosen in the Lower interval input; `None` picks one from the chart interval.
26    lower_interval: Option<Timeframe>,
27    values: IndicatorValues,
28    colors: [Color32; 3],
29    visible: bool,
30}
31
32impl UpDownVolume {
33    /// One-minute intrabars on a one-minute chart until the chart supplies its interval.
34    /// Hosts answer [`Indicator::data_requests`] through the chart or `update_data`.
35    pub fn new() -> Self {
36        Self {
37            clock: BarClock::new(Timeframe::Min1),
38            lower_interval: None,
39            values: IndicatorValues::new(),
40            colors: [
41                palette::GREEN.gamma_multiply_u8(102),
42                palette::DOWN.gamma_multiply_u8(102),
43                palette::GRAY,
44            ],
45            visible: true,
46        }
47    }
48
49    fn lower_timeframe(&self) -> Option<Timeframe> {
50        lower_interval::lower_timeframe(&self.clock, self.lower_interval)
51    }
52    fn calculate_intrabars(&mut self, data: &[Bar], intrabars: &[Bar], from: usize) {
53        let from = from.min(self.values.len()).min(data.len());
54        if from == 0 || intrabars.is_empty() {
55            self.values = IndicatorValues::missing(data.len());
56        } else {
57            self.values.truncate(from);
58            self.values
59                .resize(data.len(), crate::studies::IndicatorValue::None);
60        }
61        if intrabars.is_empty() {
62            return;
63        }
64        let Some(lower) = self.lower_timeframe() else {
65            return;
66        };
67        let ranges = lower_interval::nest(&self.clock, lower, data, intrabars, from);
68        for (i, range) in (from..).zip(ranges) {
69            if !range.is_empty() && data[i].close.is_finite() {
70                let flow = lower_interval::flow(intrabars, range);
71                for (line, value) in [flow.up, flow.down, flow.up + flow.down]
72                    .into_iter()
73                    .enumerate()
74                {
75                    self.values.set_value(line, i, value);
76                }
77            }
78        }
79    }
80}
81
82impl Default for UpDownVolume {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl Indicator for UpDownVolume {
89    fn name(&self) -> &str {
90        "Up/Down Volume"
91    }
92    fn desc(&self) -> &str {
93        "Up and down volume from lower-timeframe candles supplied by the host"
94    }
95    fn inputs(&self) -> Vec<StudyInput> {
96        vec![lower_interval::input(self.lower_interval)]
97    }
98    fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
99        let ("lower_interval", InputValue::Timeframe(chosen)) = (id, value) else {
100            return Err(crate::studies::StudyError::rejected(&self.inputs(), id));
101        };
102        self.lower_interval = chosen;
103        self.calculate(&[]);
104        Ok(())
105    }
106    fn data_requests(&self) -> Vec<StudyDataRequest> {
107        self.lower_timeframe()
108            .map(lower_interval::request)
109            .into_iter()
110            .collect()
111    }
112    fn update_data(&mut self, data: &[Bar], from: usize, sources: &[StudyData<'_>]) {
113        let from = from.min(self.values.len()).min(data.len());
114        let intrabars = match self.lower_timeframe() {
115            Some(lower) => {
116                match lower_interval::reply(&lower_interval::request(lower), from, sources) {
117                    Some(bars) => bars,
118                    None => return,
119                }
120            }
121            None => &[],
122        };
123        self.calculate_intrabars(data, intrabars, from);
124    }
125    fn set_timeframe(&mut self, timeframe: Timeframe) {
126        self.set_bar_clock(&self.clock.for_timeframe(timeframe));
127    }
128    fn set_bar_clock(&mut self, clock: &BarClock) -> bool {
129        let changed = self.clock != *clock;
130        self.clock = clock.clone();
131        changed
132    }
133    fn calculate(&mut self, data: &[Bar]) {
134        self.calculate_intrabars(data, &[], 0);
135    }
136    fn is_incremental(&self) -> bool {
137        true
138    }
139    fn is_overlay(&self) -> bool {
140        false
141    }
142    fn value_format(&self) -> StudyFormat {
143        StudyFormat::Volume
144    }
145    fn plot_style(&self, line: usize) -> PlotStyle {
146        PlotStyle {
147            color: self.colors.get(line).copied().unwrap_or(Color32::WHITE),
148            width: 1.0,
149            pattern: LineStyle::Solid,
150            plot_type: if line == 2 {
151                PlotType::Crosses
152            } else {
153                PlotType::Columns
154            },
155            visible: true,
156        }
157    }
158    fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
159        (line == 2)
160            .then(|| {
161                self.values.value(2, bar).map(|delta| {
162                    let [r, g, b, _] =
163                        self.colors[usize::from(delta <= 0.0)].to_srgba_unmultiplied();
164                    Color32::from_rgb(r, g, b)
165                })
166            })
167            .flatten()
168    }
169    fn values(&self) -> &IndicatorValues {
170        &self.values
171    }
172    fn colors(&self) -> Vec<Color32> {
173        self.colors.to_vec()
174    }
175    fn set_colors(&mut self, colors: Vec<Color32>) {
176        for (old, new) in self.colors.iter_mut().zip(colors) {
177            *old = new;
178        }
179    }
180    fn is_visible(&self) -> bool {
181        self.visible
182    }
183    fn set_visible(&mut self, visible: bool) {
184        self.visible = visible;
185    }
186    fn clone_box(&self) -> Box<dyn Indicator> {
187        Box::new(self.clone())
188    }
189    fn line_names(&self) -> Vec<String> {
190        ["Up Volume", "Down Volume", "Delta"]
191            .map(String::from)
192            .to_vec()
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use crate::model::Bar;
199    use crate::studies::*;
200    use chrono::{TimeZone, Utc};
201
202    /// Candle direction, doji ties across parent bars and missing intrabar history stay distinct.
203    #[test]
204    fn up_down_volume_uses_host_intrabar_directions() {
205        use crate::model::{BarClock, Timeframe};
206        use crate::studies::{StudyData, StudyDataRequest};
207        let requested_interval = |study: &dyn Indicator| {
208            study
209                .data_requests()
210                .first()
211                .and_then(|request| request.timeframe)
212        };
213        let update = |study: &mut dyn Indicator, data: &[Bar], timeframe, bars: &[Bar]| {
214            study.update_data(
215                data,
216                0,
217                &[StudyData {
218                    request: StudyDataRequest {
219                        metric: None,
220                        symbol: String::new(),
221                        timeframe: Some(timeframe),
222                    },
223                    bars,
224                    clock: BarClock::new(timeframe),
225                    instrument: None,
226                    timeframe,
227                }],
228            );
229        };
230        let mut study = ConfiguredStudy::new(
231            IndicatorFactory::new()
232                .create("Up/Down Volume")
233                .expect("Up/Down Volume study"),
234        );
235        study.set_timeframe(Timeframe::Day1);
236        assert_eq!(requested_interval(&study), Some(Timeframe::Min5));
237        study.set_bar_clock(&BarClock::new(Timeframe::Min5));
238        assert_eq!(requested_interval(&study), Some(Timeframe::Min1));
239        study.set_bar_clock(&BarClock::new(Timeframe::Ms100));
240        assert_eq!(requested_interval(&study), None);
241        study.set_bar_clock(&BarClock::new(Timeframe::Min5));
242        let bars: Vec<_> = [0, 5, 10, 15]
243            .into_iter()
244            .map(|m| {
245                Bar::new(
246                    Utc.timestamp_opt(m * 60, 0).unwrap(),
247                    10.0,
248                    10.0,
249                    10.0,
250                    10.0,
251                    999.0,
252                )
253            })
254            .collect();
255        let intrabars: Vec<_> = [
256            (0, 10.0, 11.0, 10.0),
257            (1, 11.0, 10.0, 20.0),
258            (2, 10.0, 10.0, 30.0),
259            (5, 9.0, 9.0, 40.0),
260            (6, 9.0, 9.0, 50.0),
261            (15, 12.0, 13.0, 0.0),
262        ]
263        .into_iter()
264        .map(|(m, o, c, v)| {
265            Bar::new(
266                Utc.timestamp_opt(m * 60, 0).unwrap(),
267                o,
268                o.max(c),
269                o.min(c),
270                c,
271                v,
272            )
273        })
274        .collect();
275        update(&mut study, &bars, Timeframe::Min1, &intrabars);
276        // First parent: up=10+30, down=-20, delta=20. Next: -40+50=10.
277        for (i, want) in [
278            [Some(40.0), Some(-20.0), Some(20.0)],
279            [Some(50.0), Some(-40.0), Some(10.0)],
280            [None; 3],
281            [Some(0.0); 3],
282        ]
283        .into_iter()
284        .enumerate()
285        {
286            for (line, expected) in want.into_iter().enumerate() {
287                assert_eq!(
288                    study.values().value(line, i),
289                    expected,
290                    "bar {i}, line {line}"
291                );
292            }
293        }
294        // A stale interval is unavailable; malformed snapshots leave the accepted values intact.
295        update(&mut study, &bars, Timeframe::Min5, &intrabars);
296        assert_eq!(study.values().value(0, 0), None);
297        update(&mut study, &bars, Timeframe::Min1, &intrabars);
298        for invalid in [
299            {
300                let mut invalid = intrabars.clone();
301                invalid[1].time = invalid[0].time;
302                invalid
303            },
304            {
305                let mut invalid = intrabars.clone();
306                invalid[0].volume = -1.0;
307                invalid
308            },
309            {
310                let mut invalid = intrabars.clone();
311                invalid[0].open = f64::NAN;
312                invalid
313            },
314        ] {
315            update(&mut study, &bars, Timeframe::Min1, &invalid);
316            assert_eq!(study.values().value(2, 0), Some(20.0));
317        }
318        let mut corrected = intrabars;
319        corrected[0].volume = 15.0;
320        update(&mut study, &bars, Timeframe::Min1, &corrected);
321        assert_eq!(study.values().value(2, 0), Some(25.0));
322        update(&mut study, &bars, Timeframe::Min1, &[]);
323        assert_eq!(study.values().value(0, 0), None);
324        // Custom intervals must nest their intrabars without rounding long bars to dates.
325        for (seconds, lower) in [(90, Timeframe::Sec1), (90_000, Timeframe::Hour1)] {
326            let timeframe = Timeframe::Custom(std::num::NonZeroU64::new(seconds).unwrap());
327            study.set_timeframe(timeframe);
328            assert_eq!(requested_interval(&study), Some(lower));
329            let bar = |second| {
330                Bar::new(
331                    Utc.timestamp_opt(second, 0).unwrap(),
332                    1.0,
333                    2.0,
334                    1.0,
335                    2.0,
336                    1.0,
337                )
338            };
339            let seconds = seconds as i64;
340            update(
341                &mut study,
342                &[bar(0), bar(seconds)],
343                lower,
344                &[0, seconds - lower.duration_ms() / 1000, seconds].map(bar),
345            );
346            assert_eq!(study.values().value(0, 0), Some(2.0));
347            assert_eq!(study.values().value(0, 1), Some(1.0));
348        }
349        // A chosen lower interval replaces the automatic one when it is shorter than the
350        // chart's and divides it; otherwise the automatic one is read.
351        study.set_bar_clock(&BarClock::new(Timeframe::Min5));
352        let custom = |s| Timeframe::Custom(std::num::NonZeroU64::new(s).unwrap());
353        for (chosen, wanted) in [
354            (Some(Timeframe::Sec30), Timeframe::Sec30),
355            (Some(Timeframe::Min5), Timeframe::Min1),
356            (Some(custom(120)), Timeframe::Min1),
357            (
358                Some(Timeframe::Ticks(std::num::NonZeroU64::MIN)),
359                Timeframe::Min1,
360            ),
361            (None, Timeframe::Min1),
362        ] {
363            study
364                .set_input("lower_interval", InputValue::Timeframe(chosen))
365                .unwrap();
366            assert_eq!(requested_interval(&study), Some(wanted), "{chosen:?}");
367        }
368        study
369            .set_input(
370                "lower_interval",
371                InputValue::Timeframe(Some(Timeframe::Sec30)),
372            )
373            .unwrap();
374        let candle = |s: i64, o: f64, c: f64, v: f64| {
375            Bar::new(
376                Utc.timestamp_opt(s, 0).unwrap(),
377                o,
378                o.max(c),
379                o.min(c),
380                c,
381                v,
382            )
383        };
384        // 0:00 rises (+7), 0:30 falls (-3); the doji at 5:00 closes below 10, so it falls (-5).
385        let half_minutes = [
386            candle(0, 10.0, 11.0, 7.0),
387            candle(30, 11.0, 10.0, 3.0),
388            candle(300, 9.0, 9.0, 5.0),
389        ];
390        update(&mut study, &bars, Timeframe::Sec30, &half_minutes);
391        for (i, want) in [
392            [Some(7.0), Some(-3.0), Some(4.0)],
393            [Some(0.0), Some(-5.0), Some(-5.0)],
394        ]
395        .into_iter()
396        .enumerate()
397        {
398            for (line, expected) in want.into_iter().enumerate() {
399                assert_eq!(
400                    study.values().value(line, i),
401                    expected,
402                    "bar {i}, line {line}"
403                );
404            }
405        }
406        // The automatic interval's candles no longer answer the request.
407        update(&mut study, &bars, Timeframe::Min1, &corrected);
408        assert_eq!(study.values().value(2, 0), None);
409        // A chosen daily interval on weekly bars west of UTC: candles stamped 00:00 UTC belong to
410        // the week of their date. Weeks open at local Monday midnight, 05:00 UTC on 5 and 12
411        // January 1970; Sunday the 11th falls (-4), Monday the 12th rises (+10).
412        study.set_bar_clock(
413            &BarClock::new(Timeframe::Week1)
414                .with_timezone(crate::model::TimezoneId::fixed_offset(-5 * 3_600).unwrap()),
415        );
416        study
417            .set_input(
418                "lower_interval",
419                InputValue::Timeframe(Some(Timeframe::Day1)),
420            )
421            .unwrap();
422        assert_eq!(requested_interval(&study), Some(Timeframe::Day1));
423        let weeks = [4, 11].map(|d| candle(d * 86_400 + 5 * 3_600, 1.0, 1.0, 999.0));
424        let days = [
425            candle(10 * 86_400, 2.0, 1.0, 4.0),
426            candle(11 * 86_400, 1.0, 2.0, 10.0),
427        ];
428        update(&mut study, &weeks, Timeframe::Day1, &days);
429        for (i, want) in [[0.0, -4.0, -4.0], [10.0, 0.0, 10.0]]
430            .into_iter()
431            .enumerate()
432        {
433            for (line, expected) in want.into_iter().enumerate() {
434                assert_eq!(
435                    study.values().value(line, i),
436                    Some(expected),
437                    "week {i}, line {line}"
438                );
439            }
440        }
441        #[cfg(feature = "widget")]
442        {
443            use crate::model::{BarData, Instrument};
444            use crate::widget::{Chart, test_frames::frame};
445            let mut chart = Chart::new(BarData::from_bars(bars.clone()));
446            chart.set_timeframe(Timeframe::Min5);
447            chart.set_symbol("OLD");
448            chart.set_instrument(Some(Instrument::new("OLD")));
449            chart.add_indicator(Box::new(UpDownVolume::new()));
450            chart.set_symbol("NEW");
451            chart.set_instrument(Some(Instrument::new("NEW")));
452            let request = chart.study_data_requests().pop().unwrap();
453            assert_eq!(request.symbol, "NEW");
454            chart.set_study_data(request, vec![bars[0].clone()], Some(Instrument::new("NEW")));
455            frame(&egui::Context::default(), &mut chart, 0.0, Vec::new());
456            assert_eq!(
457                chart.indicators().next().unwrap().1.values().value(0, 0),
458                Some(999.0)
459            );
460        }
461    }
462}