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 §ors {
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}