use crate::client::FmpClient;
use crate::error::Result;
use crate::models::company::{StockScreenerResult, TradableSymbol};
use serde::Serialize;
pub struct StockDirectory {
client: FmpClient,
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ScreenerCriteria {
#[serde(skip_serializing_if = "Option::is_none")]
pub market_cap_more_than: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub market_cap_lower_than: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price_more_than: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price_lower_than: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub beta_more_than: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub beta_lower_than: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume_more_than: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume_lower_than: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dividend_more_than: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dividend_lower_than: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_etf: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_actively_trading: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sector: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub industry: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exchange: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
impl StockDirectory {
pub(crate) fn new(client: FmpClient) -> Self {
Self { client }
}
pub async fn screen_stocks(
&self,
criteria: &ScreenerCriteria,
) -> Result<Vec<StockScreenerResult>> {
self.client
.get_with_query("v3/stock-screener", criteria)
.await
}
pub async fn get_tradable_symbols(&self) -> Result<Vec<TradableSymbol>> {
self.client
.get_with_query("v3/available-traded/list", &())
.await
}
pub async fn get_symbols_by_exchange(&self, exchange: &str) -> Result<Vec<TradableSymbol>> {
self.client
.get_with_query(&format!("v3/symbol/{}", exchange), &())
.await
}
pub async fn get_etf_list(&self) -> Result<Vec<TradableSymbol>> {
self.client.get_with_query("v3/etf/list", &()).await
}
pub async fn is_symbol_available(&self, symbol: &str) -> Result<Vec<TradableSymbol>> {
self.client
.get_with_query(&format!("v3/symbol/available-{}", symbol), &())
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let client = FmpClient::builder().api_key("test_key").build().unwrap();
let _ = StockDirectory::new(client);
}
#[test]
fn test_screener_criteria_default() {
let criteria = ScreenerCriteria::default();
assert!(criteria.market_cap_more_than.is_none());
assert!(criteria.is_etf.is_none());
}
#[test]
fn test_screener_criteria_builder() {
let mut criteria = ScreenerCriteria::default();
criteria.market_cap_more_than = Some(1_000_000_000);
criteria.beta_lower_than = Some(1.5);
criteria.is_actively_trading = Some(true);
assert_eq!(criteria.market_cap_more_than, Some(1_000_000_000));
assert_eq!(criteria.beta_lower_than, Some(1.5));
assert_eq!(criteria.is_actively_trading, Some(true));
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_screen_stocks() {
let client = FmpClient::new().unwrap();
let mut criteria = ScreenerCriteria::default();
criteria.market_cap_more_than = Some(10_000_000_000);
criteria.is_etf = Some(false);
criteria.limit = Some(10);
let result = client.stock_directory().screen_stocks(&criteria).await;
assert!(result.is_ok());
let stocks = result.unwrap();
assert!(!stocks.is_empty());
assert!(stocks.len() <= 10);
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_tradable_symbols() {
let client = FmpClient::new().unwrap();
let result = client.stock_directory().get_tradable_symbols().await;
assert!(result.is_ok());
let symbols = result.unwrap();
assert!(!symbols.is_empty());
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_symbols_by_exchange() {
let client = FmpClient::new().unwrap();
let result = client
.stock_directory()
.get_symbols_by_exchange("NASDAQ")
.await;
assert!(result.is_ok());
let symbols = result.unwrap();
assert!(!symbols.is_empty());
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_etf_list() {
let client = FmpClient::new().unwrap();
let result = client.stock_directory().get_etf_list().await;
assert!(result.is_ok());
let etfs = result.unwrap();
assert!(!etfs.is_empty());
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_is_symbol_available() {
let client = FmpClient::new().unwrap();
let result = client.stock_directory().is_symbol_available("AAPL").await;
assert!(result.is_ok());
let symbols = result.unwrap();
assert!(!symbols.is_empty());
assert_eq!(symbols[0].symbol, "AAPL");
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_screen_stocks_empty_criteria() {
let client = FmpClient::new().unwrap();
let criteria = ScreenerCriteria::default();
let result = client.stock_directory().screen_stocks(&criteria).await;
assert!(result.is_ok());
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_screen_stocks_restrictive_criteria() {
let client = FmpClient::new().unwrap();
let mut criteria = ScreenerCriteria::default();
criteria.market_cap_more_than = Some(1_000_000_000_000); criteria.price_more_than = Some(500.0);
criteria.limit = Some(5);
let result = client.stock_directory().screen_stocks(&criteria).await;
assert!(result.is_ok());
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_is_symbol_available_invalid() {
let client = FmpClient::new().unwrap();
let result = client
.stock_directory()
.is_symbol_available("INVALID_XYZ123")
.await;
if let Ok(symbols) = result {
assert!(symbols.is_empty());
}
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_symbols_by_exchange_invalid() {
let client = FmpClient::new().unwrap();
let result = client
.stock_directory()
.get_symbols_by_exchange("INVALID_EXCHANGE")
.await;
if let Ok(symbols) = result {
assert!(symbols.is_empty());
}
}
#[tokio::test]
async fn test_invalid_api_key() {
let client = FmpClient::builder()
.api_key("invalid_key_12345")
.build()
.unwrap();
let result = client.stock_directory().get_tradable_symbols().await;
assert!(result.is_err());
}
}