1pub mod config;
4
5#[cfg(feature = "alphavantage")]
6pub(crate) mod alphavantage;
7#[cfg(feature = "crypto")]
8pub(crate) mod coingecko;
9pub(crate) mod edgar;
10#[cfg(feature = "fmp")]
11pub(crate) mod fmp;
12#[cfg(feature = "fred")]
13pub(crate) mod fred;
14#[cfg(feature = "polygon")]
15pub(crate) mod polygon;
16pub(crate) mod types;
17pub(crate) mod yahoo;
18
19use crate::adapters::yahoo::client::{ClientConfig, YahooClient};
20use crate::error::{FinanceError, Result};
21use crate::models::quote::QuoteSummaryResponse;
22use futures::stream::StreamExt;
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25use std::sync::Arc;
26
27#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub enum Provider {
32 #[default]
33 Yahoo,
35 #[cfg(feature = "polygon")]
37 Polygon,
38 #[cfg(feature = "fmp")]
40 Fmp,
41 #[cfg(feature = "alphavantage")]
43 AlphaVantage,
44 #[cfg(feature = "crypto")]
46 CoinGecko,
47 #[cfg(feature = "fred")]
49 Fred,
50 Edgar,
52}
53
54impl Provider {
55 pub fn from_id_str(s: &str) -> Option<Self> {
59 match s {
60 "yahoo" => Some(Self::Yahoo),
61 #[cfg(feature = "polygon")]
62 "polygon" => Some(Self::Polygon),
63 #[cfg(feature = "fmp")]
64 "fmp" => Some(Self::Fmp),
65 #[cfg(feature = "alphavantage")]
66 "alphavantage" => Some(Self::AlphaVantage),
67 #[cfg(feature = "crypto")]
68 "coingecko" => Some(Self::CoinGecko),
69 #[cfg(feature = "fred")]
70 "fred" => Some(Self::Fred),
71 "edgar" => Some(Self::Edgar),
72 _ => None,
73 }
74 }
75
76 pub fn as_str(self) -> &'static str {
78 match self {
79 Self::Yahoo => "yahoo",
80 #[cfg(feature = "polygon")]
81 Self::Polygon => "polygon",
82 #[cfg(feature = "fmp")]
83 Self::Fmp => "fmp",
84 #[cfg(feature = "alphavantage")]
85 Self::AlphaVantage => "alphavantage",
86 #[cfg(feature = "crypto")]
87 Self::CoinGecko => "coingecko",
88 #[cfg(feature = "fred")]
89 Self::Fred => "fred",
90 Self::Edgar => "edgar",
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum Fetch {
98 Sequential,
100 Parallel,
102 #[deprecated(since = "2.6.0", note = "Use `Fetch::Parallel` instead")]
105 All,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
113pub struct Capability(u32);
114
115impl Capability {
116 pub const QUOTE: Self = Self(1 << 0);
118 pub const CHART: Self = Self(1 << 1);
120 pub const FUNDAMENTALS: Self = Self(1 << 2);
122 pub const CORPORATE: Self = Self(1 << 3);
124 pub const OPTIONS: Self = Self(1 << 4);
126 pub const CRYPTO: Self = Self(1 << 6);
130 pub const ECONOMIC: Self = Self(1 << 7);
132 pub const FOREX: Self = Self(1 << 9);
136 pub const INDICES: Self = Self(1 << 10);
138 pub const FUTURES: Self = Self(1 << 11);
140 pub const COMMODITIES: Self = Self(1 << 12);
142 pub const FILINGS: Self = Self(1 << 14);
146
147 pub const fn contains(self, other: Self) -> bool {
149 (self.0 & other.0) == other.0
150 }
151
152 pub fn name(self) -> &'static str {
156 match self.0 {
157 x if x == Self::QUOTE.0 => "quote",
158 x if x == Self::CHART.0 => "chart",
159 x if x == Self::FUNDAMENTALS.0 => "fundamentals",
160 x if x == Self::CORPORATE.0 => "corporate",
161 x if x == Self::OPTIONS.0 => "options",
162 x if x == Self::CRYPTO.0 => "crypto",
163 x if x == Self::ECONOMIC.0 => "economic",
164 x if x == Self::FOREX.0 => "forex",
165 x if x == Self::INDICES.0 => "indices",
166 x if x == Self::FUTURES.0 => "futures",
167 x if x == Self::COMMODITIES.0 => "commodities",
168 x if x == Self::FILINGS.0 => "filings",
169 _ => "unknown",
170 }
171 }
172}
173
174impl std::ops::BitOr for Capability {
175 type Output = Self;
176 fn bitor(self, rhs: Self) -> Self {
177 Self(self.0 | rhs.0)
178 }
179}
180
181pub struct Routes {
186 pub(crate) map: HashMap<Capability, Vec<Provider>>,
187 pub(crate) fetch: Fetch,
188}
189
190impl Routes {
191 pub fn new(fetch: Fetch) -> Self {
192 Self {
193 map: HashMap::new(),
194 fetch,
195 }
196 }
197}
198
199#[async_trait::async_trait]
200pub(crate) trait ProviderAdapter: Send + Sync {
201 fn id(&self) -> &'static str;
202 fn capabilities(&self) -> Capability;
203
204 async fn initialize(&self) -> Result<()> {
206 Ok(())
207 }
208
209 fn not_supported(&self, operation: &'static str) -> FinanceError {
210 FinanceError::NotSupported {
211 provider: self.id(),
212 operation,
213 }
214 }
215
216 async fn fetch_quote(&self, _: &str) -> Result<QuoteSummaryResponse> {
219 Err(self.not_supported("quote"))
220 }
221 async fn fetch_chart(
222 &self,
223 _: &str,
224 _: crate::Interval,
225 _: crate::TimeRange,
226 ) -> Result<crate::models::chart::Chart> {
227 Err(self.not_supported("chart"))
228 }
229 async fn fetch_chart_range(
230 &self,
231 _: &str,
232 _: crate::Interval,
233 _: i64,
234 _: i64,
235 ) -> Result<crate::models::chart::Chart> {
236 Err(self.not_supported("chart_range"))
237 }
238 async fn fetch_financials(
239 &self,
240 _: &str,
241 _: crate::StatementType,
242 _: crate::Frequency,
243 ) -> Result<crate::models::fundamentals::FinancialStatement> {
244 Err(self.not_supported("financials"))
245 }
246 async fn fetch_news(&self, _: &str) -> Result<Vec<crate::models::corporate::news::News>> {
247 Err(self.not_supported("news"))
248 }
249 async fn fetch_similar_symbols(
250 &self,
251 _: &str,
252 _: u32,
253 ) -> Result<Vec<crate::models::corporate::recommendation::SimilarSymbol>> {
254 Err(self.not_supported("recommendations"))
255 }
256 async fn fetch_options(
257 &self,
258 _: &str,
259 _: Option<i64>,
260 ) -> Result<crate::models::options::Options> {
261 Err(self.not_supported("options"))
262 }
263 async fn fetch_events(&self, _: &str) -> Result<crate::models::chart::events::ChartEvents> {
264 Err(self.not_supported("events"))
265 }
266 async fn fetch_quotes_batch(&self, _: &[&str]) -> Result<Vec<(String, QuoteSummaryResponse)>> {
270 Err(self.not_supported("quotes_batch"))
271 }
272
273 #[cfg(any(
274 feature = "crypto",
275 feature = "alphavantage",
276 feature = "fmp",
277 feature = "polygon"
278 ))]
279 async fn fetch_crypto_quote(
280 &self,
281 _: &str,
282 _: &str,
283 ) -> Result<crate::models::crypto::CryptoQuote> {
284 Err(self.not_supported("crypto_quote"))
285 }
286
287 #[cfg(any(feature = "fred", feature = "alphavantage", feature = "polygon"))]
288 async fn fetch_economic_series(
289 &self,
290 _: &str,
291 ) -> Result<crate::models::economic::EconomicSeries> {
292 Err(self.not_supported("economic_series"))
293 }
294
295 #[cfg(any(feature = "polygon", feature = "fmp", feature = "alphavantage"))]
296 async fn fetch_forex_quote(
297 &self,
298 _from: &str,
299 _to: &str,
300 ) -> Result<crate::models::forex::ForexQuote> {
301 Err(self.not_supported("forex_quote"))
302 }
303
304 #[cfg(any(feature = "polygon", feature = "fmp"))]
305 async fn fetch_indices_quote(&self, _: &str) -> Result<crate::models::indices::IndexQuote> {
306 Err(self.not_supported("indices_quote"))
307 }
308
309 #[cfg(feature = "polygon")]
310 async fn fetch_futures_quote(&self, _: &str) -> Result<crate::models::futures::FuturesQuote> {
311 Err(self.not_supported("futures_quote"))
312 }
313
314 #[cfg(any(feature = "fmp", feature = "alphavantage"))]
315 async fn fetch_commodities_quote(
316 &self,
317 _: &str,
318 ) -> Result<crate::models::commodities::CommodityQuote> {
319 Err(self.not_supported("commodities_quote"))
320 }
321
322 async fn fetch_filings(&self, _: &str) -> Result<crate::models::filings::ProviderFilings> {
323 Err(self.not_supported("filings"))
324 }
325}
326
327pub(crate) struct ProviderSet {
328 providers: Vec<Arc<dyn ProviderAdapter>>,
329 yahoo_client: Option<Arc<YahooClient>>,
330 routes: Routes,
331}
332
333impl ProviderSet {
334 pub fn new(
335 providers: Vec<Arc<dyn ProviderAdapter>>,
336 yahoo_client: Option<Arc<YahooClient>>,
337 routes: Routes,
338 ) -> Self {
339 Self {
340 providers,
341 yahoo_client,
342 routes,
343 }
344 }
345
346 fn candidates_for(&self, cap: Capability) -> Vec<&Arc<dyn ProviderAdapter>> {
350 if let Some(provider_ids) = self.routes.map.get(&cap) {
351 provider_ids
352 .iter()
353 .filter_map(|id| self.providers.iter().find(|p| p.id() == id.as_str()))
354 .collect()
355 } else if cap == Capability::FILINGS {
356 let mut v: Vec<&Arc<dyn ProviderAdapter>> = self
358 .providers
359 .iter()
360 .filter(|p| p.id() == "edgar")
361 .collect();
362 v.extend(self.providers.iter().filter(|p| p.id() == "yahoo"));
363 v
364 } else {
365 self.providers
367 .iter()
368 .filter(|p| p.id() == "yahoo")
369 .collect()
370 }
371 }
372
373 fn no_provider(cap: Capability) -> FinanceError {
374 FinanceError::NoProviderAvailable {
375 operation: cap.name(),
376 }
377 }
378
379 fn finish_err(cap: Capability, last: Option<FinanceError>) -> FinanceError {
380 last.unwrap_or_else(|| Self::no_provider(cap))
381 }
382
383 #[allow(deprecated)] pub(crate) async fn fetch<T, F, Fut>(&self, cap: Capability, f: F) -> Result<T>
385 where
386 F: Fn(&Arc<dyn ProviderAdapter>) -> Fut,
387 Fut: std::future::Future<Output = Result<T>>,
388 {
389 let candidates = self.candidates_for(cap);
390 if candidates.is_empty() {
391 return Err(Self::no_provider(cap));
392 }
393 match self.routes.fetch {
394 Fetch::Sequential => {
395 let mut last = None;
396 for p in &candidates {
397 match f(p).await {
398 Ok(v) => return Ok(v),
399 Err(FinanceError::NotSupported { .. }) => continue,
400 Err(e) => last = Some(e),
401 }
402 }
403 Err(Self::finish_err(cap, last))
404 }
405 Fetch::Parallel | Fetch::All => {
406 let mut futs = futures::stream::FuturesUnordered::new();
407 for p in &candidates {
408 futs.push(f(p));
409 }
410 let mut last = None;
411 while let Some(r) = futs.next().await {
412 match r {
413 Ok(v) => return Ok(v),
414 Err(FinanceError::NotSupported { .. }) => continue,
415 Err(e) => last = Some(e),
416 }
417 }
418 Err(Self::finish_err(cap, last))
419 }
420 }
421 }
422
423 pub(crate) fn first_yahoo(&self) -> Result<Arc<YahooClient>> {
424 self.yahoo_client
425 .as_ref()
426 .map(Arc::clone)
427 .ok_or_else(|| FinanceError::NoProviderAvailable { operation: "yahoo" })
428 }
429}
430
431#[allow(dead_code)] pub(crate) fn json_value_to_f64(value: serde_json::Value) -> Option<f64> {
433 value
434 .as_f64()
435 .or_else(|| value.as_i64().map(|v| v as f64))
436 .or_else(|| value.as_u64().map(|v| v as f64))
437 .or_else(|| value.as_str().and_then(|s| s.parse::<f64>().ok()))
438 .or_else(|| {
439 value
440 .get("raw")
441 .and_then(|raw| raw.as_f64().or_else(|| raw.as_i64().map(|v| v as f64)))
442 })
443}
444
445#[allow(dead_code)] pub(crate) fn build_financial_statement(
447 symbol: String,
448 statement_type: String,
449 frequency: String,
450 provider_id: Provider,
451 data: std::collections::HashMap<String, std::collections::HashMap<String, serde_json::Value>>,
452) -> crate::models::fundamentals::FinancialStatement {
453 let statement = data
454 .into_iter()
455 .filter_map(|(metric, values)| {
456 let values: std::collections::HashMap<String, f64> = values
457 .into_iter()
458 .filter_map(|(date, value)| json_value_to_f64(value).map(|v| (date, v)))
459 .collect();
460 if values.is_empty() {
461 None
462 } else {
463 Some((metric, values))
464 }
465 })
466 .collect();
467 crate::models::fundamentals::FinancialStatement {
468 symbol,
469 statement_type,
470 frequency,
471 statement,
472 provider_id: Some(provider_id),
473 }
474}
475
476pub(crate) fn build_options(
477 symbol: String,
478 provider_id: Provider,
479 expiration_dates: Vec<i64>,
480 calls: Vec<crate::models::options::OptionContract>,
481 puts: Vec<crate::models::options::OptionContract>,
482) -> crate::models::options::Options {
483 use std::collections::BTreeMap;
484
485 let mut chains_by_expiration: BTreeMap<
486 i64,
487 (
488 Vec<crate::models::options::OptionContract>,
489 Vec<crate::models::options::OptionContract>,
490 ),
491 > = BTreeMap::new();
492
493 for contract in calls {
494 let exp = contract.expiration.unwrap_or(0);
495 chains_by_expiration
496 .entry(exp)
497 .or_default()
498 .0
499 .push(contract);
500 }
501 for contract in puts {
502 let exp = contract.expiration.unwrap_or(0);
503 chains_by_expiration
504 .entry(exp)
505 .or_default()
506 .1
507 .push(contract);
508 }
509
510 let option_chains: Vec<crate::models::options::response::OptionChainData> =
511 chains_by_expiration
512 .into_iter()
513 .map(
514 |(expiration, (c, p))| crate::models::options::response::OptionChainData {
515 expiration_date: expiration,
516 has_mini_options: None,
517 calls: Some(c),
518 puts: Some(p),
519 },
520 )
521 .collect();
522
523 let expiration_dates = if expiration_dates.is_empty() {
524 option_chains
525 .iter()
526 .map(|chain| chain.expiration_date)
527 .collect()
528 } else {
529 let mut v: Vec<i64> = expiration_dates;
530 v.sort_unstable();
531 v.dedup();
532 v
533 };
534
535 let mut strikes: Vec<f64> = option_chains
536 .iter()
537 .flat_map(|chain| {
538 chain
539 .calls
540 .as_deref()
541 .unwrap_or_default()
542 .iter()
543 .map(|c| c.strike)
544 .chain(
545 chain
546 .puts
547 .as_deref()
548 .unwrap_or_default()
549 .iter()
550 .map(|p| p.strike),
551 )
552 })
553 .collect();
554 strikes.sort_by(|a, b| a.total_cmp(b));
555 strikes.dedup_by(|a, b| a.total_cmp(b).is_eq());
556
557 let result = crate::models::options::response::OptionChainResult {
558 underlying_symbol: Some(symbol),
559 expiration_dates: Some(expiration_dates),
560 strikes: Some(strikes),
561 has_mini_options: None,
562 quote: None,
563 options: option_chains,
564 };
565
566 crate::models::options::Options {
567 option_chain: crate::models::options::response::OptionChainContainer {
568 result: vec![result],
569 error: None,
570 },
571 provider_id: Some(provider_id),
572 }
573}
574
575#[allow(dead_code)] pub(crate) fn range_to_dates(range: crate::TimeRange) -> (String, String) {
577 use chrono::{Datelike, Utc};
578 let end = Utc::now();
579 if range == crate::TimeRange::YearToDate {
580 let year = end.year();
581 let start = chrono::NaiveDate::from_ymd_opt(year, 1, 1)
582 .and_then(|d| d.and_hms_opt(0, 0, 0))
583 .map(|dt| dt.and_utc())
584 .unwrap_or(end);
585 return (
586 start.format("%Y-%m-%d").to_string(),
587 end.format("%Y-%m-%d").to_string(),
588 );
589 }
590 let days = match range {
591 crate::TimeRange::OneDay => 1,
592 crate::TimeRange::FiveDays => 5,
593 crate::TimeRange::OneMonth => 30,
594 crate::TimeRange::ThreeMonths => 90,
595 crate::TimeRange::SixMonths => 180,
596 crate::TimeRange::OneYear => 365,
597 crate::TimeRange::TwoYears => 730,
598 crate::TimeRange::FiveYears => 1825,
599 crate::TimeRange::TenYears => 3650,
600 crate::TimeRange::Max => 36500,
601 crate::TimeRange::YearToDate => unreachable!("YTD handled by early return above"),
602 };
603 let start = end - chrono::Duration::days(days);
604 (
605 start.format("%Y-%m-%d").to_string(),
606 end.format("%Y-%m-%d").to_string(),
607 )
608}
609
610pub(crate) async fn build_providers(
611 ids: &[Provider],
612 config: &ClientConfig,
613 routes: Routes,
614) -> Result<ProviderSet> {
615 use yahoo::YahooProvider;
616 let mut providers: Vec<Arc<dyn ProviderAdapter>> = Vec::new();
617 let mut yahoo_client: Option<Arc<YahooClient>> = None;
618 for &id in ids {
619 match id {
620 Provider::Yahoo => {
621 let yp = YahooProvider::new(config).await?;
622 yahoo_client = Some(yp.client_arc());
623 providers.push(Arc::new(yp));
624 }
625 #[cfg(feature = "polygon")]
626 Provider::Polygon => {
627 let pp = polygon::PolygonProvider;
628 pp.initialize().await?;
629 providers.push(Arc::new(pp));
630 }
631 #[cfg(feature = "fmp")]
632 Provider::Fmp => {
633 let fp = fmp::FmpProvider;
634 fp.initialize().await?;
635 providers.push(Arc::new(fp));
636 }
637 #[cfg(feature = "alphavantage")]
638 Provider::AlphaVantage => {
639 let av = alphavantage::AlphaVantageProvider;
640 av.initialize().await?;
641 providers.push(Arc::new(av));
642 }
643 #[cfg(feature = "crypto")]
644 Provider::CoinGecko => providers.push(Arc::new(coingecko::CoinGeckoProvider)),
645 #[cfg(feature = "fred")]
646 Provider::Fred => {
647 let fp = fred::FredProvider;
648 fp.initialize().await?;
649 providers.push(Arc::new(fp));
650 }
651 Provider::Edgar => providers.push(Arc::new(edgar::EdgarProvider)),
652 }
653 }
654 let has_filings = providers
656 .iter()
657 .any(|p| p.capabilities().contains(Capability::FILINGS));
658 if !has_filings {
659 providers.push(Arc::new(edgar::EdgarProvider));
660 }
661 Ok(ProviderSet::new(providers, yahoo_client, routes))
662}