fmp_rs/endpoints/
market_hours.rs

1//! Market Hours endpoints
2
3use crate::models::market_hours::{ExchangeHours, ExtendedMarketHours, MarketHoliday};
4use crate::{FmpClient, Result};
5use chrono::Datelike;
6use serde::Serialize;
7
8/// Market Hours API endpoints
9pub struct MarketHours {
10    client: FmpClient,
11}
12
13impl MarketHours {
14    pub(crate) fn new(client: FmpClient) -> Self {
15        Self { client }
16    }
17
18    /// Get current market status and hours
19    ///
20    /// Returns the current market status (open/closed) and trading hours for major exchanges.
21    /// Useful for determining if markets are currently active for trading.
22    ///
23    /// # Example
24    /// ```no_run
25    /// # use fmp_rs::FmpClient;
26    /// # #[tokio::main]
27    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
28    /// let client = FmpClient::new()?;
29    /// let market_status = client.market_hours().get_market_hours().await?;
30    /// for market in market_status.iter().take(3) {
31    ///     println!("{}: {} ({})",
32    ///         market.stock_market.as_deref().unwrap_or("Unknown"),
33    ///         if market.is_market_open.unwrap_or(false) { "OPEN" } else { "CLOSED" },
34    ///         market.timezone.as_deref().unwrap_or("UTC"));
35    /// }
36    /// # Ok(())
37    /// # }
38    /// ```
39    pub async fn get_market_hours(&self) -> Result<Vec<crate::models::market_hours::MarketHours>> {
40        #[derive(Serialize)]
41        struct Query<'a> {
42            apikey: &'a str,
43        }
44
45        let url = self.client.build_url("/is-the-market-open");
46        self.client
47            .get_with_query(
48                &url,
49                &Query {
50                    apikey: self.client.api_key(),
51                },
52            )
53            .await
54    }
55
56    /// Get extended market hours for all exchanges
57    ///
58    /// Returns detailed trading hours including pre-market, regular session,
59    /// and after-hours trading times for all major exchanges.
60    ///
61    /// # Example
62    /// ```no_run
63    /// # use fmp_rs::FmpClient;
64    /// # #[tokio::main]
65    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
66    /// let client = FmpClient::new()?;
67    /// let extended_hours = client.market_hours().get_extended_hours().await?;
68    /// for exchange in extended_hours.iter().take(5) {
69    ///     println!("{}: Regular: {} - {}, Pre: {} - {}, After: {} - {}",
70    ///         exchange.name.as_deref().unwrap_or("Unknown"),
71    ///         exchange.market_open.as_deref().unwrap_or("N/A"),
72    ///         exchange.market_close.as_deref().unwrap_or("N/A"),
73    ///         exchange.pre_market_open.as_deref().unwrap_or("N/A"),
74    ///         exchange.pre_market_close.as_deref().unwrap_or("N/A"),
75    ///         exchange.after_hours_open.as_deref().unwrap_or("N/A"),
76    ///         exchange.after_hours_close.as_deref().unwrap_or("N/A"));
77    /// }
78    /// # Ok(())
79    /// # }
80    /// ```
81    pub async fn get_extended_hours(&self) -> Result<Vec<ExtendedMarketHours>> {
82        #[derive(Serialize)]
83        struct Query<'a> {
84            apikey: &'a str,
85        }
86
87        let url = self.client.build_url("/market-hours");
88        self.client
89            .get_with_query(
90                &url,
91                &Query {
92                    apikey: self.client.api_key(),
93                },
94            )
95            .await
96    }
97
98    /// Get market holidays for a specific year
99    ///
100    /// Returns all market holidays and early closures for major exchanges.
101    /// Useful for planning trading strategies around market closures.
102    ///
103    /// # Arguments
104    /// * `year` - Optional year (e.g., 2024). Defaults to current year if not provided.
105    ///
106    /// # Example
107    /// ```no_run
108    /// # use fmp_rs::FmpClient;
109    /// # #[tokio::main]
110    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
111    /// let client = FmpClient::new()?;
112    /// let holidays = client.market_hours().get_market_holidays(Some(2024)).await?;
113    /// for holiday in holidays.iter().take(10) {
114    ///     println!("{}: {} ({})",
115    ///         holiday.date.as_deref().unwrap_or("N/A"),
116    ///         holiday.holiday.as_deref().unwrap_or("Unknown"),
117    ///         holiday.market_status.as_deref().unwrap_or("closed"));
118    /// }
119    /// # Ok(())
120    /// # }
121    /// ```
122    pub async fn get_market_holidays(&self, year: Option<i32>) -> Result<Vec<MarketHoliday>> {
123        #[derive(Serialize)]
124        struct Query<'a> {
125            #[serde(skip_serializing_if = "Option::is_none")]
126            year: Option<i32>,
127            apikey: &'a str,
128        }
129
130        let url = self.client.build_url("/market_holidays");
131        self.client
132            .get_with_query(
133                &url,
134                &Query {
135                    year,
136                    apikey: self.client.api_key(),
137                },
138            )
139            .await
140    }
141
142    /// Get trading hours for all exchanges
143    ///
144    /// Returns comprehensive trading hours information for all global exchanges,
145    /// including regular hours, pre-market, and after-hours sessions.
146    ///
147    /// # Example
148    /// ```no_run
149    /// # use fmp_rs::FmpClient;
150    /// # #[tokio::main]
151    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
152    /// let client = FmpClient::new()?;
153    /// let exchange_hours = client.market_hours().get_exchange_hours().await?;
154    /// for exchange in exchange_hours.iter().take(5) {
155    ///     let status = if exchange.is_open.unwrap_or(false) { "🟢 OPEN" } else { "🔴 CLOSED" };
156    ///     println!("{} ({}): {} | Regular: {} - {}",
157    ///         exchange.exchange_name.as_deref().unwrap_or("Unknown"),
158    ///         exchange.country.as_deref().unwrap_or("Unknown"),
159    ///         status,
160    ///         exchange.market_open.as_deref().unwrap_or("N/A"),
161    ///         exchange.market_close.as_deref().unwrap_or("N/A"));
162    /// }
163    /// # Ok(())
164    /// # }
165    /// ```
166    pub async fn get_exchange_hours(&self) -> Result<Vec<ExchangeHours>> {
167        #[derive(Serialize)]
168        struct Query<'a> {
169            apikey: &'a str,
170        }
171
172        let url = self.client.build_url("/exchange-hours");
173        self.client
174            .get_with_query(
175                &url,
176                &Query {
177                    apikey: self.client.api_key(),
178                },
179            )
180            .await
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    fn create_test_client() -> FmpClient {
189        FmpClient::builder().api_key("test_key").build().unwrap()
190    }
191
192    #[test]
193    fn test_new() {
194        let client = create_test_client();
195        let market_hours = MarketHours::new(client);
196        // Test passes if no panic occurs
197    }
198
199    #[tokio::test]
200    #[ignore] // Requires API key
201    async fn test_get_market_hours() {
202        let client = FmpClient::new().unwrap();
203        let result = client.market_hours().get_market_hours().await;
204        assert!(result.is_ok());
205
206        let market_status = result.unwrap();
207        if !market_status.is_empty() {
208            let first_market = &market_status[0];
209            // Verify basic structure
210            assert!(first_market.stock_market.is_some() || first_market.exchange.is_some());
211            println!("Market status retrieved: {} markets", market_status.len());
212        }
213    }
214
215    #[tokio::test]
216    #[ignore] // Requires API key
217    async fn test_get_extended_hours() {
218        let client = FmpClient::new().unwrap();
219        let result = client.market_hours().get_extended_hours().await;
220        assert!(result.is_ok());
221
222        let extended_hours = result.unwrap();
223        if !extended_hours.is_empty() {
224            let first_exchange = &extended_hours[0];
225            // Should have basic exchange information
226            assert!(first_exchange.name.is_some() || first_exchange.exchange.is_some());
227            println!("Extended hours for {} exchanges", extended_hours.len());
228        }
229    }
230
231    #[tokio::test]
232    #[ignore] // Requires API key
233    async fn test_get_market_holidays_current_year() {
234        let client = FmpClient::new().unwrap();
235        let result = client.market_hours().get_market_holidays(None).await;
236        assert!(result.is_ok());
237
238        let holidays = result.unwrap();
239        if !holidays.is_empty() {
240            let first_holiday = &holidays[0];
241            assert!(first_holiday.date.is_some());
242            assert!(first_holiday.holiday.is_some());
243            println!("Found {} holidays for current year", holidays.len());
244        }
245    }
246
247    #[tokio::test]
248    #[ignore] // Requires API key
249    async fn test_get_market_holidays_specific_year() {
250        let client = FmpClient::new().unwrap();
251        let result = client.market_hours().get_market_holidays(Some(2024)).await;
252        assert!(result.is_ok());
253
254        let holidays = result.unwrap();
255        if !holidays.is_empty() {
256            // Check that we got holidays for the requested year
257            if let Some(first_holiday) = holidays.first() {
258                assert_eq!(first_holiday.year, Some(2024));
259                println!("Found {} holidays for 2024", holidays.len());
260            }
261        }
262    }
263
264    #[tokio::test]
265    #[ignore] // Requires API key
266    async fn test_get_exchange_hours() {
267        let client = FmpClient::new().unwrap();
268        let result = client.market_hours().get_exchange_hours().await;
269        assert!(result.is_ok());
270
271        let exchange_hours = result.unwrap();
272        if !exchange_hours.is_empty() {
273            let first_exchange = &exchange_hours[0];
274            // Should have exchange name and basic hours info
275            assert!(first_exchange.exchange_name.is_some() || first_exchange.exchange.is_some());
276            println!("Trading hours for {} exchanges", exchange_hours.len());
277        }
278    }
279
280    #[test]
281    fn test_market_hours_url_building() {
282        let client = create_test_client();
283        let market_hours = MarketHours::new(client);
284        // Test passes if construction succeeds without panics
285    }
286
287    #[test]
288    fn test_holiday_year_validation() {
289        // Test that year parameter is properly handled
290        let current_year = chrono::Utc::now().year();
291        let future_year = current_year + 1;
292        let past_year = current_year - 1;
293
294        // These should all be valid years for the API
295        assert!(past_year > 1900);
296        assert!(future_year < 2100);
297    }
298
299    #[test]
300    fn test_market_hours_models_serialization() {
301        use serde_json;
302
303        // Test MarketHours model
304        let market_hours = crate::models::market_hours::MarketHours {
305            exchange: Some("NYSE".to_string()),
306            stock_exchange_name: Some("New York Stock Exchange".to_string()),
307            stock_market: Some("NYSE".to_string()),
308            is_market_open: Some(true),
309            opening_hour: Some("09:30".to_string()),
310            closing_hour: Some("16:00".to_string()),
311            current_date_time: Some("2024-01-15 14:30:00".to_string()),
312            timezone: Some("EST".to_string()),
313        };
314
315        let json = serde_json::to_string(&market_hours).unwrap();
316        let deserialized: crate::models::market_hours::MarketHours =
317            serde_json::from_str(&json).unwrap();
318        assert_eq!(deserialized.exchange, Some("NYSE".to_string()));
319        assert_eq!(deserialized.is_market_open, Some(true));
320
321        // Test MarketHoliday model
322        let holiday = MarketHoliday {
323            date: Some("2024-12-25".to_string()),
324            year: Some(2024),
325            holiday: Some("Christmas Day".to_string()),
326            exchange: Some("NYSE".to_string()),
327            market_status: Some("closed".to_string()),
328            early_close_time: None,
329        };
330
331        let json = serde_json::to_string(&holiday).unwrap();
332        let deserialized: MarketHoliday = serde_json::from_str(&json).unwrap();
333        assert_eq!(deserialized.holiday, Some("Christmas Day".to_string()));
334        assert_eq!(deserialized.year, Some(2024));
335    }
336
337    #[test]
338    fn test_exchange_hours_model_completeness() {
339        let exchange = ExchangeHours {
340            exchange: Some("NASDAQ".to_string()),
341            exchange_name: Some("NASDAQ Stock Market".to_string()),
342            country: Some("United States".to_string()),
343            timezone: Some("EST".to_string()),
344            market_open: Some("09:30".to_string()),
345            market_close: Some("16:00".to_string()),
346            pre_market_start: Some("04:00".to_string()),
347            pre_market_end: Some("09:30".to_string()),
348            after_hours_start: Some("16:00".to_string()),
349            after_hours_end: Some("20:00".to_string()),
350            is_open: Some(false),
351            current_time: Some("2024-01-15 18:30:00".to_string()),
352            day_of_week: Some("Monday".to_string()),
353        };
354
355        // Verify all fields are accessible
356        assert_eq!(exchange.exchange, Some("NASDAQ".to_string()));
357        assert_eq!(exchange.country, Some("United States".to_string()));
358        assert_eq!(exchange.is_open, Some(false));
359        assert_eq!(exchange.day_of_week, Some("Monday".to_string()));
360    }
361
362    // Additional edge case tests
363    #[tokio::test]
364    #[ignore = "requires FMP API key"]
365    async fn test_get_market_holidays_future_year() {
366        let client = FmpClient::new().unwrap();
367        let future_year = chrono::Utc::now().year() + 1;
368        let result = client
369            .market_hours()
370            .get_market_holidays(Some(future_year))
371            .await;
372        assert!(result.is_ok());
373        let holidays = result.unwrap();
374        // Future years should have some holidays defined
375        if !holidays.is_empty() {
376            assert!(holidays[0].year.is_some());
377        }
378    }
379
380    #[tokio::test]
381    #[ignore = "requires FMP API key"]
382    async fn test_get_market_holidays_past_year() {
383        let client = FmpClient::new().unwrap();
384        let past_year = chrono::Utc::now().year() - 1;
385        let result = client
386            .market_hours()
387            .get_market_holidays(Some(past_year))
388            .await;
389        assert!(result.is_ok());
390        let holidays = result.unwrap();
391        // Past years should have holiday data
392        assert!(!holidays.is_empty());
393    }
394
395    #[tokio::test]
396    #[ignore = "requires FMP API key"]
397    async fn test_market_hours_consistency() {
398        let client = FmpClient::new().unwrap();
399        let market_hours_result = client.market_hours().get_market_hours().await;
400        let extended_hours_result = client.market_hours().get_extended_hours().await;
401
402        assert!(market_hours_result.is_ok());
403        assert!(extended_hours_result.is_ok());
404
405        // Both should return data
406        let market_hours = market_hours_result.unwrap();
407        let extended_hours = extended_hours_result.unwrap();
408
409        if !market_hours.is_empty() && !extended_hours.is_empty() {
410            // Market hours should have basic fields
411            assert!(market_hours[0].exchange.is_some());
412            assert!(extended_hours[0].exchange.is_some());
413        }
414    }
415}