Skip to main content

rhood_core/endpoints/
markets.rs

1use crate::api::paths;
2use crate::client::RobinhoodClient;
3use crate::models::market::*;
4use crate::models::watchlist::WatchlistItem;
5use crate::pagination::PaginatedResponse;
6use crate::{Result, RhoodError};
7
8impl RobinhoodClient {
9    /// Fetches a list of all available markets (e.g., NYSE, NASDAQ).
10    ///
11    /// # Errors
12    ///
13    /// Returns an error if the HTTP request fails or the response cannot be
14    /// deserialized.
15    pub async fn get_markets(&self) -> Result<Vec<Market>> {
16        self.get_paginated(&self.api_url(paths::MARKETS), &[]).await
17    }
18
19    /// Fetches market hours for a specific market and date.
20    ///
21    /// The `mic` parameter is a Market Identifier Code (e.g., `"XNYS"`) and
22    /// `date` is in `YYYY-MM-DD` format.
23    ///
24    /// # Errors
25    ///
26    /// Returns an error if the HTTP request fails or the response cannot be
27    /// deserialized.
28    pub async fn get_market_hours(&self, mic: &str, date: &str) -> Result<MarketHours> {
29        let url = format!("{}{mic}/hours/{date}/", self.api_url(paths::MARKETS));
30        self.get(&url).await
31    }
32
33    /// Fetches today's market hours for a market identified by its MIC code.
34    ///
35    /// Resolves the market from the full market list and follows its
36    /// `todays_hours` URL.
37    ///
38    /// # Errors
39    ///
40    /// Returns [`RhoodError::InvalidParameter`] if the MIC code is unknown
41    /// or the market has no today's-hours URL. Also returns an error on HTTP
42    /// or deserialization failures.
43    pub async fn get_market_today_hours(&self, mic: &str) -> Result<MarketHours> {
44        let markets = self.get_markets().await?;
45        let market = markets
46            .iter()
47            .find(|market| market.mic.as_deref() == Some(mic))
48            .ok_or_else(|| RhoodError::InvalidParameter(format!("Unknown market: {mic}")))?;
49        let hours_url = market
50            .todays_hours
51            .as_deref()
52            .ok_or_else(|| RhoodError::InvalidParameter("No today's hours URL".into()))?;
53        self.get(hours_url).await
54    }
55
56    /// Fetches the top 20 daily movers from Robinhood's curated list.
57    ///
58    /// Uses the `/discovery/lists/items/` endpoint with Robinhood's daily
59    /// movers list ID. Returns enriched items with live price and change data.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the HTTP request fails or the response cannot be
64    /// deserialized.
65    pub async fn get_daily_movers(&self) -> Result<Vec<WatchlistItem>> {
66        let resp: PaginatedResponse<WatchlistItem> = self
67            .get_with_params(
68                &self.api_url(paths::WATCHLIST_ITEMS),
69                &[("list_id", paths::DAILY_MOVERS_LIST_ID)],
70            )
71            .await?;
72        Ok(resp.results)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use crate::models::market::{Market, MarketHours};
79
80    #[test]
81    fn market_deserializes() {
82        let json = r#"{
83            "mic": "XNYS",
84            "acronym": "NYSE",
85            "name": "New York Stock Exchange",
86            "city": "New York",
87            "country": "US",
88            "timezone": "US/Eastern",
89            "todays_hours": "https://api.robinhood.com/markets/XNYS/hours/2026-03-31/"
90        }"#;
91        let market: Market = serde_json::from_str(json).unwrap();
92        assert_eq!(market.mic.as_deref(), Some("XNYS"));
93        assert_eq!(market.acronym.as_deref(), Some("NYSE"));
94        assert!(market.todays_hours.is_some());
95    }
96
97    #[test]
98    fn market_hours_deserializes() {
99        let json = r#"{
100            "date": "2026-03-31",
101            "is_open": true,
102            "opens_at": "2026-03-31T13:30:00Z",
103            "closes_at": "2026-03-31T20:00:00Z",
104            "extended_opens_at": "2026-03-31T09:00:00Z",
105            "extended_closes_at": "2026-03-31T22:00:00Z"
106        }"#;
107        let hours: MarketHours = serde_json::from_str(json).unwrap();
108        assert_eq!(hours.date.as_deref(), Some("2026-03-31"));
109        assert_eq!(hours.is_open, Some(true));
110        assert!(hours.opens_at.is_some());
111        assert!(hours.extended_opens_at.is_some());
112    }
113
114    #[test]
115    fn market_hours_closed_day() {
116        let json = r#"{ "date": "2026-04-05", "is_open": false }"#;
117        let hours: MarketHours = serde_json::from_str(json).unwrap();
118        assert_eq!(hours.is_open, Some(false));
119        assert!(hours.opens_at.is_none());
120    }
121}