egui_charts/studies/builtin/
anchor_period.rs1use super::vwap::trading_day;
4use crate::model::{Bar, Instrument, SessionWindow, TimezoneId};
5use chrono::{Datelike, NaiveDate};
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum AnchorPeriod {
11 #[default]
13 Session,
14 Week,
16 Month,
18 Quarter,
20 Year,
22 Decade,
24 Century,
26}
27
28impl AnchorPeriod {
29 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#[derive(Clone, Default, PartialEq)]
46pub(super) struct ExchangeDays(Option<(TimezoneId, Vec<SessionWindow>)>);
47
48impl ExchangeDays {
49 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 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 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 #[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 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}