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 {
43 Self { session }
44 }
45
46 pub fn status(&self, utc_ms: u64) -> Result<TradingStatus, StreamError> {
48 match self.session {
49 MarketSession::Crypto => Ok(TradingStatus::Open),
50 MarketSession::UsEquity => self.us_equity_status(utc_ms),
51 MarketSession::Forex => self.forex_status(utc_ms),
52 }
53 }
54
55 pub fn session(&self) -> MarketSession {
57 self.session
58 }
59
60 fn us_equity_status(&self, utc_ms: u64) -> Result<TradingStatus, StreamError> {
61 const DAY_MS: u64 = 24 * 3600 * 1000;
65 const ET_OFFSET_MS: u64 = 5 * 3600 * 1000; let utc_day_ms = utc_ms % DAY_MS;
67 let day_ms = (utc_day_ms + DAY_MS - ET_OFFSET_MS) % DAY_MS;
68 let day_of_week = (utc_ms / DAY_MS + 4) % 7; if day_of_week == 0 || day_of_week == 6 {
72 return Ok(TradingStatus::Closed);
73 }
74
75 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 {
81 Ok(TradingStatus::Open)
82 } else if (day_ms >= pre_ms && day_ms < open_ms) || (day_ms >= close_ms && day_ms < post_ms)
83 {
84 Ok(TradingStatus::Extended)
85 } else {
86 Ok(TradingStatus::Closed)
87 }
88 }
89
90 fn forex_status(&self, utc_ms: u64) -> Result<TradingStatus, StreamError> {
91 let day_of_week = (utc_ms / (24 * 3600 * 1000) + 4) % 7; let day_ms = utc_ms % (24 * 3600 * 1000);
94 let hour_22_ms = 22 * 3600 * 1000;
95
96 if day_of_week == 6 {
98 return Ok(TradingStatus::Closed);
99 }
100 if day_of_week == 0 && day_ms < hour_22_ms {
101 return Ok(TradingStatus::Closed);
102 }
103 if day_of_week == 5 && day_ms >= hour_22_ms {
105 return Ok(TradingStatus::Closed);
106 }
107 Ok(TradingStatus::Open)
108 }
109}
110
111pub fn is_tradeable(session: MarketSession, utc_ms: u64) -> Result<bool, StreamError> {
113 let sa = SessionAwareness::new(session);
114 let status = sa.status(utc_ms)?;
115 Ok(status == TradingStatus::Open || status == TradingStatus::Extended)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 const MON_OPEN_UTC_MS: u64 = 1704724200000;
125 const MON_CLOSE_UTC_MS: u64 = 1704747600000;
127 const SAT_UTC_MS: u64 = 1705104000000;
129 const SUN_BEFORE_UTC_MS: u64 = 1704621600000;
131
132 fn sa(session: MarketSession) -> SessionAwareness {
133 SessionAwareness::new(session)
134 }
135
136 #[test]
137 fn test_crypto_always_open() {
138 let sa = sa(MarketSession::Crypto);
139 assert_eq!(sa.status(MON_OPEN_UTC_MS).unwrap(), TradingStatus::Open);
140 assert_eq!(sa.status(SAT_UTC_MS).unwrap(), TradingStatus::Open);
141 assert_eq!(sa.status(0).unwrap(), TradingStatus::Open);
142 }
143
144 #[test]
145 fn test_us_equity_open_during_market_hours() {
146 let sa = sa(MarketSession::UsEquity);
147 assert_eq!(sa.status(MON_OPEN_UTC_MS).unwrap(), TradingStatus::Open);
149 }
150
151 #[test]
152 fn test_us_equity_closed_after_hours() {
153 let sa = sa(MarketSession::UsEquity);
154 let status = sa.status(MON_CLOSE_UTC_MS).unwrap();
156 assert!(status == TradingStatus::Extended || status == TradingStatus::Closed);
157 }
158
159 #[test]
160 fn test_us_equity_closed_on_saturday() {
161 let sa = sa(MarketSession::UsEquity);
162 assert_eq!(sa.status(SAT_UTC_MS).unwrap(), TradingStatus::Closed);
163 }
164
165 #[test]
166 fn test_us_equity_premarket_extended() {
167 let sa = sa(MarketSession::UsEquity);
168 let pre_ms: u64 = 1704704400000; let status = sa.status(pre_ms).unwrap();
171 assert!(status == TradingStatus::Extended || status == TradingStatus::Open);
172 }
173
174 #[test]
175 fn test_forex_open_on_monday() {
176 let sa = sa(MarketSession::Forex);
177 assert_eq!(sa.status(MON_OPEN_UTC_MS).unwrap(), TradingStatus::Open);
178 }
179
180 #[test]
181 fn test_forex_closed_on_saturday() {
182 let sa = sa(MarketSession::Forex);
183 assert_eq!(sa.status(SAT_UTC_MS).unwrap(), TradingStatus::Closed);
184 }
185
186 #[test]
187 fn test_forex_closed_sunday_before_22_utc() {
188 let sa = sa(MarketSession::Forex);
189 assert_eq!(sa.status(SUN_BEFORE_UTC_MS).unwrap(), TradingStatus::Closed);
190 }
191
192 #[test]
193 fn test_is_tradeable_crypto_always_true() {
194 assert!(is_tradeable(MarketSession::Crypto, SAT_UTC_MS).unwrap());
195 }
196
197 #[test]
198 fn test_is_tradeable_equity_open() {
199 assert!(is_tradeable(MarketSession::UsEquity, MON_OPEN_UTC_MS).unwrap());
200 }
201
202 #[test]
203 fn test_is_tradeable_equity_weekend_false() {
204 assert!(!is_tradeable(MarketSession::UsEquity, SAT_UTC_MS).unwrap());
205 }
206
207 #[test]
208 fn test_session_accessor() {
209 let sa = sa(MarketSession::Crypto);
210 assert_eq!(sa.session(), MarketSession::Crypto);
211 }
212
213 #[test]
214 fn test_market_session_equality() {
215 assert_eq!(MarketSession::Crypto, MarketSession::Crypto);
216 assert_ne!(MarketSession::Crypto, MarketSession::Forex);
217 }
218
219 #[test]
220 fn test_trading_status_equality() {
221 assert_eq!(TradingStatus::Open, TradingStatus::Open);
222 assert_ne!(TradingStatus::Open, TradingStatus::Closed);
223 }
224}