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