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, Copy, Debug, PartialEq, Eq)]
73pub struct ExtendedSession {
74 pub name: &'static str,
75 pub session: Session,
76}
77
78impl ExtendedSession {
79 pub const fn new(name: &'static str, session: Session) -> Self {
80 Self { name, session }
81 }
82}
83
84#[derive(Clone, Debug)]
86pub struct TradingHours {
87 pub sessions: Vec<Session>,
88 pub extended_sessions: Vec<ExtendedSession>,
89 pub timezone: Tz,
90}
91
92impl TradingHours {
93 pub fn new(open: NaiveTime, close: NaiveTime, timezone: Tz) -> Self {
95 Self {
96 sessions: vec![Session::regular(open, close)],
97 extended_sessions: Vec::new(),
98 timezone,
99 }
100 }
101
102 pub fn from_sessions(sessions: Vec<Session>, timezone: Tz) -> Self {
103 Self {
104 sessions,
105 extended_sessions: Vec::new(),
106 timezone,
107 }
108 }
109
110 pub fn with_extended_sessions(mut self, extended_sessions: Vec<ExtendedSession>) -> Self {
111 self.extended_sessions = extended_sessions;
112 self
113 }
114
115 pub fn forex_24x5() -> Self {
117 Self::from_sessions(
118 vec![Session::overnight(
119 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
120 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
121 )],
122 chrono_tz::America::New_York,
123 )
124 }
125
126 pub fn crypto_24x7() -> Self {
128 Self::from_sessions(
129 vec![Session {
130 open: NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
131 open_day_offset: 0,
132 close: NaiveTime::from_hms_opt(23, 59, 59).unwrap(),
133 close_day_offset: 0,
134 }],
135 chrono_tz::UTC,
136 )
137 }
138
139 pub fn contains_local_time(&self, instant: DateTime<Utc>) -> bool {
143 let local_today = instant.with_timezone(&self.timezone).date_naive();
144 for delta in [-1i64, 0, 1] {
145 let day = local_today + Duration::days(delta);
146 for s in &self.sessions {
147 if let Some((o, c)) = s.instants(self.timezone, day) {
148 if instant >= o && instant < c {
149 return true;
150 }
151 }
152 }
153 }
154 false
155 }
156
157 pub fn open_at(&self, year: i32, month: u32, day: u32) -> Option<DateTime<Utc>> {
159 let nd = NaiveDate::from_ymd_opt(year, month, day)?;
160 self.sessions
161 .first()
162 .and_then(|s| s.instants(self.timezone, nd).map(|(o, _)| o))
163 }
164
165 pub fn close_at(&self, year: i32, month: u32, day: u32) -> Option<DateTime<Utc>> {
167 let nd = NaiveDate::from_ymd_opt(year, month, day)?;
168 self.sessions
169 .last()
170 .and_then(|s| s.instants(self.timezone, nd).map(|(_, c)| c))
171 }
172}
173
174pub fn parse_hhmm(s: &str) -> Option<NaiveTime> {
176 NaiveTime::parse_from_str(s, "%H:%M").ok()
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use chrono::TimeZone;
183 use chrono_tz::America::{Chicago, New_York};
184
185 #[test]
186 fn nyse_contains_local() {
187 let th = TradingHours::new(
188 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
189 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
190 New_York,
191 );
192 let inst = New_York
193 .with_ymd_and_hms(2024, 1, 8, 9, 30, 0)
194 .unwrap()
195 .with_timezone(&Utc);
196 assert!(th.contains_local_time(inst));
197 let before = inst - Duration::minutes(1);
198 assert!(!th.contains_local_time(before));
199 }
200
201 #[test]
202 fn cme_equity_futures_overnight_open() {
203 let th = TradingHours::from_sessions(
204 vec![Session::overnight(
205 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
206 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
207 )],
208 Chicago,
209 );
210 let inst = Chicago
212 .with_ymd_and_hms(2024, 1, 7, 18, 0, 0)
213 .unwrap()
214 .with_timezone(&Utc);
215 assert!(th.contains_local_time(inst));
216 let inst2 = Chicago
218 .with_ymd_and_hms(2024, 1, 8, 16, 30, 0)
219 .unwrap()
220 .with_timezone(&Utc);
221 assert!(!th.contains_local_time(inst2));
222 }
223
224 #[test]
225 fn forex_continuous_24x5() {
226 let th = TradingHours::forex_24x5();
227 let inst = New_York
229 .with_ymd_and_hms(2024, 1, 9, 3, 0, 0)
230 .unwrap()
231 .with_timezone(&Utc);
232 assert!(th.contains_local_time(inst));
233 }
236}