fin_stream/session/
mod.rs1use crate::error::StreamError;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
15pub enum MarketSession {
16 UsEquity,
18 Crypto,
20 Forex,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
26pub enum TradingStatus {
27 Open,
29 Extended,
31 Closed,
33}
34
35pub struct SessionAwareness {
37 session: MarketSession,
38}
39
40impl SessionAwareness {
41 pub fn new(session: MarketSession) -> Self {
42 Self { session }
43 }
44
45 pub fn status(&self, utc_ms: u64) -> Result<TradingStatus, StreamError> {
47 match self.session {
48 MarketSession::Crypto => Ok(TradingStatus::Open),
49 MarketSession::UsEquity => self.us_equity_status(utc_ms),
50 MarketSession::Forex => self.forex_status(utc_ms),
51 }
52 }
53
54 pub fn session(&self) -> MarketSession { self.session }
55
56 fn us_equity_status(&self, utc_ms: u64) -> Result<TradingStatus, StreamError> {
57 const DAY_MS: u64 = 24 * 3600 * 1000;
61 const ET_OFFSET_MS: u64 = 5 * 3600 * 1000; let utc_day_ms = utc_ms % DAY_MS;
63 let day_ms = (utc_day_ms + DAY_MS - ET_OFFSET_MS) % DAY_MS;
64 let day_of_week = (utc_ms / DAY_MS + 4) % 7; if day_of_week == 0 || day_of_week == 6 {
68 return Ok(TradingStatus::Closed);
69 }
70
71 let open_ms = (9 * 3600 + 30 * 60) * 1000; let close_ms = 16 * 3600 * 1000; let pre_ms = 4 * 3600 * 1000; let post_ms = 20 * 3600 * 1000; if day_ms >= open_ms && day_ms < close_ms {
77 Ok(TradingStatus::Open)
78 } else if (day_ms >= pre_ms && day_ms < open_ms) || (day_ms >= close_ms && day_ms < post_ms) {
79 Ok(TradingStatus::Extended)
80 } else {
81 Ok(TradingStatus::Closed)
82 }
83 }
84
85 fn forex_status(&self, utc_ms: u64) -> Result<TradingStatus, StreamError> {
86 let day_of_week = (utc_ms / (24 * 3600 * 1000) + 4) % 7; let day_ms = utc_ms % (24 * 3600 * 1000);
89 let hour_22_ms = 22 * 3600 * 1000;
90
91 if day_of_week == 6 {
93 return Ok(TradingStatus::Closed);
94 }
95 if day_of_week == 0 && day_ms < hour_22_ms {
96 return Ok(TradingStatus::Closed);
97 }
98 if day_of_week == 5 && day_ms >= hour_22_ms {
100 return Ok(TradingStatus::Closed);
101 }
102 Ok(TradingStatus::Open)
103 }
104}
105
106pub fn is_tradeable(session: MarketSession, utc_ms: u64) -> Result<bool, StreamError> {
108 let sa = SessionAwareness::new(session);
109 let status = sa.status(utc_ms)?;
110 Ok(status == TradingStatus::Open || status == TradingStatus::Extended)
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 const MON_OPEN_UTC_MS: u64 = 1704724200000;
120 const MON_CLOSE_UTC_MS: u64 = 1704747600000;
122 const SAT_UTC_MS: u64 = 1705104000000;
124 const SUN_BEFORE_UTC_MS: u64 = 1704621600000;
126
127 fn sa(session: MarketSession) -> SessionAwareness {
128 SessionAwareness::new(session)
129 }
130
131 #[test]
132 fn test_crypto_always_open() {
133 let sa = sa(MarketSession::Crypto);
134 assert_eq!(sa.status(MON_OPEN_UTC_MS).unwrap(), TradingStatus::Open);
135 assert_eq!(sa.status(SAT_UTC_MS).unwrap(), TradingStatus::Open);
136 assert_eq!(sa.status(0).unwrap(), TradingStatus::Open);
137 }
138
139 #[test]
140 fn test_us_equity_open_during_market_hours() {
141 let sa = sa(MarketSession::UsEquity);
142 assert_eq!(sa.status(MON_OPEN_UTC_MS).unwrap(), TradingStatus::Open);
144 }
145
146 #[test]
147 fn test_us_equity_closed_after_hours() {
148 let sa = sa(MarketSession::UsEquity);
149 let status = sa.status(MON_CLOSE_UTC_MS).unwrap();
151 assert!(status == TradingStatus::Extended || status == TradingStatus::Closed);
152 }
153
154 #[test]
155 fn test_us_equity_closed_on_saturday() {
156 let sa = sa(MarketSession::UsEquity);
157 assert_eq!(sa.status(SAT_UTC_MS).unwrap(), TradingStatus::Closed);
158 }
159
160 #[test]
161 fn test_us_equity_premarket_extended() {
162 let sa = sa(MarketSession::UsEquity);
163 let pre_ms: u64 = 1704704400000; let status = sa.status(pre_ms).unwrap();
166 assert!(status == TradingStatus::Extended || status == TradingStatus::Open);
167 }
168
169 #[test]
170 fn test_forex_open_on_monday() {
171 let sa = sa(MarketSession::Forex);
172 assert_eq!(sa.status(MON_OPEN_UTC_MS).unwrap(), TradingStatus::Open);
173 }
174
175 #[test]
176 fn test_forex_closed_on_saturday() {
177 let sa = sa(MarketSession::Forex);
178 assert_eq!(sa.status(SAT_UTC_MS).unwrap(), TradingStatus::Closed);
179 }
180
181 #[test]
182 fn test_forex_closed_sunday_before_22_utc() {
183 let sa = sa(MarketSession::Forex);
184 assert_eq!(sa.status(SUN_BEFORE_UTC_MS).unwrap(), TradingStatus::Closed);
185 }
186
187 #[test]
188 fn test_is_tradeable_crypto_always_true() {
189 assert!(is_tradeable(MarketSession::Crypto, SAT_UTC_MS).unwrap());
190 }
191
192 #[test]
193 fn test_is_tradeable_equity_open() {
194 assert!(is_tradeable(MarketSession::UsEquity, MON_OPEN_UTC_MS).unwrap());
195 }
196
197 #[test]
198 fn test_is_tradeable_equity_weekend_false() {
199 assert!(!is_tradeable(MarketSession::UsEquity, SAT_UTC_MS).unwrap());
200 }
201
202 #[test]
203 fn test_session_accessor() {
204 let sa = sa(MarketSession::Crypto);
205 assert_eq!(sa.session(), MarketSession::Crypto);
206 }
207
208 #[test]
209 fn test_market_session_equality() {
210 assert_eq!(MarketSession::Crypto, MarketSession::Crypto);
211 assert_ne!(MarketSession::Crypto, MarketSession::Forex);
212 }
213
214 #[test]
215 fn test_trading_status_equality() {
216 assert_eq!(TradingStatus::Open, TradingStatus::Open);
217 assert_ne!(TradingStatus::Open, TradingStatus::Closed);
218 }
219}