Skip to main content

egui_charts/studies/builtin/
volume_delta.rs

1//! Volume delta candles from lower-interval candles supplied by the host.
2//!
3//! Each candle inside a chart bar adds its volume to the bar's delta when it closes above its
4//! open and subtracts it when it closes below; one that closes at its open adds it unless it
5//! closes below the previous candle, as [`UpDownVolume`](super::UpDownVolume) classifies them.
6//! [`VolumeDelta`] draws each bar as a candle that opens at zero, closes at the bar's delta and
7//! spans the lowest and highest running delta after each of its candles.
8//! [`CumulativeVolumeDelta`] adds the bar deltas from the first bar of each anchor period, the
9//! session by default, and draws each bar from the total at its open to the total at its close.
10//! A bar without candles has no value and no candle, and the total carries over it.
11//!
12//! [`Indicator::candles`] holds one candle per bar with a value, on plot 1 ("Up") when it closes
13//! at or above its open and on plot 2 ("Down") otherwise. Output 0 ("Delta") holds every close,
14//! for studies that take this one as their source, and is not drawn; outputs 1 and 2 hold the
15//! close in the column of its candle's plot.
16
17use super::anchor_period::AnchorPeriod;
18use super::lower_interval;
19use crate::model::{Bar, BarClock, LineStyle, Timeframe};
20use crate::scales::StudyFormat;
21use crate::studies::palette;
22use crate::studies::{
23    Indicator, IndicatorValue, IndicatorValues, InputKind, InputValue, PlotStyle, PlotType,
24    StudyCandle, StudyData, StudyDataRequest, StudyInput,
25};
26use egui::Color32;
27
28const PERIODS: [AnchorPeriod; 7] = [
29    AnchorPeriod::Session,
30    AnchorPeriod::Week,
31    AnchorPeriod::Month,
32    AnchorPeriod::Quarter,
33    AnchorPeriod::Year,
34    AnchorPeriod::Decade,
35    AnchorPeriod::Century,
36];
37
38#[derive(Clone)]
39struct DeltaCandles {
40    clock: BarClock,
41    /// The interval chosen in the Lower interval input; `None` picks one from the chart interval.
42    lower_interval: Option<Timeframe>,
43    /// The period from whose first bar the deltas add up; `None` starts every bar at zero.
44    anchor: Option<AnchorPeriod>,
45    candles: Vec<StudyCandle>,
46    values: IndicatorValues,
47    colors: [Color32; 3],
48    visible: bool,
49}
50
51impl DeltaCandles {
52    fn new(anchor: Option<AnchorPeriod>) -> Self {
53        Self {
54            clock: BarClock::new(Timeframe::Min1),
55            lower_interval: None,
56            anchor,
57            candles: Vec::new(),
58            values: IndicatorValues::new(),
59            colors: [palette::BLUE, palette::UP, palette::DOWN],
60            visible: true,
61        }
62    }
63
64    fn lower(&self) -> Option<Timeframe> {
65        lower_interval::lower_timeframe(&self.clock, self.lower_interval)
66    }
67
68    fn inputs(&self) -> Vec<StudyInput> {
69        let mut inputs = vec![lower_interval::input(self.lower_interval)];
70        if let Some(anchor) = self.anchor {
71            inputs.push(
72                StudyInput::new(
73                    "anchor",
74                    "Anchor period",
75                    InputValue::Choice(PERIODS.iter().position(|p| *p == anchor).unwrap_or(0)),
76                    InputKind::Choice(PERIODS.iter().map(|p| format!("{p:?}")).collect()),
77                )
78                .with_default(InputValue::Choice(0)),
79            );
80        }
81        inputs
82    }
83
84    fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
85        match (id, value) {
86            ("lower_interval", InputValue::Timeframe(chosen)) => self.lower_interval = chosen,
87            ("anchor", InputValue::Choice(at)) if self.anchor.is_some() && at < PERIODS.len() => {
88                self.anchor = Some(PERIODS[at]);
89            }
90            _ => return Err(crate::studies::StudyError::rejected(&self.inputs(), id)),
91        }
92        self.refresh(&[], 0, &[]);
93        Ok(())
94    }
95
96    fn update_data(&mut self, data: &[Bar], from: usize, sources: &[StudyData<'_>]) {
97        let from = from.min(self.values.len()).min(data.len());
98        let candles = match self.lower() {
99            Some(lower) => {
100                match lower_interval::reply(&lower_interval::request(lower), from, sources) {
101                    Some(bars) => bars,
102                    None => return,
103                }
104            }
105            None => &[],
106        };
107        self.refresh(data, from, candles);
108    }
109
110    /// Redraws the bars of `data[from..]` from `candles`; without candles no bar has a value.
111    fn refresh(&mut self, data: &[Bar], from: usize, candles: &[Bar]) {
112        let from = if candles.is_empty() {
113            0
114        } else {
115            from.min(self.values.len()).min(data.len())
116        };
117        self.candles.truncate(
118            self.candles
119                .partition_point(|candle| candle.bars.start < from),
120        );
121        if from == 0 {
122            self.values = IndicatorValues::missing(data.len());
123        } else {
124            self.values.truncate(from);
125            self.values.resize(data.len(), IndicatorValue::None);
126        }
127        let Some(lower) = self.lower().filter(|_| !candles.is_empty()) else {
128            return;
129        };
130        let sessions = self.clock.sessions();
131        let anchor = self.anchor;
132        let period = |bar: &Bar| anchor.map(|anchor| anchor.key(sessions.trading_date(bar.time)));
133        let mut last = self
134            .candles
135            .last()
136            .map(|candle| (period(&data[candle.bars.start]), candle.bar.close));
137        let ranges = lower_interval::nest(&self.clock, lower, data, candles, from);
138        for (i, range) in (from..).zip(ranges) {
139            let bar = &data[i];
140            if range.is_empty() || !bar.close.is_finite() {
141                continue;
142            }
143            let flow = lower_interval::flow(candles, range);
144            let key = period(bar);
145            let open = match last {
146                Some((at, total)) if anchor.is_some() && at == key => total,
147                _ => 0.0,
148            };
149            let (high, low, close) = (open + flow.high, open + flow.low, open + flow.delta);
150            if ![high, low, close].iter().all(|value| value.is_finite()) {
151                continue;
152            }
153            let down = close < open;
154            self.values.set(
155                i,
156                IndicatorValue::Multiple(if down {
157                    vec![close, f64::NAN, close]
158                } else {
159                    vec![close, close, f64::NAN]
160                }),
161            );
162            self.candles.push(StudyCandle {
163                bars: i..i + 1,
164                bar: Bar::new(bar.time, open, high, low, close, flow.up - flow.down),
165                plot: 1 + usize::from(down),
166            });
167            last = Some((key, close));
168        }
169    }
170}
171
172macro_rules! delta_study {
173    ($(#[$meta:meta])* $ty:ident, $name:literal, $desc:literal, $anchor:expr) => {
174        $(#[$meta])*
175        #[derive(Clone)]
176        pub struct $ty {
177            core: DeltaCandles,
178        }
179
180        impl $ty {
181            /// One-minute candles on a one-minute chart until the chart supplies its interval.
182            /// Hosts answer [`Indicator::data_requests`] through the chart or `update_data`.
183            pub fn new() -> Self {
184                Self {
185                    core: DeltaCandles::new($anchor),
186                }
187            }
188        }
189
190        impl Default for $ty {
191            fn default() -> Self {
192                Self::new()
193            }
194        }
195
196        impl Indicator for $ty {
197            fn name(&self) -> &str {
198                $name
199            }
200            fn desc(&self) -> &str {
201                $desc
202            }
203            fn inputs(&self) -> Vec<StudyInput> {
204                self.core.inputs()
205            }
206            fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
207                self.core.set_input(id, value)
208            }
209            fn data_requests(&self) -> Vec<StudyDataRequest> {
210                self.core
211                    .lower()
212                    .map(lower_interval::request)
213                    .into_iter()
214                    .collect()
215            }
216            fn update_data(&mut self, data: &[Bar], from: usize, sources: &[StudyData<'_>]) {
217                self.core.update_data(data, from, sources);
218            }
219            fn set_timeframe(&mut self, timeframe: Timeframe) {
220                self.set_bar_clock(&self.core.clock.for_timeframe(timeframe));
221            }
222            fn set_bar_clock(&mut self, clock: &BarClock) -> bool {
223                let changed = self.core.clock != *clock;
224                self.core.clock = clock.clone();
225                changed
226            }
227            fn calculate(&mut self, data: &[Bar]) {
228                self.core.refresh(data, 0, &[]);
229            }
230            fn is_incremental(&self) -> bool {
231                true
232            }
233            fn is_overlay(&self) -> bool {
234                false
235            }
236            fn value_format(&self) -> StudyFormat {
237                StudyFormat::Volume
238            }
239            fn plot_style(&self, line: usize) -> PlotStyle {
240                PlotStyle {
241                    color: self.core.colors.get(line).copied().unwrap_or(Color32::WHITE),
242                    width: self.line_style(line).width,
243                    pattern: LineStyle::Solid,
244                    plot_type: PlotType::Line,
245                    visible: line != 0,
246                }
247            }
248            fn values(&self) -> &IndicatorValues {
249                &self.core.values
250            }
251            fn candles(&self) -> &[StudyCandle] {
252                &self.core.candles
253            }
254            fn colors(&self) -> Vec<Color32> {
255                self.core.colors.to_vec()
256            }
257            fn set_colors(&mut self, colors: Vec<Color32>) {
258                for (old, new) in self.core.colors.iter_mut().zip(colors) {
259                    *old = new;
260                }
261            }
262            fn is_visible(&self) -> bool {
263                self.core.visible
264            }
265            fn set_visible(&mut self, visible: bool) {
266                self.core.visible = visible;
267            }
268            fn clone_box(&self) -> Box<dyn Indicator> {
269                Box::new(self.clone())
270            }
271            fn line_names(&self) -> Vec<String> {
272                ["Delta", "Up", "Down"].map(String::from).to_vec()
273            }
274        }
275    };
276}
277
278delta_study!(
279    /// Each bar's volume delta from lower-interval candles, as a candle from zero, in a separate
280    /// pane.
281    VolumeDelta,
282    "Volume Delta",
283    "Volume Delta - Up minus down volume of the lower-interval candles in each bar, as a candle from zero",
284    None
285);
286delta_study!(
287    /// Volume delta from lower-interval candles added up from the start of each anchor period,
288    /// as candles, in a separate pane.
289    CumulativeVolumeDelta,
290    "Cumulative Volume Delta",
291    "Cumulative Volume Delta - Volume delta of lower-interval candles added up over each anchor period",
292    Some(AnchorPeriod::Session)
293);
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use crate::studies::IndicatorFactory;
299    use chrono::{TimeZone, Utc};
300
301    /// Volume Delta draws each bar from zero along the running delta of its candles; Cumulative
302    /// Volume Delta carries the total across bars and gaps, restarts at each anchor period and
303    /// resumes it when the last bars are recomputed.
304    #[test]
305    fn delta_candles_follow_the_running_delta_of_lower_interval_candles() {
306        let factory = IndicatorFactory::new();
307        for name in ["Volume Delta", "Cumulative Volume Delta"] {
308            assert_eq!(factory.category(name), Some("Volume"), "{name}");
309        }
310        let bar = |s: i64, o: f64, c: f64, v: f64| {
311            Bar::new(
312                Utc.timestamp_opt(s, 0).unwrap(),
313                o,
314                o.max(c),
315                o.min(c),
316                c,
317                v,
318            )
319        };
320        let update = |study: &mut dyn Indicator, data: &[Bar], from: usize, candles: &[Bar]| {
321            study.update_data(
322                data,
323                from,
324                &[StudyData {
325                    request: StudyDataRequest {
326                        metric: None,
327                        symbol: String::new(),
328                        timeframe: Some(Timeframe::Min1),
329                    },
330                    bars: candles,
331                    clock: BarClock::new(Timeframe::Min1),
332                    instrument: None,
333                    timeframe: Timeframe::Min1,
334                }],
335            );
336        };
337        let drawn = |study: &dyn Indicator| -> Vec<(usize, usize, [f64; 4])> {
338            study
339                .candles()
340                .iter()
341                .map(|c| {
342                    (
343                        c.bars.start,
344                        c.plot,
345                        [c.bar.open, c.bar.high, c.bar.low, c.bar.close],
346                    )
347                })
348                .collect()
349        };
350
351        // Five-minute bars; the third has no candles.
352        let chart = [0, 300, 600, 900].map(|s| bar(s, 1.0, 1.0, 999.0));
353        let candles = [
354            bar(0, 10.0, 11.0, 5.0),   // +5
355            bar(60, 11.0, 10.0, 8.0),  // -8, running -3
356            bar(120, 10.0, 10.0, 5.0), // doji at the previous close: +5, running 2
357            bar(300, 10.0, 9.0, 4.0),  // -4
358            bar(360, 9.0, 9.0, 1.0),   // doji at the previous close: +1, running -3
359            bar(900, 9.0, 8.0, 0.0),   // no volume
360        ];
361        let mut delta = VolumeDelta::new();
362        delta.set_bar_clock(&BarClock::new(Timeframe::Min5));
363        assert_eq!(delta.data_requests()[0].timeframe, Some(Timeframe::Min1));
364        update(&mut delta, &chart, 0, &candles);
365        assert_eq!(
366            drawn(&delta),
367            [
368                (0, 1, [0.0, 5.0, -3.0, 2.0]),
369                (1, 2, [0.0, 0.0, -4.0, -3.0]),
370                (3, 1, [0.0; 4]),
371            ]
372        );
373        // The Delta output has every close; Up and Down only those of their candles.
374        let values = delta.values();
375        assert_eq!(
376            (0..4).map(|i| values.value(0, i)).collect::<Vec<_>>(),
377            [Some(2.0), Some(-3.0), None, Some(0.0)]
378        );
379        assert_eq!(
380            [
381                values.value(1, 0),
382                values.value(2, 0),
383                values.value(2, 1),
384                values.value(1, 1)
385            ],
386            [Some(2.0), None, Some(-3.0), None]
387        );
388
389        // Hourly bars from 22:00 on 1 January 1970 to 03:00 on the 2nd; 02:00 has no candles.
390        let chart =
391            [79_200, 82_800, 86_400, 90_000, 93_600, 97_200].map(|s| bar(s, 1.0, 1.0, 999.0));
392        let mut candles = vec![
393            bar(79_200, 1.0, 2.0, 10.0), // 22:00 +10
394            bar(79_260, 2.0, 1.0, 4.0),  // -4: bar delta 6, high 10
395            bar(82_800, 1.0, 1.0, 3.0),  // 23:00 doji at the previous close: +3
396            bar(82_860, 1.0, 0.0, 5.0),  // -5: bar delta -2, high 3, low -2
397            bar(86_400, 0.0, 2.0, 7.0),  // 00:00 +7
398            bar(90_000, 2.0, 1.0, 2.0),  // 01:00 -2
399            bar(97_200, 1.0, 2.0, 1.0),  // 03:00 +1
400        ];
401        let mut cumulative = CumulativeVolumeDelta::new();
402        cumulative.set_bar_clock(&BarClock::new(Timeframe::Hour1));
403        update(&mut cumulative, &chart, 0, &candles);
404        // The session total restarts at midnight and carries over the empty 02:00 bar.
405        assert_eq!(
406            drawn(&cumulative),
407            [
408                (0, 1, [0.0, 10.0, 0.0, 6.0]),
409                (1, 2, [6.0, 9.0, 4.0, 4.0]),
410                (2, 1, [0.0, 7.0, 0.0, 7.0]),
411                (3, 2, [7.0, 7.0, 5.0, 5.0]),
412                (5, 1, [5.0, 6.0, 5.0, 6.0]),
413            ]
414        );
415        // A live candle at 01:01 (+4) is recomputed from bar 3 on the total before it.
416        candles.insert(6, bar(90_060, 1.0, 3.0, 4.0));
417        update(&mut cumulative, &chart, 3, &candles);
418        assert_eq!(
419            drawn(&cumulative)[2..],
420            [
421                (2, 1, [0.0, 7.0, 0.0, 7.0]),
422                (3, 1, [7.0, 9.0, 5.0, 9.0]),
423                (5, 1, [9.0, 10.0, 9.0, 10.0]),
424            ]
425        );
426        // 1 and 2 January 1970 are in one ISO week, so a weekly total does not restart.
427        cumulative
428            .set_input("anchor", InputValue::Choice(1))
429            .unwrap();
430        update(&mut cumulative, &chart, 0, &candles);
431        assert_eq!(
432            drawn(&cumulative)[2..],
433            [
434                (2, 1, [4.0, 11.0, 4.0, 11.0]),
435                (3, 1, [11.0, 13.0, 9.0, 13.0]),
436                (5, 1, [13.0, 14.0, 13.0, 14.0]),
437            ]
438        );
439        assert_eq!(cumulative.values().value(0, 5), Some(14.0));
440    }
441}