Skip to main content

egui_charts/studies/builtin/
anchor_period.rs

1//! Calendar periods of exchange trading days, at whose start anchored studies restart.
2
3use super::vwap::trading_day;
4use crate::model::{Bar, Instrument, SessionWindow, TimezoneId};
5use chrono::{Datelike, NaiveDate};
6
7/// A calendar period of trading days.
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum AnchorPeriod {
11    /// Each trading day.
12    #[default]
13    Session,
14    /// Each ISO week, Monday to Sunday.
15    Week,
16    /// Each calendar month.
17    Month,
18    /// Each calendar quarter: January to March, April to June, July to September, October to December.
19    Quarter,
20    /// Each calendar year.
21    Year,
22    /// Each decade, 2020 to 2029 for example.
23    Decade,
24    /// Each century, 2000 to 2099 for example.
25    Century,
26}
27
28impl AnchorPeriod {
29    /// Identifies the period that holds trading day `day`.
30    pub(super) fn key(self, day: NaiveDate) -> (i32, u32) {
31        match self {
32            Self::Session => (day.year(), day.ordinal()),
33            Self::Week => (day.iso_week().year(), day.iso_week().week()),
34            Self::Month => (day.year(), day.month()),
35            Self::Quarter => (day.year(), (day.month() - 1) / 3),
36            Self::Year => (day.year(), 0),
37            Self::Decade => (day.year().div_euclid(10), 0),
38            Self::Century => (day.year().div_euclid(100), 0),
39        }
40    }
41}
42
43/// The exchange time zone and trading session supplied by the chart instrument; UTC
44/// without session windows until one is supplied.
45#[derive(Clone, Default, PartialEq)]
46pub(super) struct ExchangeDays(Option<(TimezoneId, Vec<SessionWindow>)>);
47
48impl ExchangeDays {
49    /// Takes the instrument's time zone and session. Returns whether they changed.
50    pub(super) fn set(&mut self, instrument: Option<&Instrument>) -> bool {
51        let next = instrument.map(|instrument| {
52            (
53                instrument.time_zone.clone().unwrap_or_else(TimezoneId::utc),
54                instrument
55                    .session
56                    .as_ref()
57                    .map_or_else(Vec::new, |s| s.windows.clone()),
58            )
59        });
60        let changed = self.0 != next;
61        self.0 = next;
62        changed
63    }
64
65    /// The trading day of each bar, counted as `VolumeWeightedAvgPrice` counts them: the UTC
66    /// date when no two consecutive bars share one (bars of a day or longer), otherwise the
67    /// exchange date, a session window that spans midnight opening the next trading day.
68    pub(super) fn of(&self, data: &[Bar]) -> Vec<NaiveDate> {
69        let daily = data
70            .windows(2)
71            .all(|pair| pair[0].time.date_naive() != pair[1].time.date_naive());
72        data.iter().map(|bar| self.day(bar, daily)).collect()
73    }
74
75    /// Trading day with the caller's already established daily/intraday classification.
76    pub(super) fn day(&self, bar: &Bar, daily: bool) -> NaiveDate {
77        if daily {
78            return bar.time.date_naive();
79        }
80        let (zone, windows) = self.0.as_ref().map_or(
81            (crate::model::TimezoneId::utc(), &[][..]),
82            |(zone, windows)| (zone.clone(), windows.as_slice()),
83        );
84        trading_day(bar.time, &zone, windows)
85    }
86}
87
88#[cfg(all(test, feature = "timezones"))]
89mod tests {
90    use super::*;
91    use crate::model::Session;
92    use chrono::{NaiveTime, Weekday};
93
94    /// The instrument's zone and session move intraday bars to the exchange trading day,
95    /// while bars a day or longer keep their UTC date.
96    #[test]
97    fn trading_days_follow_the_instrument_session() {
98        let hour = |h| NaiveTime::from_hms_opt(h, 0, 0).unwrap();
99        let chicago = Instrument::new("CHICAGO")
100            .with_time_zone(TimezoneId::named("America/Chicago").unwrap())
101            .with_session(Session::new([SessionWindow::new(
102                [Weekday::Mon, Weekday::Tue],
103                hour(17),
104                hour(16),
105            )]));
106        let new_york = Instrument::new("NEW YORK")
107            .with_time_zone(TimezoneId::named("America/New_York").unwrap());
108        let bars =
109            |times: [&str; 2]| times.map(|t| Bar::new(t.parse().unwrap(), 1.0, 1.0, 1.0, 1.0, 1.0));
110        let date = |d: &str| d.parse::<NaiveDate>().unwrap();
111        // 15:00 and 17:00 in Chicago on Monday 2 March 2026; daily bars at 00:00 UTC.
112        let intraday = bars(["2026-03-02T21:00:00Z", "2026-03-02T23:00:00Z"]);
113        let daily = bars(["2024-01-04T00:00:00Z", "2024-01-05T00:00:00Z"]);
114        for (instrument, data, want) in [
115            (None, &intraday, ["2026-03-02", "2026-03-02"]),
116            (Some(&chicago), &intraday, ["2026-03-02", "2026-03-03"]),
117            (Some(&new_york), &daily, ["2024-01-04", "2024-01-05"]),
118        ] {
119            let mut days = ExchangeDays::default();
120            assert_eq!(days.set(instrument), instrument.is_some());
121            assert_eq!(
122                days.of(data),
123                want.map(date),
124                "{:?}",
125                instrument.map(|i| &i.symbol)
126            );
127        }
128    }
129}