fmp_rs/endpoints/
etf.rs

1//! ETF endpoints
2
3use crate::client::FmpClient;
4use crate::error::Result;
5use crate::models::etf::{
6    CountryWeighting, EtfHolder, EtfHolding, EtfInfo, EtfListItem, EtfSearchResult, SectorWeighting,
7};
8
9/// ETF API endpoints
10pub struct Etf {
11    client: FmpClient,
12}
13
14impl Etf {
15    pub(crate) fn new(client: FmpClient) -> Self {
16        Self { client }
17    }
18
19    /// Get a list of all available ETFs
20    ///
21    /// # Example
22    ///
23    /// ```no_run
24    /// use fmp_rs::FmpClient;
25    ///
26    /// #[tokio::main]
27    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
28    ///     let client = FmpClient::new()?;
29    ///     let etfs = client.etf().get_etf_list().await?;
30    ///     
31    ///     for etf in etfs.iter().take(5) {
32    ///         println!("{}: {}", etf.symbol, etf.name);
33    ///     }
34    ///     Ok(())
35    /// }
36    /// ```
37    pub async fn get_etf_list(&self) -> Result<Vec<EtfListItem>> {
38        self.client.get("/api/v3/etf/list").await
39    }
40
41    /// Search for ETFs by name or symbol
42    ///
43    /// # Arguments
44    ///
45    /// * `query` - Search query (name or symbol fragment)
46    /// * `limit` - Optional limit on number of results
47    /// * `exchange` - Optional exchange filter (e.g., "NASDAQ", "NYSE")
48    ///
49    /// # Example
50    ///
51    /// ```no_run
52    /// use fmp_rs::FmpClient;
53    ///
54    /// #[tokio::main]
55    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
56    ///     let client = FmpClient::new()?;
57    ///     let results = client.etf().search_etf("vanguard", Some(10), None).await?;
58    ///     
59    ///     for etf in &results {
60    ///         println!("{}: {} ({})", etf.symbol, etf.name,
61    ///                  etf.exchange_short_name.as_deref().unwrap_or("N/A"));
62    ///     }
63    ///     Ok(())
64    /// }
65    /// ```
66    pub async fn search_etf(
67        &self,
68        query: &str,
69        limit: Option<u32>,
70        exchange: Option<&str>,
71    ) -> Result<Vec<EtfSearchResult>> {
72        let mut url = format!("/api/v3/search/etf?query={}", query);
73        if let Some(limit) = limit {
74            url.push_str(&format!("&limit={}", limit));
75        }
76        if let Some(exchange) = exchange {
77            url.push_str(&format!("&exchange={}", exchange));
78        }
79        self.client.get(&url).await
80    }
81
82    /// Get institutional holders of an ETF (who holds this ETF)
83    ///
84    /// # Arguments
85    ///
86    /// * `symbol` - ETF symbol (e.g., "SPY")
87    ///
88    /// # Example
89    ///
90    /// ```no_run
91    /// use fmp_rs::FmpClient;
92    ///
93    /// #[tokio::main]
94    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
95    ///     let client = FmpClient::new()?;
96    ///     let holders = client.etf().get_etf_holder("SPY").await?;
97    ///     
98    ///     println!("Top holders of SPY:");
99    ///     for holder in holders.iter().take(10) {
100    ///         println!("  {}: {}%", holder.name,
101    ///                  holder.weight_percentage.unwrap_or(0.0));
102    ///     }
103    ///     Ok(())
104    /// }
105    /// ```
106    pub async fn get_etf_holder(&self, symbol: &str) -> Result<Vec<EtfHolder>> {
107        self.client
108            .get(&format!("/api/v3/etf-holder/{}", symbol))
109            .await
110    }
111
112    /// Get holdings of an ETF (what this ETF holds)
113    ///
114    /// # Arguments
115    ///
116    /// * `symbol` - ETF symbol (e.g., "SPY")
117    ///
118    /// # Example
119    ///
120    /// ```no_run
121    /// use fmp_rs::FmpClient;
122    ///
123    /// #[tokio::main]
124    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
125    ///     let client = FmpClient::new()?;
126    ///     let holdings = client.etf().get_etf_holdings("SPY").await?;
127    ///     
128    ///     println!("Top holdings in SPY:");
129    ///     for holding in holdings.iter().take(10) {
130    ///         println!("  {}: {:.2}% ({})",
131    ///                  holding.asset, holding.weight_percentage, holding.name);
132    ///     }
133    ///     Ok(())
134    /// }
135    /// ```
136    pub async fn get_etf_holdings(&self, symbol: &str) -> Result<Vec<EtfHolding>> {
137        self.client
138            .get(&format!("/api/v3/etf-holdings/{}", symbol))
139            .await
140    }
141
142    /// Get sector weighting of an ETF
143    ///
144    /// # Arguments
145    ///
146    /// * `symbol` - ETF symbol (e.g., "SPY")
147    ///
148    /// # Example
149    ///
150    /// ```no_run
151    /// use fmp_rs::FmpClient;
152    ///
153    /// #[tokio::main]
154    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
155    ///     let client = FmpClient::new()?;
156    ///     let sectors = client.etf().get_etf_sector_weighting("SPY").await?;
157    ///     
158    ///     println!("Sector allocation for SPY:");
159    ///     for sector in &sectors {
160    ///         println!("  {}: {}%", sector.sector, sector.weight_percentage);
161    ///     }
162    ///     Ok(())
163    /// }
164    /// ```
165    pub async fn get_etf_sector_weighting(&self, symbol: &str) -> Result<Vec<SectorWeighting>> {
166        self.client
167            .get(&format!("/api/v3/etf-sector-weightings/{}", symbol))
168            .await
169    }
170
171    /// Get country weighting of an ETF
172    ///
173    /// # Arguments
174    ///
175    /// * `symbol` - ETF symbol (e.g., "SPY")
176    ///
177    /// # Example
178    ///
179    /// ```no_run
180    /// use fmp_rs::FmpClient;
181    ///
182    /// #[tokio::main]
183    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
184    ///     let client = FmpClient::new()?;
185    ///     let countries = client.etf().get_etf_country_weighting("SPY").await?;
186    ///     
187    ///     println!("Country allocation for SPY:");
188    ///     for country in &countries {
189    ///         println!("  {}: {}%", country.country, country.weight_percentage);
190    ///     }
191    ///     Ok(())
192    /// }
193    /// ```
194    pub async fn get_etf_country_weighting(&self, symbol: &str) -> Result<Vec<CountryWeighting>> {
195        self.client
196            .get(&format!("/api/v3/etf-country-weightings/{}", symbol))
197            .await
198    }
199
200    /// Get detailed information about an ETF
201    ///
202    /// # Arguments
203    ///
204    /// * `symbol` - ETF symbol (e.g., "SPY")
205    ///
206    /// # Example
207    ///
208    /// ```no_run
209    /// use fmp_rs::FmpClient;
210    ///
211    /// #[tokio::main]
212    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
213    ///     let client = FmpClient::new()?;
214    ///     let info = client.etf().get_etf_info("SPY").await?;
215    ///     
216    ///     if let Some(etf) = info.first() {
217    ///         println!("ETF: {} ({})", etf.company_name, etf.symbol);
218    ///         println!("AUM: ${:.2}B", etf.aum / 1_000_000_000.0);
219    ///         println!("Expense Ratio: {:.2}%", etf.expense_ratio);
220    ///         println!("Holdings: {}", etf.holdings_count);
221    ///         println!("Inception: {}", etf.inception_date);
222    ///     }
223    ///     Ok(())
224    /// }
225    /// ```
226    pub async fn get_etf_info(&self, symbol: &str) -> Result<Vec<EtfInfo>> {
227        self.client
228            .get(&format!("/api/v4/etf-info?symbol={}", symbol))
229            .await
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_new() {
239        let client = FmpClient::builder().api_key("test_key").build().unwrap();
240        let _ = Etf::new(client);
241    }
242
243    // Golden path tests
244    #[tokio::test]
245    #[ignore = "requires FMP API key"]
246    async fn test_get_etf_list() {
247        let client = FmpClient::new().unwrap();
248        let result = client.etf().get_etf_list().await;
249        assert!(result.is_ok());
250        let etfs = result.unwrap();
251        assert!(!etfs.is_empty());
252    }
253
254    #[tokio::test]
255    #[ignore = "requires FMP API key"]
256    async fn test_search_etf() {
257        let client = FmpClient::new().unwrap();
258        let result = client.etf().search_etf("vanguard", Some(10), None).await;
259        assert!(result.is_ok());
260        let results = result.unwrap();
261        assert!(!results.is_empty());
262        assert!(results.len() <= 10);
263    }
264
265    #[tokio::test]
266    #[ignore = "requires FMP API key"]
267    async fn test_get_etf_holder() {
268        let client = FmpClient::new().unwrap();
269        let result = client.etf().get_etf_holder("SPY").await;
270        assert!(result.is_ok());
271        let holders = result.unwrap();
272        assert!(!holders.is_empty());
273    }
274
275    #[tokio::test]
276    #[ignore = "requires FMP API key"]
277    async fn test_get_etf_holdings() {
278        let client = FmpClient::new().unwrap();
279        let result = client.etf().get_etf_holdings("SPY").await;
280        assert!(result.is_ok());
281        let holdings = result.unwrap();
282        assert!(!holdings.is_empty());
283    }
284
285    #[tokio::test]
286    #[ignore = "requires FMP API key"]
287    async fn test_get_etf_sector_weighting() {
288        let client = FmpClient::new().unwrap();
289        let result = client.etf().get_etf_sector_weighting("SPY").await;
290        assert!(result.is_ok());
291        let sectors = result.unwrap();
292        assert!(!sectors.is_empty());
293    }
294
295    #[tokio::test]
296    #[ignore = "requires FMP API key"]
297    async fn test_get_etf_country_weighting() {
298        let client = FmpClient::new().unwrap();
299        let result = client.etf().get_etf_country_weighting("SPY").await;
300        assert!(result.is_ok());
301        let countries = result.unwrap();
302        assert!(!countries.is_empty());
303    }
304
305    #[tokio::test]
306    #[ignore = "requires FMP API key"]
307    async fn test_get_etf_info() {
308        let client = FmpClient::new().unwrap();
309        let result = client.etf().get_etf_info("SPY").await;
310        assert!(result.is_ok());
311        let info = result.unwrap();
312        assert!(!info.is_empty());
313    }
314
315    // Edge case tests
316    #[tokio::test]
317    #[ignore = "requires FMP API key"]
318    async fn test_search_etf_with_exchange() {
319        let client = FmpClient::new().unwrap();
320        let result = client.etf().search_etf("sp", Some(5), Some("NYSE")).await;
321        assert!(result.is_ok());
322    }
323
324    #[tokio::test]
325    #[ignore = "requires FMP API key"]
326    async fn test_search_etf_no_limit() {
327        let client = FmpClient::new().unwrap();
328        let result = client.etf().search_etf("tech", None, None).await;
329        assert!(result.is_ok());
330    }
331
332    #[tokio::test]
333    #[ignore = "requires FMP API key"]
334    async fn test_get_etf_holder_invalid_symbol() {
335        let client = FmpClient::new().unwrap();
336        let result = client.etf().get_etf_holder("INVALID_ETF_XYZ123").await;
337        // Should either return empty vec or error
338        if let Ok(holders) = result {
339            assert!(holders.is_empty());
340        }
341    }
342
343    #[tokio::test]
344    #[ignore = "requires FMP API key"]
345    async fn test_get_etf_holdings_invalid_symbol() {
346        let client = FmpClient::new().unwrap();
347        let result = client.etf().get_etf_holdings("INVALID_ETF_XYZ123").await;
348        // Should either return empty vec or error
349        if let Ok(holdings) = result {
350            assert!(holdings.is_empty());
351        }
352    }
353
354    #[tokio::test]
355    #[ignore = "requires FMP API key"]
356    async fn test_get_etf_info_multiple_etfs() {
357        let client = FmpClient::new().unwrap();
358        // Test with a well-known ETF
359        let result = client.etf().get_etf_info("QQQ").await;
360        assert!(result.is_ok());
361        let info = result.unwrap();
362        assert!(!info.is_empty());
363        if let Some(etf) = info.first() {
364            assert_eq!(etf.symbol, "QQQ");
365        }
366    }
367
368    #[tokio::test]
369    #[ignore = "requires FMP API key"]
370    async fn test_search_etf_special_characters() {
371        let client = FmpClient::new().unwrap();
372        let result = client.etf().search_etf("S&P", Some(5), None).await;
373        assert!(result.is_ok());
374    }
375
376    // Error handling tests
377    #[tokio::test]
378    async fn test_invalid_api_key() {
379        let client = FmpClient::builder()
380            .api_key("invalid_key_12345")
381            .build()
382            .unwrap();
383        let result = client.etf().get_etf_list().await;
384        assert!(result.is_err());
385    }
386
387    #[tokio::test]
388    async fn test_empty_symbol() {
389        let client = FmpClient::builder().api_key("test_key").build().unwrap();
390        let result = client.etf().get_etf_holder("").await;
391        // Should handle gracefully
392        assert!(result.is_err() || result.unwrap().is_empty());
393    }
394
395    #[tokio::test]
396    async fn test_empty_search_query() {
397        let client = FmpClient::builder().api_key("test_key").build().unwrap();
398        let result = client.etf().search_etf("", Some(10), None).await;
399        // Should handle gracefully
400        assert!(result.is_err() || result.unwrap().is_empty());
401    }
402}