use chrono::{DateTime, Duration, NaiveDate, NaiveTime, TimeZone, Utc};
use chrono_tz::Tz;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Session {
pub open: NaiveTime,
pub open_day_offset: i32,
pub close: NaiveTime,
pub close_day_offset: i32,
}
impl Session {
pub const fn regular(open: NaiveTime, close: NaiveTime) -> Self {
Self {
open,
open_day_offset: 0,
close,
close_day_offset: 0,
}
}
pub const fn overnight(open: NaiveTime, close: NaiveTime) -> Self {
Self {
open,
open_day_offset: -1,
close,
close_day_offset: 0,
}
}
pub fn instants(
&self,
tz: Tz,
trading_day: NaiveDate,
) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
let open_local = trading_day + Duration::days(self.open_day_offset as i64);
let close_local = trading_day + Duration::days(self.close_day_offset as i64);
let open = tz
.from_local_datetime(&open_local.and_time(self.open))
.single()?;
let close = tz
.from_local_datetime(&close_local.and_time(self.close))
.single()?;
Some((open.with_timezone(&Utc), close.with_timezone(&Utc)))
}
}
#[derive(Clone, Debug)]
pub struct TradingHours {
pub sessions: Vec<Session>,
pub timezone: Tz,
}
impl TradingHours {
pub fn new(open: NaiveTime, close: NaiveTime, timezone: Tz) -> Self {
Self {
sessions: vec![Session::regular(open, close)],
timezone,
}
}
pub fn from_sessions(sessions: Vec<Session>, timezone: Tz) -> Self {
Self { sessions, timezone }
}
pub fn forex_24x5() -> Self {
Self::from_sessions(
vec![Session::overnight(
NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
)],
chrono_tz::America::New_York,
)
}
pub fn crypto_24x7() -> Self {
Self::from_sessions(
vec![Session {
open: NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
open_day_offset: 0,
close: NaiveTime::from_hms_opt(23, 59, 59).unwrap(),
close_day_offset: 0,
}],
chrono_tz::UTC,
)
}
pub fn contains_local_time(&self, instant: DateTime<Utc>) -> bool {
let local_today = instant.with_timezone(&self.timezone).date_naive();
for delta in [-1i64, 0, 1] {
let day = local_today + Duration::days(delta);
for s in &self.sessions {
if let Some((o, c)) = s.instants(self.timezone, day) {
if instant >= o && instant < c {
return true;
}
}
}
}
false
}
pub fn open_at(&self, year: i32, month: u32, day: u32) -> Option<DateTime<Utc>> {
let nd = NaiveDate::from_ymd_opt(year, month, day)?;
self.sessions
.first()
.and_then(|s| s.instants(self.timezone, nd).map(|(o, _)| o))
}
pub fn close_at(&self, year: i32, month: u32, day: u32) -> Option<DateTime<Utc>> {
let nd = NaiveDate::from_ymd_opt(year, month, day)?;
self.sessions
.last()
.and_then(|s| s.instants(self.timezone, nd).map(|(_, c)| c))
}
}
pub fn parse_hhmm(s: &str) -> Option<NaiveTime> {
NaiveTime::parse_from_str(s, "%H:%M").ok()
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
use chrono_tz::America::{Chicago, New_York};
#[test]
fn nyse_contains_local() {
let th = TradingHours::new(
NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
New_York,
);
let inst = New_York
.with_ymd_and_hms(2024, 1, 8, 9, 30, 0)
.unwrap()
.with_timezone(&Utc);
assert!(th.contains_local_time(inst));
let before = inst - Duration::minutes(1);
assert!(!th.contains_local_time(before));
}
#[test]
fn cme_equity_futures_overnight_open() {
let th = TradingHours::from_sessions(
vec![Session::overnight(
NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
)],
Chicago,
);
let inst = Chicago
.with_ymd_and_hms(2024, 1, 7, 18, 0, 0)
.unwrap()
.with_timezone(&Utc);
assert!(th.contains_local_time(inst));
let inst2 = Chicago
.with_ymd_and_hms(2024, 1, 8, 16, 30, 0)
.unwrap()
.with_timezone(&Utc);
assert!(!th.contains_local_time(inst2));
}
#[test]
fn forex_continuous_24x5() {
let th = TradingHours::forex_24x5();
let inst = New_York
.with_ymd_and_hms(2024, 1, 9, 3, 0, 0)
.unwrap()
.with_timezone(&Utc);
assert!(th.contains_local_time(inst));
}
}