Skip to main content

finance_query/models/discovery/
reference.rs

1//! Provider-routed symbol reference models.
2//!
3//! Returned by the [`Capability::DISCOVERY`](crate::Capability::DISCOVERY)
4//! route via [`Providers::discovery`](crate::Providers::discovery). These are
5//! provider-neutral shapes — unlike [`SearchResults`](super::search::SearchResults),
6//! which mirrors Yahoo's response for the [`crate::finance::search`] shortcut.
7
8use serde::{Deserialize, Serialize};
9
10/// A symbol matched by a search or listing query.
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12#[non_exhaustive]
13pub struct SymbolMatch {
14    /// Ticker symbol, uppercased.
15    pub symbol: String,
16    /// Provider-native identifier, when the provider uses one distinct from
17    /// the ticker — e.g. the CoinGecko coin id (`"bitcoin"`), which is what
18    /// [`Providers::crypto`](crate::Providers::crypto) accepts. `None` for
19    /// providers whose ticker *is* the identifier.
20    pub id: Option<String>,
21    /// Security or company name.
22    pub name: Option<String>,
23    /// Listing exchange, as reported by the provider.
24    pub exchange: Option<String>,
25    /// Asset type (e.g. `"CS"` for common stock, `"ETF"`).
26    pub asset_type: Option<String>,
27    /// Quote currency.
28    pub currency: Option<String>,
29    /// Whether the symbol is currently active/tradable.
30    pub active: Option<bool>,
31    /// Rank within the provider's universe by market cap (1 = largest).
32    pub market_cap_rank: Option<u32>,
33    /// Small logo/icon URL.
34    pub thumbnail: Option<String>,
35    /// Full-size logo URL.
36    pub image: Option<String>,
37}
38
39/// Detailed reference data for a single symbol.
40#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41#[non_exhaustive]
42pub struct SymbolDetails {
43    /// Ticker symbol.
44    pub symbol: String,
45    /// Company name.
46    pub name: Option<String>,
47    /// Business description.
48    pub description: Option<String>,
49    /// Primary listing exchange.
50    pub exchange: Option<String>,
51    /// Asset type.
52    pub asset_type: Option<String>,
53    /// SEC Central Index Key.
54    pub cik: Option<String>,
55    /// SIC classification code.
56    pub sic_code: Option<String>,
57    /// SIC classification description.
58    pub sic_description: Option<String>,
59    /// Company homepage.
60    pub homepage_url: Option<String>,
61    /// Total employees.
62    pub employees: Option<u64>,
63    /// Market capitalisation.
64    pub market_cap: Option<f64>,
65    /// Date the security was listed (`YYYY-MM-DD`).
66    pub list_date: Option<String>,
67    /// Shares outstanding, weighted across share classes.
68    pub shares_outstanding: Option<f64>,
69}
70
71/// A tradable exchange.
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73#[non_exhaustive]
74pub struct ExchangeInfo {
75    /// Provider-assigned exchange ID.
76    pub id: Option<i64>,
77    /// Exchange name.
78    pub name: Option<String>,
79    /// ISO 10383 Market Identifier Code.
80    pub mic: Option<String>,
81    /// Operating MIC of the parent venue.
82    pub operating_mic: Option<String>,
83    /// Asset class traded (e.g. `"stocks"`, `"options"`).
84    pub asset_class: Option<String>,
85    /// Locale (e.g. `"us"`).
86    pub locale: Option<String>,
87    /// Venue type (e.g. `"exchange"`, `"TRF"`).
88    pub exchange_type: Option<String>,
89    /// Exchange homepage.
90    pub url: Option<String>,
91}
92
93/// A symbol matched by a screener query.
94#[derive(Debug, Clone, Default, Serialize, Deserialize)]
95#[non_exhaustive]
96pub struct ScreenerMatch {
97    /// Ticker symbol.
98    pub symbol: String,
99    /// Company name.
100    pub name: Option<String>,
101    /// Latest price.
102    pub price: Option<f64>,
103    /// Market capitalisation.
104    pub market_cap: Option<f64>,
105    /// GICS-style sector.
106    pub sector: Option<String>,
107    /// Industry classification.
108    pub industry: Option<String>,
109    /// Beta against the broad market.
110    pub beta: Option<f64>,
111    /// Trading volume.
112    pub volume: Option<f64>,
113    /// Listing exchange.
114    pub exchange: Option<String>,
115    /// Country of domicile.
116    pub country: Option<String>,
117    /// Whether the symbol is an ETF.
118    pub is_etf: Option<bool>,
119    /// Whether the symbol is actively trading.
120    pub is_actively_trading: Option<bool>,
121}
122
123/// Filters for a provider-routed screener query.
124///
125/// All fields are optional; unset filters are omitted from the request. Build
126/// with [`ScreenerFilters::new`] and the chainable setters.
127#[derive(Debug, Clone, Default, PartialEq)]
128#[non_exhaustive]
129pub struct ScreenerFilters {
130    /// Minimum market capitalisation.
131    pub market_cap_min: Option<f64>,
132    /// Maximum market capitalisation.
133    pub market_cap_max: Option<f64>,
134    /// Minimum price.
135    pub price_min: Option<f64>,
136    /// Maximum price.
137    pub price_max: Option<f64>,
138    /// Minimum trading volume.
139    pub volume_min: Option<f64>,
140    /// Minimum beta.
141    pub beta_min: Option<f64>,
142    /// Maximum beta.
143    pub beta_max: Option<f64>,
144    /// Sector name filter.
145    pub sector: Option<String>,
146    /// Industry name filter.
147    pub industry: Option<String>,
148    /// Exchange filter.
149    pub exchange: Option<String>,
150    /// Country filter.
151    pub country: Option<String>,
152    /// Restrict to actively trading symbols.
153    pub actively_trading: Option<bool>,
154    /// Maximum number of results.
155    pub limit: Option<u32>,
156}
157
158impl ScreenerFilters {
159    /// An empty filter set — matches the provider's default universe.
160    pub fn new() -> Self {
161        Self::default()
162    }
163
164    /// Restrict market capitalisation to `[min, max]`.
165    pub fn market_cap(mut self, min: Option<f64>, max: Option<f64>) -> Self {
166        self.market_cap_min = min;
167        self.market_cap_max = max;
168        self
169    }
170
171    /// Restrict price to `[min, max]`.
172    pub fn price(mut self, min: Option<f64>, max: Option<f64>) -> Self {
173        self.price_min = min;
174        self.price_max = max;
175        self
176    }
177
178    /// Restrict beta to `[min, max]`.
179    pub fn beta(mut self, min: Option<f64>, max: Option<f64>) -> Self {
180        self.beta_min = min;
181        self.beta_max = max;
182        self
183    }
184
185    /// Require at least `min` traded volume.
186    pub fn volume_min(mut self, min: f64) -> Self {
187        self.volume_min = Some(min);
188        self
189    }
190
191    /// Restrict to a sector.
192    pub fn sector(mut self, sector: impl Into<String>) -> Self {
193        self.sector = Some(sector.into());
194        self
195    }
196
197    /// Restrict to an industry.
198    pub fn industry(mut self, industry: impl Into<String>) -> Self {
199        self.industry = Some(industry.into());
200        self
201    }
202
203    /// Restrict to an exchange.
204    pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
205        self.exchange = Some(exchange.into());
206        self
207    }
208
209    /// Restrict to a country of domicile.
210    pub fn country(mut self, country: impl Into<String>) -> Self {
211        self.country = Some(country.into());
212        self
213    }
214
215    /// Restrict to symbols that are actively trading.
216    pub fn actively_trading(mut self, actively_trading: bool) -> Self {
217        self.actively_trading = Some(actively_trading);
218        self
219    }
220
221    /// Cap the number of results returned.
222    pub fn limit(mut self, limit: u32) -> Self {
223        self.limit = Some(limit);
224        self
225    }
226
227    /// Render as provider query-string pairs, omitting unset filters.
228    ///
229    /// Uses Financial Modeling Prep's parameter names — the only provider
230    /// currently routed for `Capability::DISCOVERY` screening.
231    #[cfg(feature = "fmp")]
232    pub(crate) fn to_query(&self) -> Vec<(&'static str, String)> {
233        let mut q: Vec<(&'static str, String)> = Vec::new();
234        let mut num = |k: &'static str, v: Option<f64>| {
235            if let Some(v) = v {
236                q.push((k, v.to_string()));
237            }
238        };
239        num("marketCapMoreThan", self.market_cap_min);
240        num("marketCapLowerThan", self.market_cap_max);
241        num("priceMoreThan", self.price_min);
242        num("priceLowerThan", self.price_max);
243        num("volumeMoreThan", self.volume_min);
244        num("betaMoreThan", self.beta_min);
245        num("betaLowerThan", self.beta_max);
246        for (k, v) in [
247            ("sector", &self.sector),
248            ("industry", &self.industry),
249            ("exchange", &self.exchange),
250            ("country", &self.country),
251        ] {
252            if let Some(v) = v {
253                q.push((k, v.clone()));
254            }
255        }
256        if let Some(v) = self.actively_trading {
257            q.push(("isActivelyTrading", v.to_string()));
258        }
259        if let Some(v) = self.limit {
260            q.push(("limit", v.to_string()));
261        }
262        q
263    }
264}
265
266// Gated as a whole: every test here exercises `to_query`, which is fmp-only.
267#[cfg(all(test, feature = "fmp"))]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn empty_filters_render_no_query_params() {
273        assert!(ScreenerFilters::new().to_query().is_empty());
274    }
275
276    #[test]
277    fn set_filters_render_with_provider_parameter_names() {
278        let q = ScreenerFilters::new()
279            .market_cap(Some(1e9), None)
280            .price(None, Some(50.0))
281            .sector("Technology")
282            .actively_trading(true)
283            .limit(25)
284            .to_query();
285
286        assert_eq!(
287            q,
288            vec![
289                ("marketCapMoreThan", "1000000000".to_string()),
290                ("priceLowerThan", "50".to_string()),
291                ("sector", "Technology".to_string()),
292                ("isActivelyTrading", "true".to_string()),
293                ("limit", "25".to_string()),
294            ]
295        );
296    }
297}