1use chrono::{DateTime, Duration, NaiveDate, NaiveTime, TimeZone, Utc};
14use chrono_tz::Tz;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct Session {
25 pub open: NaiveTime,
26 pub open_day_offset: i32,
27 pub close: NaiveTime,
28 pub close_day_offset: i32,
29}
30
31impl Session {
32 pub const fn regular(open: NaiveTime, close: NaiveTime) -> Self {
33 Self {
34 open,
35 open_day_offset: 0,
36 close,
37 close_day_offset: 0,
38 }
39 }
40
41 pub const fn overnight(open: NaiveTime, close: NaiveTime) -> Self {
44 Self {
45 open,
46 open_day_offset: -1,
47 close,
48 close_day_offset: 0,
49 }
50 }
51
52 pub fn instants(
55 &self,
56 tz: Tz,
57 trading_day: NaiveDate,
58 ) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
59 let open_local = trading_day + Duration::days(self.open_day_offset as i64);
60 let close_local = trading_day + Duration::days(self.close_day_offset as i64);
61 let open = tz
62 .from_local_datetime(&open_local.and_time(self.open))
63 .single()?;
64 let close = tz
65 .from_local_datetime(&close_local.and_time(self.close))
66 .single()?;
67 Some((open.with_timezone(&Utc), close.with_timezone(&Utc)))
68 }
69}
70
71#[derive(Clone, Debug)]
73pub struct TradingHours {
74 pub sessions: Vec<Session>,
75 pub timezone: Tz,
76}
77
78impl TradingHours {
79 pub fn new(open: NaiveTime, close: NaiveTime, timezone: Tz) -> Self {
81 Self {
82 sessions: vec![Session::regular(open, close)],
83 timezone,
84 }
85 }
86
87 pub fn from_sessions(sessions: Vec<Session>, timezone: Tz) -> Self {
88 Self { sessions, timezone }
89 }
90
91 pub fn forex_24x5() -> Self {
93 Self::from_sessions(
94 vec![Session::overnight(
95 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
96 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
97 )],
98 chrono_tz::America::New_York,
99 )
100 }
101
102 pub fn crypto_24x7() -> Self {
104 Self::from_sessions(
105 vec![Session {
106 open: NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
107 open_day_offset: 0,
108 close: NaiveTime::from_hms_opt(23, 59, 59).unwrap(),
109 close_day_offset: 0,
110 }],
111 chrono_tz::UTC,
112 )
113 }
114
115 pub fn contains_local_time(&self, instant: DateTime<Utc>) -> bool {
119 let local_today = instant.with_timezone(&self.timezone).date_naive();
120 for delta in [-1i64, 0, 1] {
121 let day = local_today + Duration::days(delta);
122 for s in &self.sessions {
123 if let Some((o, c)) = s.instants(self.timezone, day) {
124 if instant >= o && instant < c {
125 return true;
126 }
127 }
128 }
129 }
130 false
131 }
132
133 pub fn open_at(&self, year: i32, month: u32, day: u32) -> Option<DateTime<Utc>> {
135 let nd = NaiveDate::from_ymd_opt(year, month, day)?;
136 self.sessions
137 .first()
138 .and_then(|s| s.instants(self.timezone, nd).map(|(o, _)| o))
139 }
140
141 pub fn close_at(&self, year: i32, month: u32, day: u32) -> Option<DateTime<Utc>> {
143 let nd = NaiveDate::from_ymd_opt(year, month, day)?;
144 self.sessions
145 .last()
146 .and_then(|s| s.instants(self.timezone, nd).map(|(_, c)| c))
147 }
148}
149
150pub fn parse_hhmm(s: &str) -> Option<NaiveTime> {
152 NaiveTime::parse_from_str(s, "%H:%M").ok()
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use chrono::TimeZone;
159 use chrono_tz::America::{Chicago, New_York};
160
161 #[test]
162 fn nyse_contains_local() {
163 let th = TradingHours::new(
164 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
165 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
166 New_York,
167 );
168 let inst = New_York
169 .with_ymd_and_hms(2024, 1, 8, 9, 30, 0)
170 .unwrap()
171 .with_timezone(&Utc);
172 assert!(th.contains_local_time(inst));
173 let before = inst - Duration::minutes(1);
174 assert!(!th.contains_local_time(before));
175 }
176
177 #[test]
178 fn cme_equity_futures_overnight_open() {
179 let th = TradingHours::from_sessions(
180 vec![Session::overnight(
181 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
182 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
183 )],
184 Chicago,
185 );
186 let inst = Chicago
188 .with_ymd_and_hms(2024, 1, 7, 18, 0, 0)
189 .unwrap()
190 .with_timezone(&Utc);
191 assert!(th.contains_local_time(inst));
192 let inst2 = Chicago
194 .with_ymd_and_hms(2024, 1, 8, 16, 30, 0)
195 .unwrap()
196 .with_timezone(&Utc);
197 assert!(!th.contains_local_time(inst2));
198 }
199
200 #[test]
201 fn forex_continuous_24x5() {
202 let th = TradingHours::forex_24x5();
203 let inst = New_York
205 .with_ymd_and_hms(2024, 1, 9, 3, 0, 0)
206 .unwrap()
207 .with_timezone(&Utc);
208 assert!(th.contains_local_time(inst));
209 }
212}