fmp_rs/endpoints/
stock_directory.rs1use crate::client::FmpClient;
4use crate::error::Result;
5use crate::models::company::{StockScreenerResult, TradableSymbol};
6use serde::Serialize;
7
8pub struct StockDirectory {
10 client: FmpClient,
11}
12
13#[derive(Debug, Clone, Default, Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct ScreenerCriteria {
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub market_cap_more_than: Option<i64>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub market_cap_lower_than: Option<i64>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub price_more_than: Option<f64>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub price_lower_than: Option<f64>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub beta_more_than: Option<f64>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub beta_lower_than: Option<f64>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub volume_more_than: Option<i64>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub volume_lower_than: Option<i64>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub dividend_more_than: Option<f64>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub dividend_lower_than: Option<f64>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub is_etf: Option<bool>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub is_actively_trading: Option<bool>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub sector: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub industry: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub country: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub exchange: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub limit: Option<u32>,
51}
52
53impl StockDirectory {
54 pub(crate) fn new(client: FmpClient) -> Self {
55 Self { client }
56 }
57
58 pub async fn screen_stocks(
87 &self,
88 criteria: &ScreenerCriteria,
89 ) -> Result<Vec<StockScreenerResult>> {
90 self.client
91 .get_with_query("v3/stock-screener", criteria)
92 .await
93 }
94
95 pub async fn get_tradable_symbols(&self) -> Result<Vec<TradableSymbol>> {
112 self.client
113 .get_with_query("v3/available-traded/list", &())
114 .await
115 }
116
117 pub async fn get_symbols_by_exchange(&self, exchange: &str) -> Result<Vec<TradableSymbol>> {
134 self.client
135 .get_with_query(&format!("v3/symbol/{}", exchange), &())
136 .await
137 }
138
139 pub async fn get_etf_list(&self) -> Result<Vec<TradableSymbol>> {
156 self.client.get_with_query("v3/etf/list", &()).await
157 }
158
159 pub async fn is_symbol_available(&self, symbol: &str) -> Result<Vec<TradableSymbol>> {
178 self.client
179 .get_with_query(&format!("v3/symbol/available-{}", symbol), &())
180 .await
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_new() {
190 let client = FmpClient::builder().api_key("test_key").build().unwrap();
191 let _ = StockDirectory::new(client);
192 }
193
194 #[test]
195 fn test_screener_criteria_default() {
196 let criteria = ScreenerCriteria::default();
197 assert!(criteria.market_cap_more_than.is_none());
198 assert!(criteria.is_etf.is_none());
199 }
200
201 #[test]
202 fn test_screener_criteria_builder() {
203 let mut criteria = ScreenerCriteria::default();
204 criteria.market_cap_more_than = Some(1_000_000_000);
205 criteria.beta_lower_than = Some(1.5);
206 criteria.is_actively_trading = Some(true);
207
208 assert_eq!(criteria.market_cap_more_than, Some(1_000_000_000));
209 assert_eq!(criteria.beta_lower_than, Some(1.5));
210 assert_eq!(criteria.is_actively_trading, Some(true));
211 }
212
213 #[tokio::test]
215 #[ignore = "requires FMP API key"]
216 async fn test_screen_stocks() {
217 let client = FmpClient::new().unwrap();
218 let mut criteria = ScreenerCriteria::default();
219 criteria.market_cap_more_than = Some(10_000_000_000);
220 criteria.is_etf = Some(false);
221 criteria.limit = Some(10);
222
223 let result = client.stock_directory().screen_stocks(&criteria).await;
224 assert!(result.is_ok());
225 let stocks = result.unwrap();
226 assert!(!stocks.is_empty());
227 assert!(stocks.len() <= 10);
228 }
229
230 #[tokio::test]
231 #[ignore = "requires FMP API key"]
232 async fn test_get_tradable_symbols() {
233 let client = FmpClient::new().unwrap();
234 let result = client.stock_directory().get_tradable_symbols().await;
235 assert!(result.is_ok());
236 let symbols = result.unwrap();
237 assert!(!symbols.is_empty());
238 }
239
240 #[tokio::test]
241 #[ignore = "requires FMP API key"]
242 async fn test_get_symbols_by_exchange() {
243 let client = FmpClient::new().unwrap();
244 let result = client
245 .stock_directory()
246 .get_symbols_by_exchange("NASDAQ")
247 .await;
248 assert!(result.is_ok());
249 let symbols = result.unwrap();
250 assert!(!symbols.is_empty());
251 }
252
253 #[tokio::test]
254 #[ignore = "requires FMP API key"]
255 async fn test_get_etf_list() {
256 let client = FmpClient::new().unwrap();
257 let result = client.stock_directory().get_etf_list().await;
258 assert!(result.is_ok());
259 let etfs = result.unwrap();
260 assert!(!etfs.is_empty());
261 }
262
263 #[tokio::test]
264 #[ignore = "requires FMP API key"]
265 async fn test_is_symbol_available() {
266 let client = FmpClient::new().unwrap();
267 let result = client.stock_directory().is_symbol_available("AAPL").await;
268 assert!(result.is_ok());
269 let symbols = result.unwrap();
270 assert!(!symbols.is_empty());
271 assert_eq!(symbols[0].symbol, "AAPL");
272 }
273
274 #[tokio::test]
276 #[ignore = "requires FMP API key"]
277 async fn test_screen_stocks_empty_criteria() {
278 let client = FmpClient::new().unwrap();
279 let criteria = ScreenerCriteria::default();
280 let result = client.stock_directory().screen_stocks(&criteria).await;
281 assert!(result.is_ok());
282 }
283
284 #[tokio::test]
285 #[ignore = "requires FMP API key"]
286 async fn test_screen_stocks_restrictive_criteria() {
287 let client = FmpClient::new().unwrap();
288 let mut criteria = ScreenerCriteria::default();
289 criteria.market_cap_more_than = Some(1_000_000_000_000); criteria.price_more_than = Some(500.0);
291 criteria.limit = Some(5);
292
293 let result = client.stock_directory().screen_stocks(&criteria).await;
294 assert!(result.is_ok());
295 }
297
298 #[tokio::test]
299 #[ignore = "requires FMP API key"]
300 async fn test_is_symbol_available_invalid() {
301 let client = FmpClient::new().unwrap();
302 let result = client
303 .stock_directory()
304 .is_symbol_available("INVALID_XYZ123")
305 .await;
306 if let Ok(symbols) = result {
307 assert!(symbols.is_empty());
308 }
309 }
310
311 #[tokio::test]
312 #[ignore = "requires FMP API key"]
313 async fn test_get_symbols_by_exchange_invalid() {
314 let client = FmpClient::new().unwrap();
315 let result = client
316 .stock_directory()
317 .get_symbols_by_exchange("INVALID_EXCHANGE")
318 .await;
319 if let Ok(symbols) = result {
321 assert!(symbols.is_empty());
322 }
323 }
324
325 #[tokio::test]
327 async fn test_invalid_api_key() {
328 let client = FmpClient::builder()
329 .api_key("invalid_key_12345")
330 .build()
331 .unwrap();
332 let result = client.stock_directory().get_tradable_symbols().await;
333 assert!(result.is_err());
334 }
335}