Skip to main content

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}