finance_query/adapters/fmp/
screener.rs1use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12#[non_exhaustive]
13pub struct ScreenerResult {
14 pub symbol: Option<String>,
16 #[serde(rename = "companyName")]
18 pub company_name: Option<String>,
19 #[serde(rename = "marketCap")]
21 pub market_cap: Option<f64>,
22 pub sector: Option<String>,
24 pub industry: Option<String>,
26 pub beta: Option<f64>,
28 pub price: Option<f64>,
30 #[serde(rename = "lastAnnualDividend")]
32 pub last_annual_dividend: Option<f64>,
33 pub volume: Option<f64>,
35 pub exchange: Option<String>,
37 #[serde(rename = "exchangeShortName")]
39 pub exchange_short_name: Option<String>,
40 pub country: Option<String>,
42 #[serde(rename = "isEtf")]
44 pub is_etf: Option<bool>,
45 #[serde(rename = "isFund")]
47 pub is_fund: Option<bool>,
48 #[serde(rename = "isActivelyTrading")]
50 pub is_actively_trading: Option<bool>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55#[non_exhaustive]
56pub struct SearchResult {
57 pub symbol: Option<String>,
59 pub name: Option<String>,
61 pub currency: Option<String>,
63 #[serde(rename = "stockExchange")]
65 pub stock_exchange: Option<String>,
66 #[serde(rename = "exchangeShortName")]
68 pub exchange_short_name: Option<String>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73#[non_exhaustive]
74pub struct CikResult {
75 pub symbol: Option<String>,
77 #[serde(rename = "companyCik")]
79 pub company_cik: Option<String>,
80}
81
82pub 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
90pub 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", ¶ms).await
110}
111
112pub 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}