finance_query/domains/discovery.rs
1//! Symbol discovery handle.
2//!
3//! Created via [`Providers::discovery`](crate::Providers::discovery).
4
5use std::sync::Arc;
6
7use crate::error::Result;
8use crate::models::discovery::reference::{
9 ExchangeInfo, ScreenerFilters, ScreenerMatch, SymbolDetails, SymbolMatch,
10};
11use crate::providers::Capability;
12
13domain_handle! {
14 /// Symbol discovery backed by configured data providers.
15 ///
16 /// Unlike [`crate::finance::search`] — a Yahoo-only convenience shortcut —
17 /// this routes through [`Capability::DISCOVERY`], so it honours the provider
18 /// priority configured on [`Providers::builder`](crate::Providers::builder)
19 /// and falls back across providers.
20 ///
21 /// Created via [`Providers::discovery`](crate::Providers::discovery).
22 pub struct Discovery
23 caches: {
24 cache: Vec<SymbolMatch>,
25 details_cache: SymbolDetails,
26 }
27}
28
29impl Discovery {
30 /// Search the configured providers' symbol universe.
31 ///
32 /// Results are cached per `(query, limit)` pair.
33 pub async fn search(&self, query: &str, limit: u32) -> Result<Vec<SymbolMatch>> {
34 let key = format!("{query}\u{1f}{limit}");
35 let providers = Arc::clone(&self.providers);
36 let query = query.to_string();
37 self.cache
38 .get_or_try(key, move || async move {
39 providers
40 .fetch(Capability::DISCOVERY, move |p| {
41 let query = query.clone();
42 let p = p.clone();
43 async move {
44 p.as_discovery()
45 .ok_or_else(|| {
46 p.not_supported(crate::providers::Operation::SymbolSearch)
47 })?
48 .fetch_symbol_search(&query, limit)
49 .await
50 }
51 })
52 .await
53 })
54 .await
55 }
56
57 /// Fetch detailed reference data for one symbol.
58 pub async fn details(&self, symbol: &str) -> Result<SymbolDetails> {
59 let providers = Arc::clone(&self.providers);
60 let symbol = symbol.to_string();
61 let key = symbol.clone();
62 self.details_cache
63 .get_or_try(key, move || async move {
64 providers
65 .fetch(Capability::DISCOVERY, move |p| {
66 let symbol = symbol.clone();
67 let p = p.clone();
68 async move {
69 p.as_discovery()
70 .ok_or_else(|| {
71 p.not_supported(crate::providers::Operation::SymbolDetails)
72 })?
73 .fetch_symbol_details(&symbol)
74 .await
75 }
76 })
77 .await
78 })
79 .await
80 }
81
82 /// Fetch the tradable exchange listing.
83 pub async fn exchanges(&self) -> Result<Vec<ExchangeInfo>> {
84 dispatch_via!(
85 self,
86 DISCOVERY,
87 as_discovery,
88 Exchanges,
89 fetch_exchanges,
90 []
91 )
92 }
93
94 /// Fetch the providers' whole listed-security universe.
95 ///
96 /// `active = false` asks for delisted securities instead. This is an
97 /// unfiltered dump — expect thousands of rows in one response — so prefer
98 /// [`search`](Self::search) when you have a query. Cached per `active`.
99 /// EDGAR serves `active = true` keylessly from SEC's bulk ticker files
100 /// (no exchange-listing history, so `active = false` isn't supported
101 /// there); Alpha Vantage and FMP serve both.
102 pub async fn listing_status(&self, active: bool) -> Result<Vec<SymbolMatch>> {
103 let providers = Arc::clone(&self.providers);
104 self.cache
105 .get_or_try(
106 format!("listing_status\u{1f}{active}"),
107 move || async move {
108 providers
109 .fetch(Capability::DISCOVERY, move |p| {
110 let p = p.clone();
111 async move {
112 p.as_discovery()
113 .ok_or_else(|| {
114 p.not_supported(crate::providers::Operation::ListingStatus)
115 })?
116 .fetch_listing_status(active)
117 .await
118 }
119 })
120 .await
121 },
122 )
123 .await
124 }
125
126 /// Run a screener query over the providers' universe.
127 ///
128 /// Not cached — screener filters are open-ended and results are
129 /// price-sensitive, so every call fetches fresh.
130 pub async fn screener(&self, filters: &ScreenerFilters) -> Result<Vec<ScreenerMatch>> {
131 let filters = filters.clone();
132 dispatch_via!(
133 self,
134 DISCOVERY,
135 as_discovery,
136 Screener,
137 fetch_screener,
138 [filters],
139 &filters
140 )
141 }
142}