Skip to main content

finance_query/adapters/fmp/
screener.rs

1//! Stock screener, symbol search, and CIK lookup endpoints for Financial Modeling Prep.
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9
10/// A result from the stock screener endpoint.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[non_exhaustive]
13pub struct ScreenerResult {
14    /// Ticker symbol.
15    pub symbol: Option<String>,
16    /// Company name.
17    #[serde(rename = "companyName")]
18    pub company_name: Option<String>,
19    /// Market capitalization.
20    #[serde(rename = "marketCap")]
21    pub market_cap: Option<f64>,
22    /// Sector.
23    pub sector: Option<String>,
24    /// Industry.
25    pub industry: Option<String>,
26    /// Beta.
27    pub beta: Option<f64>,
28    /// Current price.
29    pub price: Option<f64>,
30    /// Last annual dividend.
31    #[serde(rename = "lastAnnualDividend")]
32    pub last_annual_dividend: Option<f64>,
33    /// Trading volume.
34    pub volume: Option<f64>,
35    /// Exchange.
36    pub exchange: Option<String>,
37    /// Short exchange name.
38    #[serde(rename = "exchangeShortName")]
39    pub exchange_short_name: Option<String>,
40    /// Country.
41    pub country: Option<String>,
42    /// Whether the symbol is an ETF.
43    #[serde(rename = "isEtf")]
44    pub is_etf: Option<bool>,
45    /// Whether the symbol is a fund.
46    #[serde(rename = "isFund")]
47    pub is_fund: Option<bool>,
48    /// Whether the symbol is actively trading.
49    #[serde(rename = "isActivelyTrading")]
50    pub is_actively_trading: Option<bool>,
51}
52
53/// A result from the symbol search endpoint.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[non_exhaustive]
56pub struct SearchResult {
57    /// Ticker symbol.
58    pub symbol: Option<String>,
59    /// Security name.
60    pub name: Option<String>,
61    /// Currency.
62    pub currency: Option<String>,
63    /// Exchange name.
64    #[serde(rename = "stockExchange")]
65    pub stock_exchange: Option<String>,
66    /// Short exchange name.
67    #[serde(rename = "exchangeShortName")]
68    pub exchange_short_name: Option<String>,
69}
70
71/// A result from the CIK search endpoint.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[non_exhaustive]
74pub struct CikResult {
75    /// Ticker symbol.
76    pub symbol: Option<String>,
77    /// Company CIK number.
78    #[serde(rename = "companyCik")]
79    pub company_cik: Option<String>,
80}
81
82/// Screen stocks by various financial criteria.
83///
84/// * `params` - Query params such as `marketCapMoreThan`, `sector`, `industry`, `country`, `exchange`, `limit`, etc.
85pub async fn stock_screener(params: &[(&str, &str)]) -> Result<Vec<ScreenerResult>> {
86    let client = build_client()?;
87    client.get("/api/v3/stock-screener", params).await
88}
89
90/// Search for symbols matching a query string.
91///
92/// * `query` - Search query (e.g., `"apple"`)
93/// * `limit` - Maximum number of results (optional)
94/// * `exchange` - Filter by exchange (optional)
95pub async fn symbol_search(
96    query: &str,
97    limit: Option<u32>,
98    exchange: Option<&str>,
99) -> Result<Vec<SearchResult>> {
100    let client = build_client()?;
101    let limit_str = limit.map(|l| l.to_string());
102    let mut params: Vec<(&str, &str)> = vec![("query", query)];
103    if let Some(ref l) = limit_str {
104        params.push(("limit", l));
105    }
106    if let Some(e) = exchange {
107        params.push(("exchange", e));
108    }
109    client.get("/api/v3/search", &params).await
110}
111
112/// Look up a company by CIK number.
113///
114/// * `cik` - CIK number (e.g., `"0000320193"`)
115pub async fn cik_search(cik: &str) -> Result<Vec<CikResult>> {
116    let client = build_client()?;
117    let path = format!("/api/v3/cik/{}", encode_path_segment(cik));
118    client.get(&path, &[]).await
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[tokio::test]
126    async fn test_symbol_search_mock() {
127        let mut server = mockito::Server::new_async().await;
128        let _mock = server
129            .mock("GET", "/api/v3/search")
130            .match_query(mockito::Matcher::AllOf(vec![
131                mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
132                mockito::Matcher::UrlEncoded("query".into(), "apple".into()),
133                mockito::Matcher::UrlEncoded("limit".into(), "5".into()),
134            ]))
135            .with_status(200)
136            .with_body(
137                serde_json::json!([
138                    {
139                        "symbol": "AAPL",
140                        "name": "Apple Inc.",
141                        "currency": "USD",
142                        "stockExchange": "NASDAQ",
143                        "exchangeShortName": "NASDAQ"
144                    }
145                ])
146                .to_string(),
147            )
148            .create_async()
149            .await;
150
151        let client = super::super::build_test_client(&server.url()).unwrap();
152        let result: Vec<SearchResult> = client
153            .get("/api/v3/search", &[("query", "apple"), ("limit", "5")])
154            .await
155            .unwrap();
156        assert_eq!(result.len(), 1);
157        assert_eq!(result[0].symbol.as_deref(), Some("AAPL"));
158        assert_eq!(result[0].name.as_deref(), Some("Apple Inc."));
159    }
160}