Skip to main content

egui_charts/studies/builtin/
relative_volume_at_time.rs

1//! Relative Volume at Time
2//!
3//! Compares the volume traded so far in the current period, by default the trading day, with
4//! the average volume traded by the same point of the previous periods. A reading of 2 at
5//! 10:30 means twice the usual volume by 10:30, so unusual activity shows early in the day
6//! instead of once the day is complete.
7//!
8//! # Calculation
9//! For a bar at offset `d` into its period, with `n` the length:
10//!
11//! - current = the volume from the start of the period through the bar (Cumulative mode,
12//!   the default), or the bar's own volume (Regular mode)
13//! - past_k = the same quantity at the last bar of the k-th previous period whose offset is
14//!   at most `d`, or 0 when that period has no bar by then
15//! - RVOL = current / ((past_1 + ... + past_n) / n)
16//!
17//! The offset of a bar is the time elapsed since the open of its trading day, plus one day for
18//! each calendar day from the first day of the period (the Monday of a week, the 1st of a
19//! month) to that trading day, so a week's Tuesday 10:30 lines up with every other Tuesday
20//! 10:30. Trading days follow the instrument's time zone and session, as for the VWAP. Bars of
21//! a day or longer are dated by their UTC date and have no intraday part, so with the day
22//! anchor every daily bar is a period of its own and the study compares each bar's volume with
23//! the average of the previous `n` bars. There is no value before `n` previous periods, or when
24//! their average is zero.
25//!
26//! # Example
27//! ```
28//! use chrono::{TimeZone, Utc};
29//! use egui_charts::model::Bar;
30//! use egui_charts::studies::{Indicator, RelativeVolumeAtTime};
31//!
32//! // Two hourly bars a day; the third day trades twice the volume of the first two.
33//! let bars: Vec<Bar> = [(0, 100.0), (1, 300.0), (24, 100.0), (25, 300.0), (48, 200.0), (49, 600.0)]
34//!     .into_iter()
35//!     .map(|(h, v)| Bar::new(Utc.timestamp_opt(h * 3_600, 0).unwrap(), 1.0, 1.0, 1.0, 1.0, v))
36//!     .collect();
37//! let mut study = RelativeVolumeAtTime::new(2);
38//! study.calculate(&bars);
39//! assert_eq!(study.values().value(0, 3), None);
40//! assert_eq!(study.values().value(0, 4), Some(2.0));
41//! assert_eq!(study.values().value(0, 5), Some(2.0));
42//! ```
43
44use crate::model::{Bar, BarClock, ExchangeSessionProvider, Instrument, Session, TimezoneId};
45use crate::studies::palette;
46use crate::studies::stepper::Stepper;
47use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
48use chrono::{Datelike, NaiveDate};
49use egui::Color32;
50
51const DAY_MS: i64 = 86_400_000;
52
53/// The period at whose start [`RelativeVolumeAtTime`] restarts its comparison.
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub enum RelativeVolumeAnchor {
56    /// Each trading day.
57    #[default]
58    Day,
59    /// Each ISO week, Monday to Sunday, of trading days.
60    Week,
61    /// Each calendar month of trading days.
62    Month,
63}
64
65impl RelativeVolumeAnchor {
66    /// The period that holds trading day `day`, and the days from the period's first day.
67    fn locate(self, day: NaiveDate) -> ((i32, u32), u32) {
68        match self {
69            Self::Day => ((day.year(), day.ordinal()), 0),
70            Self::Week => (
71                (day.iso_week().year(), day.iso_week().week()),
72                day.weekday().num_days_from_monday(),
73            ),
74            Self::Month => ((day.year(), day.month()), day.day0()),
75        }
76    }
77}
78
79/// The volume compared by [`RelativeVolumeAtTime`].
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
81pub enum RelativeVolumeMode {
82    /// Volume accumulated since the start of the period.
83    #[default]
84    Cumulative,
85    /// The volume of the bar alone.
86    Regular,
87}
88
89/// Volume relative to its average at the same point of the previous periods, as columns
90/// around a level at 1.
91///
92/// A bar with a non-finite volume has no value and adds nothing. The chart supplies the
93/// instrument's time zone and session through [`Indicator::set_instrument`] and its interval
94/// through [`Indicator::set_bar_clock`]. Without them trading days are UTC days, and direct
95/// callers on daily bars stamped away from UTC midnight pass a daily clock.
96#[derive(Clone)]
97pub struct RelativeVolumeAtTime {
98    length: usize,
99    anchor: RelativeVolumeAnchor,
100    mode: RelativeVolumeMode,
101    instrument: Option<(TimezoneId, Option<Session>)>,
102    /// Whether the chart interval is a day or longer.
103    daily: bool,
104    values: IndicatorValues,
105    stepper: Stepper<State>,
106    periods: std::collections::VecDeque<Vec<(i64, f64)>>,
107    undo: Undo,
108    colors: [Color32; 2],
109    visible: bool,
110}
111
112#[derive(Clone, Default)]
113struct State {
114    source: usize,
115    key: Option<(i32, u32)>,
116    cumulative: f64,
117    calendar_date: Option<NaiveDate>,
118    opens: Vec<(chrono::DateTime<chrono::Utc>, NaiveDate)>,
119}
120
121#[derive(Clone, Default)]
122struct Undo {
123    appended: bool,
124    new_period: bool,
125    expired: Option<Vec<(i64, f64)>>,
126}
127
128impl RelativeVolumeAtTime {
129    /// Cumulative daily relative volume over `length` previous days (zero becomes one).
130    pub fn new(length: usize) -> Self {
131        Self {
132            length: length.max(1),
133            anchor: RelativeVolumeAnchor::Day,
134            mode: RelativeVolumeMode::Cumulative,
135            instrument: None,
136            daily: false,
137            values: IndicatorValues::new(),
138            stepper: Stepper::default(),
139            periods: std::collections::VecDeque::new(),
140            undo: Undo::default(),
141            colors: [
142                palette::GREEN.gamma_multiply_u8(77),
143                palette::DOWN.gamma_multiply_u8(77),
144            ],
145            visible: true,
146        }
147    }
148
149    /// Restarts at each selected period.
150    pub fn with_anchor(mut self, anchor: RelativeVolumeAnchor) -> Self {
151        self.anchor = anchor;
152        self
153    }
154
155    /// Compares cumulative or single-bar volume.
156    pub fn with_mode(mut self, mode: RelativeVolumeMode) -> Self {
157        self.mode = mode;
158        self
159    }
160}
161
162impl Default for RelativeVolumeAtTime {
163    fn default() -> Self {
164        Self::new(10)
165    }
166}
167
168impl Indicator for RelativeVolumeAtTime {
169    crate::studies::inputs::study_inputs!(self;
170        anchor: choice("Anchor", [RelativeVolumeAnchor::Day, RelativeVolumeAnchor::Week, RelativeVolumeAnchor::Month]),
171        length: length("Length", 1, usize::MAX),
172        mode: choice("Calculation mode", [RelativeVolumeMode::Cumulative, RelativeVolumeMode::Regular])
173    ; {});
174
175    fn name(&self) -> &str {
176        "Relative Volume at Time"
177    }
178
179    fn desc(&self) -> &str {
180        "Relative Volume at Time - Volume against its average at the same time of past periods"
181    }
182
183    fn plot_style(&self, _line: usize) -> crate::studies::PlotStyle {
184        crate::studies::PlotStyle {
185            color: self.colors[0],
186            width: 1.0,
187            pattern: crate::model::LineStyle::Solid,
188            plot_type: crate::studies::PlotType::Columns,
189            visible: true,
190        }
191    }
192
193    fn plot_color(&self, _line: usize, bar: usize) -> Option<Color32> {
194        let value = self.values.value(0, bar)?;
195        Some(self.colors[usize::from(value <= 1.0)])
196    }
197
198    fn pane(&self) -> crate::studies::StudyPane {
199        crate::studies::StudyPane {
200            levels: vec![(1.0, palette::GRAY.gamma_multiply_u8(128), "Equality")],
201            ..Default::default()
202        }
203    }
204
205    fn calculate(&mut self, data: &[Bar]) {
206        self.update(data, 0);
207    }
208
209    fn update(&mut self, data: &[Bar], from: usize) {
210        let len = self.values.len();
211        if len > 0 && from == len && data.len() == len {
212            return;
213        }
214        let start = if len > 0 && from >= len - 1 && data.len() >= len {
215            from.min(len)
216        } else {
217            0
218        };
219        if start == 0 {
220            self.periods.clear();
221        } else if start < len {
222            if self.undo.appended {
223                self.periods.back_mut().unwrap().pop();
224            }
225            if self.undo.new_period {
226                self.periods.pop_back();
227            }
228            if let Some(expired) = self.undo.expired.take() {
229                self.periods.push_front(expired);
230            }
231        }
232        self.undo = Undo::default();
233        let provider = (!self.daily).then(|| {
234            let (zone, session) = self
235                .instrument
236                .clone()
237                .unwrap_or_else(|| (TimezoneId::utc(), None));
238            ExchangeSessionProvider::new(zone, session)
239        });
240        self.stepper
241            .run(data, from, &mut self.values, |state, bar| {
242                let source = state.source;
243                state.source += 1;
244                if !bar.volume.is_finite() {
245                    return IndicatorValue::None;
246                }
247                if !self.daily && state.calendar_date != Some(bar.time.date_naive()) {
248                    state.opens = provider.as_ref().unwrap().opens([bar.time]);
249                    state.calendar_date = Some(bar.time.date_naive());
250                }
251                let (open, day) = if self.daily {
252                    (bar.time, bar.time.date_naive())
253                } else {
254                    match state.opens.partition_point(|(open, _)| *open <= bar.time) {
255                        0 => (bar.time, bar.time.date_naive()),
256                        n => state.opens[n - 1],
257                    }
258                };
259                let (period, days) = self.anchor.locate(day);
260                let offset = i64::from(days) * DAY_MS + (bar.time - open).num_milliseconds();
261                let last = source + 1 == data.len();
262                if state.key != Some(period) {
263                    state.key = Some(period);
264                    state.cumulative = 0.0;
265                    self.periods.push_back(Vec::new());
266                    let expired = if self.periods.len() > self.length.saturating_add(1) {
267                        self.periods.pop_front()
268                    } else {
269                        None
270                    };
271                    if last {
272                        self.undo.new_period = true;
273                        self.undo.expired = expired;
274                    }
275                }
276                state.cumulative += bar.volume;
277                let current = match self.mode {
278                    RelativeVolumeMode::Cumulative => state.cumulative,
279                    RelativeVolumeMode::Regular => bar.volume,
280                };
281                self.periods.back_mut().unwrap().push((offset, current));
282                if last {
283                    self.undo.appended = true;
284                }
285                if self.periods.len() <= self.length {
286                    return IndicatorValue::None;
287                }
288                let total: f64 = self
289                    .periods
290                    .iter()
291                    .take(self.length)
292                    .map(|past| match past.partition_point(|&(o, _)| o <= offset) {
293                        0 => 0.0,
294                        n => past[n - 1].1,
295                    })
296                    .sum();
297                let average = total / self.length as f64;
298                if average != 0.0 {
299                    IndicatorValue::Single(current / average)
300                } else {
301                    IndicatorValue::None
302                }
303            });
304    }
305
306    fn is_incremental(&self) -> bool {
307        true
308    }
309
310    fn set_instrument(&mut self, instrument: Option<&Instrument>) -> bool {
311        let next = instrument.map(|instrument| {
312            (
313                instrument.time_zone.clone().unwrap_or_else(TimezoneId::utc),
314                instrument.session.clone(),
315            )
316        });
317        let changed = next != self.instrument;
318        self.instrument = next;
319        changed
320    }
321
322    fn set_bar_clock(&mut self, clock: &BarClock) -> bool {
323        let daily = clock.timeframe().is_some_and(|timeframe| {
324            timeframe.duration_ms() >= crate::model::Timeframe::Day1.duration_ms()
325        });
326        let changed = daily != self.daily;
327        self.daily = daily;
328        changed
329    }
330
331    fn values(&self) -> &IndicatorValues {
332        &self.values
333    }
334
335    fn colors(&self) -> Vec<Color32> {
336        self.colors.to_vec()
337    }
338
339    fn set_colors(&mut self, colors: Vec<Color32>) {
340        for (old, new) in self.colors.iter_mut().zip(colors) {
341            *old = new;
342        }
343    }
344
345    fn is_overlay(&self) -> bool {
346        false
347    }
348
349    fn is_visible(&self) -> bool {
350        self.visible
351    }
352
353    fn set_visible(&mut self, visible: bool) {
354        self.visible = visible;
355    }
356
357    fn clone_box(&self) -> Box<dyn Indicator> {
358        Box::new(self.clone())
359    }
360
361    fn line_names(&self) -> Vec<String> {
362        vec![format!("RVOL({})", self.length)]
363    }
364}