Skip to main content

finance_query/providers/
provider.rs

1//! The [`Provider`] identifier.
2
3use serde::de::{Error as DeError, Unexpected};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use super::*;
7
8/// Typed identifier for a financial data provider.
9///
10/// Variants are feature-gated: unavailable providers are excluded at compile time.
11#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub enum Provider {
14    #[default]
15    /// Yahoo Finance (always available).
16    Yahoo,
17    /// Polygon.io (requires `polygon` feature).
18    #[cfg(feature = "polygon")]
19    Polygon,
20    /// Financial Modeling Prep (requires `fmp` feature).
21    #[cfg(feature = "fmp")]
22    Fmp,
23    /// Alpha Vantage (requires `alphavantage` feature).
24    #[cfg(feature = "alphavantage")]
25    AlphaVantage,
26    /// CoinGecko cryptocurrency data (requires `crypto` feature).
27    #[cfg(feature = "crypto")]
28    CoinGecko,
29    /// FRED economic data (requires `fred` feature).
30    #[cfg(feature = "fred")]
31    Fred,
32    /// World Bank Open Data global macro indicators (requires `worldbank` feature, keyless).
33    #[cfg(feature = "worldbank")]
34    WorldBank,
35    /// US Treasury FiscalData (requires `fiscaldata` feature, keyless).
36    #[cfg(feature = "fiscaldata")]
37    FiscalData,
38    /// US Bureau of Labor Statistics (requires `bls` feature; keyless v1,
39    /// keyed v2 when `BLS_API_KEY` is set).
40    #[cfg(feature = "bls")]
41    Bls,
42    /// Frankfurter ECB reference exchange rates (requires `frankfurter` feature, keyless).
43    #[cfg(feature = "frankfurter")]
44    Frankfurter,
45    /// Binance public market data (requires `binance` feature, keyless).
46    #[cfg(feature = "binance")]
47    Binance,
48    /// Kraken public market data (requires `kraken` feature, keyless).
49    #[cfg(feature = "kraken")]
50    Kraken,
51    /// FINRA daily short-sale volume (requires `finra` feature, keyless).
52    #[cfg(feature = "finra")]
53    Finra,
54    /// DefiLlama DeFi TVL data (requires `defi` feature, keyless).
55    #[cfg(feature = "defi")]
56    DefiLlama,
57    /// GDELT DOC 2.0 global news search (requires `gdelt` feature, keyless).
58    #[cfg(feature = "gdelt")]
59    Gdelt,
60    /// CFTC Commitments of Traders futures positioning (requires `cftc`
61    /// feature, keyless).
62    #[cfg(feature = "cftc")]
63    Cftc,
64    /// Nasdaq market-wide earnings/IPO/dividend/split calendars (requires
65    /// `nasdaq` feature, keyless).
66    #[cfg(feature = "nasdaq")]
67    Nasdaq,
68    /// Wikipedia S&P 500 index-constituent table (requires `wikipedia`
69    /// feature, keyless).
70    #[cfg(feature = "wikipedia")]
71    Wikipedia,
72    /// Combined House + Senate PTR stock-trade disclosures (requires the
73    /// `housetrades` and/or `senatetrades` feature, keyless).
74    #[cfg(any(feature = "housetrades", feature = "senatetrades"))]
75    CongressTrades,
76    /// SEC EDGAR filings (always available, keyless).
77    Edgar,
78    /// NYSE/NASDAQ market holidays, computed locally from federal-holiday
79    /// rules rather than fetched (always available, no network call).
80    LocalMarketCalendar,
81    /// A static table of major global exchanges (always available, no
82    /// network call).
83    LocalExchange,
84    /// A provider supplied by a downstream crate and registered with
85    /// `ProvidersBuilder::with_adapter`. Build one with
86    /// [`Provider::custom`], which is the only way to obtain a [`CustomId`].
87    ///
88    /// Interning is per-process, so a custom id deserializes only after
89    /// something in this process has constructed it.
90    ///
91    /// The id is held as an index rather than a string so that `Provider`
92    /// stays small: it is stored in every model's `provider_id`, and a fat
93    /// pointer here grows `Candle` by 22%.
94    Custom(CustomId),
95}
96
97impl Provider {
98    /// Parse a provider id string back to the typed variant.
99    /// Returns `None` if the string doesn't match any known provider.
100    /// Prefer this over string conversion to avoid panics.
101    pub fn from_id_str(s: &str) -> Option<Self> {
102        match s {
103            "yahoo" => Some(Self::Yahoo),
104            #[cfg(feature = "polygon")]
105            "polygon" => Some(Self::Polygon),
106            #[cfg(feature = "fmp")]
107            "fmp" => Some(Self::Fmp),
108            #[cfg(feature = "alphavantage")]
109            "alphavantage" => Some(Self::AlphaVantage),
110            #[cfg(feature = "crypto")]
111            "coingecko" => Some(Self::CoinGecko),
112            #[cfg(feature = "fred")]
113            "fred" => Some(Self::Fred),
114            #[cfg(feature = "worldbank")]
115            "worldbank" => Some(Self::WorldBank),
116            #[cfg(feature = "fiscaldata")]
117            "fiscaldata" => Some(Self::FiscalData),
118            #[cfg(feature = "bls")]
119            "bls" => Some(Self::Bls),
120            #[cfg(feature = "frankfurter")]
121            "frankfurter" => Some(Self::Frankfurter),
122            #[cfg(feature = "binance")]
123            "binance" => Some(Self::Binance),
124            #[cfg(feature = "kraken")]
125            "kraken" => Some(Self::Kraken),
126            #[cfg(feature = "finra")]
127            "finra" => Some(Self::Finra),
128            #[cfg(feature = "defi")]
129            "defillama" => Some(Self::DefiLlama),
130            #[cfg(feature = "gdelt")]
131            "gdelt" => Some(Self::Gdelt),
132            #[cfg(feature = "cftc")]
133            "cftc" => Some(Self::Cftc),
134            #[cfg(feature = "nasdaq")]
135            "nasdaq" => Some(Self::Nasdaq),
136            #[cfg(feature = "wikipedia")]
137            "wikipedia" => Some(Self::Wikipedia),
138            #[cfg(any(feature = "housetrades", feature = "senatetrades"))]
139            "congresstrades" => Some(Self::CongressTrades),
140            "edgar" => Some(Self::Edgar),
141            "local_market_calendar" => Some(Self::LocalMarketCalendar),
142            "local_exchange" => Some(Self::LocalExchange),
143            other => lookup(other).map(Self::Custom),
144        }
145    }
146
147    /// A provider this crate does not build, identified by `id`.
148    ///
149    /// Interning is process-wide and append-only, so the same id always maps
150    /// to the same value and `Provider::custom("x") == Provider::custom("x")`.
151    pub fn custom(id: &'static str) -> Self {
152        Self::Custom(intern(id))
153    }
154
155    /// String identifier matching [`ProviderCore::id`].
156    pub fn as_str(self) -> &'static str {
157        match self {
158            Self::Yahoo => "yahoo",
159            #[cfg(feature = "polygon")]
160            Self::Polygon => "polygon",
161            #[cfg(feature = "fmp")]
162            Self::Fmp => "fmp",
163            #[cfg(feature = "alphavantage")]
164            Self::AlphaVantage => "alphavantage",
165            #[cfg(feature = "crypto")]
166            Self::CoinGecko => "coingecko",
167            #[cfg(feature = "fred")]
168            Self::Fred => "fred",
169            #[cfg(feature = "worldbank")]
170            Self::WorldBank => "worldbank",
171            #[cfg(feature = "fiscaldata")]
172            Self::FiscalData => "fiscaldata",
173            #[cfg(feature = "bls")]
174            Self::Bls => "bls",
175            #[cfg(feature = "frankfurter")]
176            Self::Frankfurter => "frankfurter",
177            #[cfg(feature = "binance")]
178            Self::Binance => "binance",
179            #[cfg(feature = "kraken")]
180            Self::Kraken => "kraken",
181            #[cfg(feature = "finra")]
182            Self::Finra => "finra",
183            #[cfg(feature = "defi")]
184            Self::DefiLlama => "defillama",
185            #[cfg(feature = "gdelt")]
186            Self::Gdelt => "gdelt",
187            #[cfg(feature = "cftc")]
188            Self::Cftc => "cftc",
189            #[cfg(feature = "nasdaq")]
190            Self::Nasdaq => "nasdaq",
191            #[cfg(feature = "wikipedia")]
192            Self::Wikipedia => "wikipedia",
193            #[cfg(any(feature = "housetrades", feature = "senatetrades"))]
194            Self::CongressTrades => "congresstrades",
195            Self::Edgar => "edgar",
196            Self::LocalMarketCalendar => "local_market_calendar",
197            Self::LocalExchange => "local_exchange",
198            Self::Custom(id) => id.as_str(),
199        }
200    }
201
202    /// Every built-in provider variant compiled into this build, whether or
203    /// not it is configured.
204    ///
205    /// Custom providers are absent: they exist only once registered, and this
206    /// answers from the enum rather than from a live [`crate::ProviderSet`].
207    /// Returns a `Vec` rather than an iterator because the variants are
208    /// feature-gated and assembled at runtime.
209    pub fn all() -> Vec<Self> {
210        let mut v = vec![Self::Yahoo];
211        #[cfg(feature = "polygon")]
212        v.push(Self::Polygon);
213        #[cfg(feature = "fmp")]
214        v.push(Self::Fmp);
215        #[cfg(feature = "alphavantage")]
216        v.push(Self::AlphaVantage);
217        #[cfg(feature = "crypto")]
218        v.push(Self::CoinGecko);
219        #[cfg(feature = "fred")]
220        v.push(Self::Fred);
221        #[cfg(feature = "worldbank")]
222        v.push(Self::WorldBank);
223        #[cfg(feature = "fiscaldata")]
224        v.push(Self::FiscalData);
225        #[cfg(feature = "bls")]
226        v.push(Self::Bls);
227        #[cfg(feature = "frankfurter")]
228        v.push(Self::Frankfurter);
229        #[cfg(feature = "binance")]
230        v.push(Self::Binance);
231        #[cfg(feature = "kraken")]
232        v.push(Self::Kraken);
233        #[cfg(feature = "finra")]
234        v.push(Self::Finra);
235        #[cfg(feature = "defi")]
236        v.push(Self::DefiLlama);
237        #[cfg(feature = "gdelt")]
238        v.push(Self::Gdelt);
239        #[cfg(feature = "cftc")]
240        v.push(Self::Cftc);
241        #[cfg(feature = "nasdaq")]
242        v.push(Self::Nasdaq);
243        #[cfg(feature = "wikipedia")]
244        v.push(Self::Wikipedia);
245        #[cfg(any(feature = "housetrades", feature = "senatetrades"))]
246        v.push(Self::CongressTrades);
247        v.push(Self::Edgar);
248        v.push(Self::LocalMarketCalendar);
249        v.push(Self::LocalExchange);
250        v
251    }
252
253    /// Capability bitflags for this provider variant, derived from each
254    /// adapter's `as_*` accessor overrides — implementing a capability trait
255    /// and declaring it can no longer drift apart. Yahoo is the one exception:
256    /// constructing `YahooProvider` needs a live auth handshake, so its set is
257    /// a const declared beside its accessor overrides (`yahoo::CAPS`).
258    ///
259    /// [`Provider::Custom`] returns [`Capability::NONE`]. An id carries no
260    /// adapter, so a custom provider's real set is
261    /// [`ProviderAdapter::capabilities`](crate::ProviderAdapter::capabilities)
262    /// on the registered instance.
263    pub fn capabilities(self) -> Capability {
264        match self {
265            Self::Yahoo => yahoo::CAPS,
266            #[cfg(feature = "polygon")]
267            Self::Polygon => ProviderAdapter::capabilities(&polygon::PolygonProvider),
268            #[cfg(feature = "fmp")]
269            Self::Fmp => ProviderAdapter::capabilities(&fmp::FmpProvider),
270            #[cfg(feature = "alphavantage")]
271            Self::AlphaVantage => {
272                ProviderAdapter::capabilities(&alphavantage::AlphaVantageProvider)
273            }
274            #[cfg(feature = "crypto")]
275            Self::CoinGecko => ProviderAdapter::capabilities(&coingecko::CoinGeckoProvider),
276            #[cfg(feature = "fred")]
277            Self::Fred => ProviderAdapter::capabilities(&fred::FredProvider),
278            #[cfg(feature = "worldbank")]
279            Self::WorldBank => ProviderAdapter::capabilities(&worldbank::WorldBankProvider),
280            #[cfg(feature = "fiscaldata")]
281            Self::FiscalData => ProviderAdapter::capabilities(&fiscaldata::FiscalDataProvider),
282            #[cfg(feature = "bls")]
283            Self::Bls => ProviderAdapter::capabilities(&bls::BlsProvider),
284            #[cfg(feature = "frankfurter")]
285            Self::Frankfurter => ProviderAdapter::capabilities(&frankfurter::FrankfurterProvider),
286            #[cfg(feature = "binance")]
287            Self::Binance => ProviderAdapter::capabilities(&binance::BinanceProvider),
288            #[cfg(feature = "kraken")]
289            Self::Kraken => ProviderAdapter::capabilities(&kraken::KrakenProvider),
290            #[cfg(feature = "finra")]
291            Self::Finra => ProviderAdapter::capabilities(&finra::FinraProvider),
292            #[cfg(feature = "defi")]
293            Self::DefiLlama => ProviderAdapter::capabilities(&defillama::DefiLlamaProvider),
294            #[cfg(feature = "gdelt")]
295            Self::Gdelt => ProviderAdapter::capabilities(&gdelt::GdeltProvider),
296            #[cfg(feature = "cftc")]
297            Self::Cftc => ProviderAdapter::capabilities(&cftc::CftcProvider),
298            #[cfg(feature = "nasdaq")]
299            Self::Nasdaq => ProviderAdapter::capabilities(&nasdaq::NasdaqProvider),
300            #[cfg(feature = "wikipedia")]
301            Self::Wikipedia => ProviderAdapter::capabilities(&wikipedia::WikipediaProvider),
302            #[cfg(any(feature = "housetrades", feature = "senatetrades"))]
303            Self::CongressTrades => {
304                ProviderAdapter::capabilities(&congresstrades::CongressTradesProvider)
305            }
306            Self::Edgar => ProviderAdapter::capabilities(&edgar::EdgarProvider),
307            Self::LocalMarketCalendar => {
308                ProviderAdapter::capabilities(&market_calendar::LocalMarketCalendarProvider)
309            }
310            Self::LocalExchange => {
311                ProviderAdapter::capabilities(&local_exchanges::LocalExchangeProvider)
312            }
313            Self::Custom(_) => Capability::NONE,
314        }
315    }
316}
317
318/// Index of a custom provider id, obtained from [`Provider::custom`].
319///
320/// Opaque so that only an interned id can be named: the index is meaningless
321/// outside the process that registered it.
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
323pub struct CustomId(u16);
324
325impl CustomId {
326    /// The id this index was interned from.
327    pub fn as_str(self) -> &'static str {
328        self.get().unwrap_or(UNKNOWN_CUSTOM)
329    }
330
331    fn get(self) -> Option<&'static str> {
332        let ids = registry().read().ok()?;
333        ids.get(self.0 as usize).copied()
334    }
335}
336
337/// Only reachable past [`MAX_CUSTOM_IDS`], where `intern` stops issuing new
338/// indices rather than wrapping onto another provider's identity.
339const UNKNOWN_CUSTOM: &str = "custom:overflow";
340
341/// `CustomId` is a `u16` so that `Provider` fits the padding every model
342/// already had; a wider index grows `Candle` and its siblings.
343const MAX_CUSTOM_IDS: usize = u16::MAX as usize;
344
345fn registry() -> &'static std::sync::RwLock<Vec<&'static str>> {
346    static IDS: std::sync::OnceLock<std::sync::RwLock<Vec<&'static str>>> =
347        std::sync::OnceLock::new();
348    IDS.get_or_init(|| std::sync::RwLock::new(Vec::new()))
349}
350
351fn intern(id: &'static str) -> CustomId {
352    let mut ids = match registry().write() {
353        Ok(ids) => ids,
354        Err(poisoned) => poisoned.into_inner(),
355    };
356    if let Some(index) = ids.iter().position(|existing| *existing == id) {
357        return CustomId(index as u16);
358    }
359    if ids.len() >= MAX_CUSTOM_IDS {
360        return CustomId(u16::MAX);
361    }
362    ids.push(id);
363    CustomId((ids.len() - 1) as u16)
364}
365
366fn lookup(id: &str) -> Option<CustomId> {
367    registry()
368        .read()
369        .ok()?
370        .iter()
371        .position(|existing| *existing == id)
372        .map(|index| CustomId(index as u16))
373}
374
375impl Serialize for Provider {
376    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
377        serializer.serialize_str(self.as_str())
378    }
379}
380
381/// Borrowed, owned, and escaped inputs all land in `visit_str`, so no shape
382/// allocates. `Cow`'s own `Deserialize` always yields `Owned`.
383struct IdVisitor;
384
385impl serde::de::Visitor<'_> for IdVisitor {
386    type Value = Provider;
387
388    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389        f.write_str("a provider id")
390    }
391
392    fn visit_str<E: DeError>(self, v: &str) -> std::result::Result<Provider, E> {
393        Provider::from_id_str(v).ok_or_else(|| E::invalid_value(Unexpected::Str(v), &self))
394    }
395}
396
397impl<'de> Deserialize<'de> for Provider {
398    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
399        deserializer.deserialize_str(IdVisitor)
400    }
401}
402
403impl std::fmt::Display for Provider {
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        f.write_str(self.as_str())
406    }
407}