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 pub(crate) fn all() -> Vec<Self> {
99 let mut v = vec![Self::Yahoo];
100 #[cfg(feature = "polygon")]
101 v.push(Self::Polygon);
102 #[cfg(feature = "fmp")]
103 v.push(Self::Fmp);
104 #[cfg(feature = "alphavantage")]
105 v.push(Self::AlphaVantage);
106 #[cfg(feature = "crypto")]
107 v.push(Self::CoinGecko);
108 #[cfg(feature = "fred")]
109 v.push(Self::Fred);
110 v.push(Self::Edgar);
111 v
112 }
113
114 pub(crate) fn capabilities(self) -> Capability {
119 match self {
120 Self::Yahoo => {
121 Capability::QUOTE
122 | Capability::CHART
123 | Capability::FUNDAMENTALS
124 | Capability::CORPORATE
125 | Capability::OPTIONS
126 }
127 #[cfg(feature = "polygon")]
128 Self::Polygon => {
129 Capability::QUOTE
130 | Capability::CHART
131 | Capability::FUNDAMENTALS
132 | Capability::CORPORATE
133 | Capability::OPTIONS
134 | Capability::CRYPTO
135 | Capability::FOREX
136 | Capability::FUTURES
137 | Capability::INDICES
138 | Capability::FILINGS
139 | Capability::ECONOMIC
140 }
141 #[cfg(feature = "fmp")]
142 Self::Fmp => {
143 Capability::QUOTE
144 | Capability::CHART
145 | Capability::FUNDAMENTALS
146 | Capability::CORPORATE
147 | Capability::INDICES
148 | Capability::COMMODITIES
149 | Capability::FOREX
150 | Capability::CRYPTO
151 }
152 #[cfg(feature = "alphavantage")]
153 Self::AlphaVantage => {
154 Capability::QUOTE
155 | Capability::CHART
156 | Capability::FUNDAMENTALS
157 | Capability::CORPORATE
158 | Capability::OPTIONS
159 | Capability::CRYPTO
160 | Capability::FOREX
161 | Capability::COMMODITIES
162 | Capability::ECONOMIC
163 }
164 #[cfg(feature = "crypto")]
165 Self::CoinGecko => Capability::CRYPTO,
166 #[cfg(feature = "fred")]
167 Self::Fred => Capability::ECONOMIC,
168 Self::Edgar => Capability::FILINGS,
169 }
170 }
171}
172
173impl std::fmt::Display for Provider {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 f.write_str(self.as_str())
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum Fetch {
182 Sequential,
184 Parallel,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
193pub struct Capability(u32);
194
195impl Capability {
196 pub const QUOTE: Self = Self(1 << 0);
198 pub const CHART: Self = Self(1 << 1);
200 pub const FUNDAMENTALS: Self = Self(1 << 2);
202 pub const CORPORATE: Self = Self(1 << 3);
204 pub const OPTIONS: Self = Self(1 << 4);
206 pub const CRYPTO: Self = Self(1 << 6);
210 pub const ECONOMIC: Self = Self(1 << 7);
212 pub const FOREX: Self = Self(1 << 9);
216 pub const INDICES: Self = Self(1 << 10);
218 pub const FUTURES: Self = Self(1 << 11);
220 pub const COMMODITIES: Self = Self(1 << 12);
222 pub const FILINGS: Self = Self(1 << 14);
226
227 pub const fn contains(self, other: Self) -> bool {
229 (self.0 & other.0) == other.0
230 }
231
232 pub fn name(self) -> &'static str {
236 match self.0 {
237 x if x == Self::QUOTE.0 => "quote",
238 x if x == Self::CHART.0 => "chart",
239 x if x == Self::FUNDAMENTALS.0 => "fundamentals",
240 x if x == Self::CORPORATE.0 => "corporate",
241 x if x == Self::OPTIONS.0 => "options",
242 x if x == Self::CRYPTO.0 => "crypto",
243 x if x == Self::ECONOMIC.0 => "economic",
244 x if x == Self::FOREX.0 => "forex",
245 x if x == Self::INDICES.0 => "indices",
246 x if x == Self::FUTURES.0 => "futures",
247 x if x == Self::COMMODITIES.0 => "commodities",
248 x if x == Self::FILINGS.0 => "filings",
249 _ => "unknown",
250 }
251 }
252}
253
254impl std::fmt::Display for Capability {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 f.write_str(self.name())
257 }
258}
259
260impl Capability {
261 pub(crate) fn candidate_providers(self) -> Vec<Provider> {
268 Provider::all()
269 .into_iter()
270 .filter(|p| p.capabilities().contains(self))
271 .collect()
272 }
273}
274
275impl std::ops::BitOr for Capability {
276 type Output = Self;
277 fn bitor(self, rhs: Self) -> Self {
278 Self(self.0 | rhs.0)
279 }
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
289#[non_exhaustive]
290pub enum Operation {
291 Quote,
293 Chart,
295 ChartRange,
297 Financials,
299 News,
301 Recommendations,
303 Options,
305 Events,
307 QuotesBatch,
309 Spark,
311 CryptoQuote,
313 EconomicSeries,
315 ForexQuote,
317 IndicesQuote,
319 FuturesQuote,
321 CommoditiesQuote,
323 Filings,
325}
326
327impl Operation {
328 pub fn as_str(self) -> &'static str {
330 match self {
331 Self::Quote => "quote",
332 Self::Chart => "chart",
333 Self::ChartRange => "chart_range",
334 Self::Financials => "financials",
335 Self::News => "news",
336 Self::Recommendations => "recommendations",
337 Self::Options => "options",
338 Self::Events => "events",
339 Self::QuotesBatch => "quotes_batch",
340 Self::Spark => "spark",
341 Self::CryptoQuote => "crypto_quote",
342 Self::EconomicSeries => "economic_series",
343 Self::ForexQuote => "forex_quote",
344 Self::IndicesQuote => "indices_quote",
345 Self::FuturesQuote => "futures_quote",
346 Self::CommoditiesQuote => "commodities_quote",
347 Self::Filings => "filings",
348 }
349 }
350
351 pub fn capability(self) -> Capability {
353 match self {
354 Self::Quote | Self::QuotesBatch => Capability::QUOTE,
355 Self::Chart | Self::ChartRange | Self::Spark => Capability::CHART,
356 Self::Financials => Capability::FUNDAMENTALS,
357 Self::News | Self::Recommendations | Self::Events => Capability::CORPORATE,
358 Self::Options => Capability::OPTIONS,
359 Self::CryptoQuote => Capability::CRYPTO,
360 Self::EconomicSeries => Capability::ECONOMIC,
361 Self::ForexQuote => Capability::FOREX,
362 Self::IndicesQuote => Capability::INDICES,
363 Self::FuturesQuote => Capability::FUTURES,
364 Self::CommoditiesQuote => Capability::COMMODITIES,
365 Self::Filings => Capability::FILINGS,
366 }
367 }
368}
369
370impl std::fmt::Display for Operation {
371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372 f.write_str(self.as_str())
373 }
374}
375
376#[derive(Debug)]
381pub struct Routes {
382 pub(crate) map: HashMap<Capability, Vec<Provider>>,
383 pub(crate) fetch: Fetch,
384}
385
386impl Routes {
387 pub fn new(fetch: Fetch) -> Self {
388 Self {
389 map: HashMap::new(),
390 fetch,
391 }
392 }
393}
394
395#[async_trait::async_trait]
396pub(crate) trait ProviderAdapter: Send + Sync {
397 fn id(&self) -> Provider;
398 fn capabilities(&self) -> Capability;
399
400 async fn initialize(&self) -> Result<()> {
402 Ok(())
403 }
404
405 fn not_supported(&self, operation: Operation) -> FinanceError {
406 FinanceError::NotSupported {
407 provider: self.id(),
408 operation,
409 candidates: operation.capability().candidate_providers(),
410 }
411 }
412
413 async fn fetch_quote(&self, _: &str) -> Result<QuoteSummaryResponse> {
416 Err(self.not_supported(Operation::Quote))
417 }
418 async fn fetch_chart(
419 &self,
420 _: &str,
421 _: crate::Interval,
422 _: crate::TimeRange,
423 ) -> Result<crate::models::chart::Chart> {
424 Err(self.not_supported(Operation::Chart))
425 }
426 async fn fetch_chart_range(
427 &self,
428 _: &str,
429 _: crate::Interval,
430 _: i64,
431 _: i64,
432 ) -> Result<crate::models::chart::Chart> {
433 Err(self.not_supported(Operation::ChartRange))
434 }
435 async fn fetch_financials(
436 &self,
437 _: &str,
438 _: crate::StatementType,
439 _: crate::Frequency,
440 ) -> Result<crate::models::fundamentals::FinancialStatement> {
441 Err(self.not_supported(Operation::Financials))
442 }
443 async fn fetch_news(&self, _: &str) -> Result<Vec<crate::models::corporate::news::News>> {
444 Err(self.not_supported(Operation::News))
445 }
446 async fn fetch_similar_symbols(
447 &self,
448 _: &str,
449 _: u32,
450 ) -> Result<Vec<crate::models::corporate::recommendation::SimilarSymbol>> {
451 Err(self.not_supported(Operation::Recommendations))
452 }
453 async fn fetch_options(
454 &self,
455 _: &str,
456 _: Option<i64>,
457 ) -> Result<crate::models::options::Options> {
458 Err(self.not_supported(Operation::Options))
459 }
460 async fn fetch_events(&self, _: &str) -> Result<crate::models::chart::events::ChartEvents> {
461 Err(self.not_supported(Operation::Events))
462 }
463 async fn fetch_quotes_batch(&self, _: &[&str]) -> Result<Vec<(String, QuoteSummaryResponse)>> {
467 Err(self.not_supported(Operation::QuotesBatch))
468 }
469
470 async fn fetch_spark(
474 &self,
475 _: &[&str],
476 _: crate::Interval,
477 _: crate::TimeRange,
478 ) -> Result<Vec<(String, crate::models::chart::spark::Spark)>> {
479 Err(self.not_supported(Operation::Spark))
480 }
481
482 #[cfg(any(
483 feature = "crypto",
484 feature = "alphavantage",
485 feature = "fmp",
486 feature = "polygon"
487 ))]
488 async fn fetch_crypto_quote(
489 &self,
490 _: &str,
491 _: &str,
492 ) -> Result<crate::models::crypto::CryptoQuote> {
493 Err(self.not_supported(Operation::CryptoQuote))
494 }
495
496 #[cfg(any(feature = "fred", feature = "alphavantage", feature = "polygon"))]
497 async fn fetch_economic_series(
498 &self,
499 _: &str,
500 ) -> Result<crate::models::economic::EconomicSeries> {
501 Err(self.not_supported(Operation::EconomicSeries))
502 }
503
504 #[cfg(any(feature = "polygon", feature = "fmp", feature = "alphavantage"))]
505 async fn fetch_forex_quote(
506 &self,
507 _from: &str,
508 _to: &str,
509 ) -> Result<crate::models::forex::ForexQuote> {
510 Err(self.not_supported(Operation::ForexQuote))
511 }
512
513 #[cfg(any(feature = "polygon", feature = "fmp"))]
514 async fn fetch_indices_quote(&self, _: &str) -> Result<crate::models::indices::IndexQuote> {
515 Err(self.not_supported(Operation::IndicesQuote))
516 }
517
518 #[cfg(feature = "polygon")]
519 async fn fetch_futures_quote(&self, _: &str) -> Result<crate::models::futures::FuturesQuote> {
520 Err(self.not_supported(Operation::FuturesQuote))
521 }
522
523 #[cfg(any(feature = "fmp", feature = "alphavantage"))]
524 async fn fetch_commodities_quote(
525 &self,
526 _: &str,
527 ) -> Result<crate::models::commodities::CommodityQuote> {
528 Err(self.not_supported(Operation::CommoditiesQuote))
529 }
530
531 async fn fetch_filings(&self, _: &str) -> Result<crate::models::filings::ProviderFilings> {
532 Err(self.not_supported(Operation::Filings))
533 }
534}
535
536pub(crate) struct ProviderSet {
537 providers: Vec<Arc<dyn ProviderAdapter>>,
538 yahoo_client: Option<Arc<YahooClient>>,
539 routes: Routes,
540}
541
542impl std::fmt::Debug for ProviderSet {
543 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
544 f.debug_struct("ProviderSet")
545 .field(
546 "providers",
547 &self.providers.iter().map(|p| p.id()).collect::<Vec<_>>(),
548 )
549 .field("has_yahoo_client", &self.yahoo_client.is_some())
550 .field("routes", &self.routes)
551 .finish()
552 }
553}
554
555impl ProviderSet {
556 pub fn new(
557 providers: Vec<Arc<dyn ProviderAdapter>>,
558 yahoo_client: Option<Arc<YahooClient>>,
559 routes: Routes,
560 ) -> Self {
561 Self {
562 providers,
563 yahoo_client,
564 routes,
565 }
566 }
567
568 fn candidates_for(&self, cap: Capability) -> Vec<&Arc<dyn ProviderAdapter>> {
572 if let Some(provider_ids) = self.routes.map.get(&cap) {
573 provider_ids
574 .iter()
575 .filter_map(|id| self.providers.iter().find(|p| p.id() == *id))
576 .collect()
577 } else if cap == Capability::FILINGS {
578 let mut v: Vec<&Arc<dyn ProviderAdapter>> = self
580 .providers
581 .iter()
582 .filter(|p| p.id() == Provider::Edgar)
583 .collect();
584 v.extend(self.providers.iter().filter(|p| p.id() == Provider::Yahoo));
585 v
586 } else {
587 self.providers
589 .iter()
590 .filter(|p| p.id() == Provider::Yahoo)
591 .collect()
592 }
593 }
594
595 fn no_provider(cap: Capability) -> FinanceError {
596 FinanceError::NoProviderAvailable {
597 operation: cap,
598 candidates: cap.candidate_providers(),
599 }
600 }
601
602 fn finish_err(cap: Capability, last: Option<FinanceError>) -> FinanceError {
603 last.unwrap_or_else(|| Self::no_provider(cap))
604 }
605
606 pub(crate) async fn fetch<T, F, Fut>(&self, cap: Capability, f: F) -> Result<T>
607 where
608 F: Fn(&Arc<dyn ProviderAdapter>) -> Fut,
609 Fut: std::future::Future<Output = Result<T>>,
610 {
611 let candidates = self.candidates_for(cap);
612 if candidates.is_empty() {
613 return Err(Self::no_provider(cap));
614 }
615 match self.routes.fetch {
616 Fetch::Sequential => {
617 let mut last = None;
618 for p in &candidates {
619 match f(p).await {
620 Ok(v) => return Ok(v),
621 Err(FinanceError::NotSupported { .. }) => continue,
622 Err(e) => last = Some(e),
623 }
624 }
625 Err(Self::finish_err(cap, last))
626 }
627 Fetch::Parallel => {
628 let mut futs = futures::stream::FuturesUnordered::new();
629 for p in &candidates {
630 futs.push(f(p));
631 }
632 let mut last = None;
633 while let Some(r) = futs.next().await {
634 match r {
635 Ok(v) => return Ok(v),
636 Err(FinanceError::NotSupported { .. }) => continue,
637 Err(e) => last = Some(e),
638 }
639 }
640 Err(Self::finish_err(cap, last))
641 }
642 }
643 }
644
645 pub(crate) fn first_yahoo(&self) -> Result<Arc<YahooClient>> {
646 self.yahoo_client.as_ref().map(Arc::clone).ok_or_else(|| {
647 FinanceError::NoProviderAvailable {
648 operation: Capability::QUOTE,
649 candidates: vec![Provider::Yahoo],
650 }
651 })
652 }
653}
654
655#[allow(dead_code)] pub(crate) fn json_value_to_f64(value: serde_json::Value) -> Option<f64> {
657 value
658 .as_f64()
659 .or_else(|| value.as_i64().map(|v| v as f64))
660 .or_else(|| value.as_u64().map(|v| v as f64))
661 .or_else(|| value.as_str().and_then(|s| s.parse::<f64>().ok()))
662 .or_else(|| {
663 value
664 .get("raw")
665 .and_then(|raw| raw.as_f64().or_else(|| raw.as_i64().map(|v| v as f64)))
666 })
667}
668
669#[allow(dead_code)] pub(crate) fn build_financial_statement(
671 symbol: String,
672 statement_type: String,
673 frequency: String,
674 provider_id: Provider,
675 data: std::collections::HashMap<String, std::collections::HashMap<String, serde_json::Value>>,
676) -> crate::models::fundamentals::FinancialStatement {
677 let statement = data
678 .into_iter()
679 .filter_map(|(metric, values)| {
680 let values: std::collections::HashMap<String, f64> = values
681 .into_iter()
682 .filter_map(|(date, value)| json_value_to_f64(value).map(|v| (date, v)))
683 .collect();
684 if values.is_empty() {
685 None
686 } else {
687 Some((metric, values))
688 }
689 })
690 .collect();
691 crate::models::fundamentals::FinancialStatement {
692 symbol,
693 statement_type,
694 frequency,
695 statement,
696 provider_id: Some(provider_id),
697 }
698}
699
700pub(crate) fn build_options(
701 symbol: String,
702 provider_id: Provider,
703 expiration_dates: Vec<i64>,
704 calls: Vec<crate::models::options::OptionContract>,
705 puts: Vec<crate::models::options::OptionContract>,
706) -> crate::models::options::Options {
707 use std::collections::BTreeMap;
708
709 let mut chains_by_expiration: BTreeMap<
710 i64,
711 (
712 Vec<crate::models::options::OptionContract>,
713 Vec<crate::models::options::OptionContract>,
714 ),
715 > = BTreeMap::new();
716
717 for contract in calls {
718 let exp = contract.expiration.unwrap_or(0);
719 chains_by_expiration
720 .entry(exp)
721 .or_default()
722 .0
723 .push(contract);
724 }
725 for contract in puts {
726 let exp = contract.expiration.unwrap_or(0);
727 chains_by_expiration
728 .entry(exp)
729 .or_default()
730 .1
731 .push(contract);
732 }
733
734 let option_chains: Vec<crate::models::options::response::OptionChainData> =
735 chains_by_expiration
736 .into_iter()
737 .map(
738 |(expiration, (c, p))| crate::models::options::response::OptionChainData {
739 expiration_date: expiration,
740 has_mini_options: None,
741 calls: Some(c),
742 puts: Some(p),
743 },
744 )
745 .collect();
746
747 let expiration_dates = if expiration_dates.is_empty() {
748 option_chains
749 .iter()
750 .map(|chain| chain.expiration_date)
751 .collect()
752 } else {
753 let mut v: Vec<i64> = expiration_dates;
754 v.sort_unstable();
755 v.dedup();
756 v
757 };
758
759 let mut strikes: Vec<f64> = option_chains
760 .iter()
761 .flat_map(|chain| {
762 chain
763 .calls
764 .as_deref()
765 .unwrap_or_default()
766 .iter()
767 .map(|c| c.strike)
768 .chain(
769 chain
770 .puts
771 .as_deref()
772 .unwrap_or_default()
773 .iter()
774 .map(|p| p.strike),
775 )
776 })
777 .collect();
778 strikes.sort_by(|a, b| a.total_cmp(b));
779 strikes.dedup_by(|a, b| a.total_cmp(b).is_eq());
780
781 let result = crate::models::options::response::OptionChainResult {
782 underlying_symbol: Some(symbol),
783 expiration_dates: Some(expiration_dates),
784 strikes: Some(strikes),
785 has_mini_options: None,
786 quote: None,
787 options: option_chains,
788 };
789
790 crate::models::options::Options {
791 option_chain: crate::models::options::response::OptionChainContainer {
792 result: vec![result],
793 error: None,
794 },
795 provider_id: Some(provider_id),
796 }
797}
798
799#[allow(dead_code)] pub(crate) fn range_to_dates(range: crate::TimeRange) -> (String, String) {
801 use chrono::{Datelike, Utc};
802 let end = Utc::now();
803 if range == crate::TimeRange::YearToDate {
804 let year = end.year();
805 let start = chrono::NaiveDate::from_ymd_opt(year, 1, 1)
806 .and_then(|d| d.and_hms_opt(0, 0, 0))
807 .map(|dt| dt.and_utc())
808 .unwrap_or(end);
809 return (
810 start.format("%Y-%m-%d").to_string(),
811 end.format("%Y-%m-%d").to_string(),
812 );
813 }
814 let days = match range {
815 crate::TimeRange::OneDay => 1,
816 crate::TimeRange::FiveDays => 5,
817 crate::TimeRange::OneMonth => 30,
818 crate::TimeRange::ThreeMonths => 90,
819 crate::TimeRange::SixMonths => 180,
820 crate::TimeRange::OneYear => 365,
821 crate::TimeRange::TwoYears => 730,
822 crate::TimeRange::FiveYears => 1825,
823 crate::TimeRange::TenYears => 3650,
824 crate::TimeRange::Max => 36500,
825 crate::TimeRange::YearToDate => unreachable!("YTD handled by early return above"),
826 };
827 let start = end - chrono::Duration::days(days);
828 (
829 start.format("%Y-%m-%d").to_string(),
830 end.format("%Y-%m-%d").to_string(),
831 )
832}
833
834pub(crate) async fn build_providers(
835 ids: &[Provider],
836 config: &ClientConfig,
837 routes: Routes,
838) -> Result<ProviderSet> {
839 use yahoo::YahooProvider;
840 let mut providers: Vec<Arc<dyn ProviderAdapter>> = Vec::new();
841 let mut yahoo_client: Option<Arc<YahooClient>> = None;
842 for &id in ids {
843 match id {
844 Provider::Yahoo => {
845 let yp = YahooProvider::new(config).await?;
846 yahoo_client = Some(yp.client_arc());
847 providers.push(Arc::new(yp));
848 }
849 #[cfg(feature = "polygon")]
850 Provider::Polygon => {
851 let pp = polygon::PolygonProvider;
852 pp.initialize().await?;
853 providers.push(Arc::new(pp));
854 }
855 #[cfg(feature = "fmp")]
856 Provider::Fmp => {
857 let fp = fmp::FmpProvider;
858 fp.initialize().await?;
859 providers.push(Arc::new(fp));
860 }
861 #[cfg(feature = "alphavantage")]
862 Provider::AlphaVantage => {
863 let av = alphavantage::AlphaVantageProvider;
864 av.initialize().await?;
865 providers.push(Arc::new(av));
866 }
867 #[cfg(feature = "crypto")]
868 Provider::CoinGecko => providers.push(Arc::new(coingecko::CoinGeckoProvider)),
869 #[cfg(feature = "fred")]
870 Provider::Fred => {
871 let fp = fred::FredProvider;
872 fp.initialize().await?;
873 providers.push(Arc::new(fp));
874 }
875 Provider::Edgar => providers.push(Arc::new(edgar::EdgarProvider)),
876 }
877 }
878 let has_filings = providers
880 .iter()
881 .any(|p| p.capabilities().contains(Capability::FILINGS));
882 if !has_filings {
883 providers.push(Arc::new(edgar::EdgarProvider));
884 }
885 Ok(ProviderSet::new(providers, yahoo_client, routes))
886}
887
888#[cfg(test)]
889mod tests {
890 use super::*;
891
892 struct NoSparkProvider;
895
896 #[async_trait::async_trait]
897 impl ProviderAdapter for NoSparkProvider {
898 fn id(&self) -> Provider {
899 Provider::Yahoo
900 }
901 fn capabilities(&self) -> Capability {
902 Capability::CHART
903 }
904 }
905
906 #[tokio::test]
907 async fn fetch_spark_defaults_to_not_supported() {
908 let err = NoSparkProvider
909 .fetch_spark(
910 &["AAPL"],
911 crate::Interval::OneDay,
912 crate::TimeRange::FiveDays,
913 )
914 .await
915 .unwrap_err();
916 assert!(matches!(
917 err,
918 FinanceError::NotSupported {
919 operation: Operation::Spark,
920 ..
921 }
922 ));
923 }
924
925 #[tokio::test]
926 async fn spark_routes_through_provider_set() {
927 let set = ProviderSet::new(
931 vec![Arc::new(NoSparkProvider)],
932 None,
933 Routes::new(Fetch::Sequential),
934 );
935 let result = set
936 .fetch(Capability::CHART, |p| {
937 let p = p.clone();
938 async move {
939 p.fetch_spark(
940 &["AAPL"],
941 crate::Interval::OneDay,
942 crate::TimeRange::FiveDays,
943 )
944 .await
945 }
946 })
947 .await;
948 assert!(result.is_err());
949 }
950
951 #[cfg(feature = "polygon")]
957 #[test]
958 fn polygon_capabilities_match_static_table() {
959 assert_eq!(
960 ProviderAdapter::capabilities(&polygon::PolygonProvider),
961 Provider::Polygon.capabilities()
962 );
963 }
964
965 #[cfg(feature = "fmp")]
966 #[test]
967 fn fmp_capabilities_match_static_table() {
968 assert_eq!(
969 ProviderAdapter::capabilities(&fmp::FmpProvider),
970 Provider::Fmp.capabilities()
971 );
972 }
973
974 #[cfg(feature = "alphavantage")]
975 #[test]
976 fn alphavantage_capabilities_match_static_table() {
977 assert_eq!(
978 ProviderAdapter::capabilities(&alphavantage::AlphaVantageProvider),
979 Provider::AlphaVantage.capabilities()
980 );
981 }
982
983 #[cfg(feature = "crypto")]
984 #[test]
985 fn coingecko_capabilities_match_static_table() {
986 assert_eq!(
987 ProviderAdapter::capabilities(&coingecko::CoinGeckoProvider),
988 Provider::CoinGecko.capabilities()
989 );
990 }
991
992 #[cfg(feature = "fred")]
993 #[test]
994 fn fred_capabilities_match_static_table() {
995 assert_eq!(
996 ProviderAdapter::capabilities(&fred::FredProvider),
997 Provider::Fred.capabilities()
998 );
999 }
1000
1001 #[test]
1002 fn edgar_capabilities_match_static_table() {
1003 assert_eq!(
1004 ProviderAdapter::capabilities(&edgar::EdgarProvider),
1005 Provider::Edgar.capabilities()
1006 );
1007 }
1008}