fmp_rs/endpoints/
indexes.rs

1//! Indexes endpoints
2
3use crate::{
4    client::FmpClient,
5    error::Result,
6    models::indexes::{IndexConstituent, IndexHistorical, IndexQuote, IndexSymbol},
7};
8use serde::{Deserialize, Serialize};
9
10/// Indexes API endpoints
11pub struct Indexes {
12    client: FmpClient,
13}
14
15impl Indexes {
16    pub(crate) fn new(client: FmpClient) -> Self {
17        Self { client }
18    }
19
20    /// Get list of available market indexes
21    ///
22    /// Returns all available market index symbols (S&P 500, Nasdaq, Dow Jones, etc.).
23    ///
24    /// # Example
25    /// ```no_run
26    /// # use fmp_rs::FmpClient;
27    /// # #[tokio::main]
28    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
29    /// let client = FmpClient::new()?;
30    /// let indexes = client.indexes().get_index_list().await?;
31    /// for index in indexes.iter().take(10) {
32    ///     println!("{}: {}", index.symbol, index.name.as_deref().unwrap_or("N/A"));
33    /// }
34    /// # Ok(())
35    /// # }
36    /// ```
37    pub async fn get_index_list(&self) -> Result<Vec<IndexSymbol>> {
38        #[derive(Serialize)]
39        struct Query<'a> {
40            apikey: &'a str,
41        }
42
43        let url = self.client.build_url("/symbol/available-indexes");
44        self.client
45            .get_with_query(
46                &url,
47                &Query {
48                    apikey: self.client.api_key(),
49                },
50            )
51            .await
52    }
53
54    /// Get real-time index quote
55    ///
56    /// Returns current level and market data for an index.
57    ///
58    /// # Arguments
59    /// * `symbol` - Index symbol (e.g., "^GSPC" for S&P 500, "^DJI" for Dow Jones)
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 quote = client.indexes().get_index_quote("^GSPC").await?;
68    /// if let Some(q) = quote.first() {
69    ///     println!("S&P 500: {:.2}", q.price.unwrap_or(0.0));
70    ///     println!("Change: {:+.2}%", q.changes_percentage.unwrap_or(0.0));
71    /// }
72    /// # Ok(())
73    /// # }
74    /// ```
75    pub async fn get_index_quote(&self, symbol: &str) -> Result<Vec<IndexQuote>> {
76        #[derive(Serialize)]
77        struct Query<'a> {
78            apikey: &'a str,
79        }
80
81        let url = self.client.build_url(&format!("/quote/{}", symbol));
82        self.client
83            .get_with_query(
84                &url,
85                &Query {
86                    apikey: self.client.api_key(),
87                },
88            )
89            .await
90    }
91
92    /// Get historical index data
93    ///
94    /// Returns daily historical data for an index.
95    ///
96    /// # Arguments
97    /// * `symbol` - Index symbol (e.g., "^GSPC")
98    /// * `from` - Start date (optional, format: YYYY-MM-DD)
99    /// * `to` - End date (optional, format: YYYY-MM-DD)
100    ///
101    /// # Example
102    /// ```no_run
103    /// # use fmp_rs::FmpClient;
104    /// # #[tokio::main]
105    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
106    /// let client = FmpClient::new()?;
107    /// let history = client.indexes().get_index_historical("^GSPC", None, None).await?;
108    /// for day in history.iter().take(5) {
109    ///     println!("{}: {:.2}", day.date, day.close);
110    /// }
111    /// # Ok(())
112    /// # }
113    /// ```
114    pub async fn get_index_historical(
115        &self,
116        symbol: &str,
117        from: Option<&str>,
118        to: Option<&str>,
119    ) -> Result<Vec<IndexHistorical>> {
120        #[derive(Serialize)]
121        struct Query<'a> {
122            #[serde(skip_serializing_if = "Option::is_none")]
123            from: Option<&'a str>,
124            #[serde(skip_serializing_if = "Option::is_none")]
125            to: Option<&'a str>,
126            apikey: &'a str,
127        }
128
129        let url = self
130            .client
131            .build_url(&format!("/historical-price-full/{}", symbol));
132
133        #[derive(Deserialize)]
134        struct Response {
135            historical: Vec<IndexHistorical>,
136        }
137
138        let response: Response = self
139            .client
140            .get_with_query(
141                &url,
142                &Query {
143                    from,
144                    to,
145                    apikey: self.client.api_key(),
146                },
147            )
148            .await?;
149
150        Ok(response.historical)
151    }
152
153    /// Get index constituents
154    ///
155    /// Returns all component stocks that make up an index (e.g., S&P 500 companies).
156    ///
157    /// # Arguments
158    /// * `symbol` - Index symbol (e.g., "^GSPC" for S&P 500 constituents)
159    ///
160    /// # Example
161    /// ```no_run
162    /// # use fmp_rs::FmpClient;
163    /// # #[tokio::main]
164    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
165    /// let client = FmpClient::new()?;
166    /// let constituents = client.indexes().get_index_constituents("^GSPC").await?;
167    /// println!("S&P 500 has {} components", constituents.len());
168    /// for stock in constituents.iter().take(10) {
169    ///     println!("{}: {} ({})",
170    ///         stock.symbol,
171    ///         stock.name.as_deref().unwrap_or("N/A"),
172    ///         stock.sector.as_deref().unwrap_or("N/A"));
173    /// }
174    /// # Ok(())
175    /// # }
176    /// ```
177    pub async fn get_index_constituents(&self, symbol: &str) -> Result<Vec<IndexConstituent>> {
178        #[derive(Serialize)]
179        struct Query<'a> {
180            apikey: &'a str,
181        }
182
183        let url = self.client.build_url(&format!("{}_constituent", symbol));
184        self.client
185            .get_with_query(
186                &url,
187                &Query {
188                    apikey: self.client.api_key(),
189                },
190            )
191            .await
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    // Golden path tests
200    #[tokio::test]
201    #[ignore = "requires FMP API key"]
202    async fn test_get_index_list() {
203        let client = FmpClient::new().unwrap();
204        let result = client.indexes().get_index_list().await;
205        assert!(result.is_ok());
206        let indexes = result.unwrap();
207        assert!(!indexes.is_empty());
208    }
209
210    #[tokio::test]
211    #[ignore = "requires FMP API key"]
212    async fn test_get_index_quote() {
213        let client = FmpClient::new().unwrap();
214        let result = client.indexes().get_index_quote("^GSPC").await;
215        assert!(result.is_ok());
216        let quotes = result.unwrap();
217        assert!(!quotes.is_empty());
218        assert!(quotes[0].price.is_some());
219    }
220
221    #[tokio::test]
222    #[ignore = "requires FMP API key"]
223    async fn test_get_index_historical() {
224        let client = FmpClient::new().unwrap();
225        let result = client
226            .indexes()
227            .get_index_historical("^GSPC", None, None)
228            .await;
229        assert!(result.is_ok());
230        let history = result.unwrap();
231        assert!(!history.is_empty());
232    }
233
234    #[tokio::test]
235    #[ignore = "requires FMP API key"]
236    async fn test_get_index_constituents() {
237        let client = FmpClient::new().unwrap();
238        let result = client.indexes().get_index_constituents("^GSPC").await;
239        assert!(result.is_ok());
240        let constituents = result.unwrap();
241        assert!(!constituents.is_empty());
242    }
243
244    // Edge case tests
245    #[tokio::test]
246    #[ignore = "requires FMP API key"]
247    async fn test_index_historical_with_dates() {
248        let client = FmpClient::new().unwrap();
249        let result = client
250            .indexes()
251            .get_index_historical("^GSPC", Some("2024-01-01"), Some("2024-01-31"))
252            .await;
253        assert!(result.is_ok());
254    }
255
256    #[tokio::test]
257    #[ignore = "requires FMP API key"]
258    async fn test_various_major_indexes() {
259        let client = FmpClient::new().unwrap();
260        // S&P 500, Nasdaq, Dow Jones
261        for symbol in &["^GSPC", "^IXIC", "^DJI"] {
262            let result = client.indexes().get_index_quote(symbol).await;
263            assert!(result.is_ok());
264        }
265    }
266
267    #[tokio::test]
268    #[ignore = "requires FMP API key"]
269    async fn test_dow_jones_constituents() {
270        let client = FmpClient::new().unwrap();
271        let result = client.indexes().get_index_constituents("^DJI").await;
272        assert!(result.is_ok());
273        let constituents = result.unwrap();
274        // Dow Jones has 30 companies
275        assert!(constituents.len() <= 30);
276    }
277
278    // Error handling tests
279    #[tokio::test]
280    async fn test_invalid_api_key() {
281        let client = FmpClient::builder()
282            .api_key("invalid_key_12345")
283            .build()
284            .unwrap();
285        let result = client.indexes().get_index_list().await;
286        assert!(result.is_err());
287    }
288
289    #[tokio::test]
290    #[ignore = "requires FMP API key"]
291    async fn test_invalid_symbol() {
292        let client = FmpClient::new().unwrap();
293        let result = client.indexes().get_index_quote("INVALIDINDEX123").await;
294        // Should return empty or error
295        match result {
296            Ok(quotes) => assert!(quotes.is_empty()),
297            Err(_) => {} // Error is acceptable for invalid symbol
298        }
299    }
300}