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}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub struct Capability(u32);
110
111impl Capability {
112 pub const QUOTE: Self = Self(1 << 0);
114 pub const CHART: Self = Self(1 << 1);
116 pub const FUNDAMENTALS: Self = Self(1 << 2);
118 pub const CORPORATE: Self = Self(1 << 3);
120 pub const OPTIONS: Self = Self(1 << 4);
122 pub const CRYPTO: Self = Self(1 << 6);
126 pub const ECONOMIC: Self = Self(1 << 7);
128 pub const FOREX: Self = Self(1 << 9);
132 pub const INDICES: Self = Self(1 << 10);
134 pub const FUTURES: Self = Self(1 << 11);
136 pub const COMMODITIES: Self = Self(1 << 12);
138 pub const FILINGS: Self = Self(1 << 14);
142
143 pub const fn contains(self, other: Self) -> bool {
145 (self.0 & other.0) == other.0
146 }
147
148 pub fn name(self) -> &'static str {
152 match self.0 {
153 x if x == Self::QUOTE.0 => "quote",
154 x if x == Self::CHART.0 => "chart",
155 x if x == Self::FUNDAMENTALS.0 => "fundamentals",
156 x if x == Self::CORPORATE.0 => "corporate",
157 x if x == Self::OPTIONS.0 => "options",
158 x if x == Self::CRYPTO.0 => "crypto",
159 x if x == Self::ECONOMIC.0 => "economic",
160 x if x == Self::FOREX.0 => "forex",
161 x if x == Self::INDICES.0 => "indices",
162 x if x == Self::FUTURES.0 => "futures",
163 x if x == Self::COMMODITIES.0 => "commodities",
164 x if x == Self::FILINGS.0 => "filings",
165 _ => "unknown",
166 }
167 }
168}
169
170impl std::ops::BitOr for Capability {
171 type Output = Self;
172 fn bitor(self, rhs: Self) -> Self {
173 Self(self.0 | rhs.0)
174 }
175}
176
177pub struct Routes {
182 pub(crate) map: HashMap<Capability, Vec<Provider>>,
183 pub(crate) fetch: Fetch,
184}
185
186impl Routes {
187 pub fn new(fetch: Fetch) -> Self {
188 Self {
189 map: HashMap::new(),
190 fetch,
191 }
192 }
193}
194
195#[async_trait::async_trait]
196pub(crate) trait ProviderAdapter: Send + Sync {
197 fn id(&self) -> &'static str;
198 fn capabilities(&self) -> Capability;
199
200 async fn initialize(&self) -> Result<()> {
202 Ok(())
203 }
204
205 fn not_supported(&self, operation: &'static str) -> FinanceError {
206 FinanceError::NotSupported {
207 provider: self.id(),
208 operation,
209 }
210 }
211
212 async fn fetch_quote(&self, _: &str) -> Result<QuoteSummaryResponse> {
215 Err(self.not_supported("quote"))
216 }
217 async fn fetch_chart(
218 &self,
219 _: &str,
220 _: crate::Interval,
221 _: crate::TimeRange,
222 ) -> Result<crate::models::chart::Chart> {
223 Err(self.not_supported("chart"))
224 }
225 async fn fetch_chart_range(
226 &self,
227 _: &str,
228 _: crate::Interval,
229 _: i64,
230 _: i64,
231 ) -> Result<crate::models::chart::Chart> {
232 Err(self.not_supported("chart_range"))
233 }
234 async fn fetch_financials(
235 &self,
236 _: &str,
237 _: crate::StatementType,
238 _: crate::Frequency,
239 ) -> Result<crate::models::fundamentals::FinancialStatement> {
240 Err(self.not_supported("financials"))
241 }
242 async fn fetch_news(&self, _: &str) -> Result<Vec<crate::models::corporate::news::News>> {
243 Err(self.not_supported("news"))
244 }
245 async fn fetch_similar_symbols(
246 &self,
247 _: &str,
248 _: u32,
249 ) -> Result<Vec<crate::models::corporate::recommendation::SimilarSymbol>> {
250 Err(self.not_supported("recommendations"))
251 }
252 async fn fetch_options(
253 &self,
254 _: &str,
255 _: Option<i64>,
256 ) -> Result<crate::models::options::Options> {
257 Err(self.not_supported("options"))
258 }
259 async fn fetch_events(&self, _: &str) -> Result<crate::models::chart::events::ChartEvents> {
260 Err(self.not_supported("events"))
261 }
262 async fn fetch_quotes_batch(&self, _: &[&str]) -> Result<Vec<(String, QuoteSummaryResponse)>> {
266 Err(self.not_supported("quotes_batch"))
267 }
268
269 async fn fetch_spark(
273 &self,
274 _: &[&str],
275 _: crate::Interval,
276 _: crate::TimeRange,
277 ) -> Result<Vec<(String, crate::models::chart::spark::Spark)>> {
278 Err(self.not_supported("spark"))
279 }
280
281 #[cfg(any(
282 feature = "crypto",
283 feature = "alphavantage",
284 feature = "fmp",
285 feature = "polygon"
286 ))]
287 async fn fetch_crypto_quote(
288 &self,
289 _: &str,
290 _: &str,
291 ) -> Result<crate::models::crypto::CryptoQuote> {
292 Err(self.not_supported("crypto_quote"))
293 }
294
295 #[cfg(any(feature = "fred", feature = "alphavantage", feature = "polygon"))]
296 async fn fetch_economic_series(
297 &self,
298 _: &str,
299 ) -> Result<crate::models::economic::EconomicSeries> {
300 Err(self.not_supported("economic_series"))
301 }
302
303 #[cfg(any(feature = "polygon", feature = "fmp", feature = "alphavantage"))]
304 async fn fetch_forex_quote(
305 &self,
306 _from: &str,
307 _to: &str,
308 ) -> Result<crate::models::forex::ForexQuote> {
309 Err(self.not_supported("forex_quote"))
310 }
311
312 #[cfg(any(feature = "polygon", feature = "fmp"))]
313 async fn fetch_indices_quote(&self, _: &str) -> Result<crate::models::indices::IndexQuote> {
314 Err(self.not_supported("indices_quote"))
315 }
316
317 #[cfg(feature = "polygon")]
318 async fn fetch_futures_quote(&self, _: &str) -> Result<crate::models::futures::FuturesQuote> {
319 Err(self.not_supported("futures_quote"))
320 }
321
322 #[cfg(any(feature = "fmp", feature = "alphavantage"))]
323 async fn fetch_commodities_quote(
324 &self,
325 _: &str,
326 ) -> Result<crate::models::commodities::CommodityQuote> {
327 Err(self.not_supported("commodities_quote"))
328 }
329
330 async fn fetch_filings(&self, _: &str) -> Result<crate::models::filings::ProviderFilings> {
331 Err(self.not_supported("filings"))
332 }
333}
334
335pub(crate) struct ProviderSet {
336 providers: Vec<Arc<dyn ProviderAdapter>>,
337 yahoo_client: Option<Arc<YahooClient>>,
338 routes: Routes,
339}
340
341impl ProviderSet {
342 pub fn new(
343 providers: Vec<Arc<dyn ProviderAdapter>>,
344 yahoo_client: Option<Arc<YahooClient>>,
345 routes: Routes,
346 ) -> Self {
347 Self {
348 providers,
349 yahoo_client,
350 routes,
351 }
352 }
353
354 fn candidates_for(&self, cap: Capability) -> Vec<&Arc<dyn ProviderAdapter>> {
358 if let Some(provider_ids) = self.routes.map.get(&cap) {
359 provider_ids
360 .iter()
361 .filter_map(|id| self.providers.iter().find(|p| p.id() == id.as_str()))
362 .collect()
363 } else if cap == Capability::FILINGS {
364 let mut v: Vec<&Arc<dyn ProviderAdapter>> = self
366 .providers
367 .iter()
368 .filter(|p| p.id() == "edgar")
369 .collect();
370 v.extend(self.providers.iter().filter(|p| p.id() == "yahoo"));
371 v
372 } else {
373 self.providers
375 .iter()
376 .filter(|p| p.id() == "yahoo")
377 .collect()
378 }
379 }
380
381 fn no_provider(cap: Capability) -> FinanceError {
382 FinanceError::NoProviderAvailable {
383 operation: cap.name(),
384 }
385 }
386
387 fn finish_err(cap: Capability, last: Option<FinanceError>) -> FinanceError {
388 last.unwrap_or_else(|| Self::no_provider(cap))
389 }
390
391 pub(crate) async fn fetch<T, F, Fut>(&self, cap: Capability, f: F) -> Result<T>
392 where
393 F: Fn(&Arc<dyn ProviderAdapter>) -> Fut,
394 Fut: std::future::Future<Output = Result<T>>,
395 {
396 let candidates = self.candidates_for(cap);
397 if candidates.is_empty() {
398 return Err(Self::no_provider(cap));
399 }
400 match self.routes.fetch {
401 Fetch::Sequential => {
402 let mut last = None;
403 for p in &candidates {
404 match f(p).await {
405 Ok(v) => return Ok(v),
406 Err(FinanceError::NotSupported { .. }) => continue,
407 Err(e) => last = Some(e),
408 }
409 }
410 Err(Self::finish_err(cap, last))
411 }
412 Fetch::Parallel => {
413 let mut futs = futures::stream::FuturesUnordered::new();
414 for p in &candidates {
415 futs.push(f(p));
416 }
417 let mut last = None;
418 while let Some(r) = futs.next().await {
419 match r {
420 Ok(v) => return Ok(v),
421 Err(FinanceError::NotSupported { .. }) => continue,
422 Err(e) => last = Some(e),
423 }
424 }
425 Err(Self::finish_err(cap, last))
426 }
427 }
428 }
429
430 pub(crate) fn first_yahoo(&self) -> Result<Arc<YahooClient>> {
431 self.yahoo_client
432 .as_ref()
433 .map(Arc::clone)
434 .ok_or_else(|| FinanceError::NoProviderAvailable { operation: "yahoo" })
435 }
436}
437
438#[allow(dead_code)] pub(crate) fn json_value_to_f64(value: serde_json::Value) -> Option<f64> {
440 value
441 .as_f64()
442 .or_else(|| value.as_i64().map(|v| v as f64))
443 .or_else(|| value.as_u64().map(|v| v as f64))
444 .or_else(|| value.as_str().and_then(|s| s.parse::<f64>().ok()))
445 .or_else(|| {
446 value
447 .get("raw")
448 .and_then(|raw| raw.as_f64().or_else(|| raw.as_i64().map(|v| v as f64)))
449 })
450}
451
452#[allow(dead_code)] pub(crate) fn build_financial_statement(
454 symbol: String,
455 statement_type: String,
456 frequency: String,
457 provider_id: Provider,
458 data: std::collections::HashMap<String, std::collections::HashMap<String, serde_json::Value>>,
459) -> crate::models::fundamentals::FinancialStatement {
460 let statement = data
461 .into_iter()
462 .filter_map(|(metric, values)| {
463 let values: std::collections::HashMap<String, f64> = values
464 .into_iter()
465 .filter_map(|(date, value)| json_value_to_f64(value).map(|v| (date, v)))
466 .collect();
467 if values.is_empty() {
468 None
469 } else {
470 Some((metric, values))
471 }
472 })
473 .collect();
474 crate::models::fundamentals::FinancialStatement {
475 symbol,
476 statement_type,
477 frequency,
478 statement,
479 provider_id: Some(provider_id),
480 }
481}
482
483pub(crate) fn build_options(
484 symbol: String,
485 provider_id: Provider,
486 expiration_dates: Vec<i64>,
487 calls: Vec<crate::models::options::OptionContract>,
488 puts: Vec<crate::models::options::OptionContract>,
489) -> crate::models::options::Options {
490 use std::collections::BTreeMap;
491
492 let mut chains_by_expiration: BTreeMap<
493 i64,
494 (
495 Vec<crate::models::options::OptionContract>,
496 Vec<crate::models::options::OptionContract>,
497 ),
498 > = BTreeMap::new();
499
500 for contract in calls {
501 let exp = contract.expiration.unwrap_or(0);
502 chains_by_expiration
503 .entry(exp)
504 .or_default()
505 .0
506 .push(contract);
507 }
508 for contract in puts {
509 let exp = contract.expiration.unwrap_or(0);
510 chains_by_expiration
511 .entry(exp)
512 .or_default()
513 .1
514 .push(contract);
515 }
516
517 let option_chains: Vec<crate::models::options::response::OptionChainData> =
518 chains_by_expiration
519 .into_iter()
520 .map(
521 |(expiration, (c, p))| crate::models::options::response::OptionChainData {
522 expiration_date: expiration,
523 has_mini_options: None,
524 calls: Some(c),
525 puts: Some(p),
526 },
527 )
528 .collect();
529
530 let expiration_dates = if expiration_dates.is_empty() {
531 option_chains
532 .iter()
533 .map(|chain| chain.expiration_date)
534 .collect()
535 } else {
536 let mut v: Vec<i64> = expiration_dates;
537 v.sort_unstable();
538 v.dedup();
539 v
540 };
541
542 let mut strikes: Vec<f64> = option_chains
543 .iter()
544 .flat_map(|chain| {
545 chain
546 .calls
547 .as_deref()
548 .unwrap_or_default()
549 .iter()
550 .map(|c| c.strike)
551 .chain(
552 chain
553 .puts
554 .as_deref()
555 .unwrap_or_default()
556 .iter()
557 .map(|p| p.strike),
558 )
559 })
560 .collect();
561 strikes.sort_by(|a, b| a.total_cmp(b));
562 strikes.dedup_by(|a, b| a.total_cmp(b).is_eq());
563
564 let result = crate::models::options::response::OptionChainResult {
565 underlying_symbol: Some(symbol),
566 expiration_dates: Some(expiration_dates),
567 strikes: Some(strikes),
568 has_mini_options: None,
569 quote: None,
570 options: option_chains,
571 };
572
573 crate::models::options::Options {
574 option_chain: crate::models::options::response::OptionChainContainer {
575 result: vec![result],
576 error: None,
577 },
578 provider_id: Some(provider_id),
579 }
580}
581
582#[allow(dead_code)] pub(crate) fn range_to_dates(range: crate::TimeRange) -> (String, String) {
584 use chrono::{Datelike, Utc};
585 let end = Utc::now();
586 if range == crate::TimeRange::YearToDate {
587 let year = end.year();
588 let start = chrono::NaiveDate::from_ymd_opt(year, 1, 1)
589 .and_then(|d| d.and_hms_opt(0, 0, 0))
590 .map(|dt| dt.and_utc())
591 .unwrap_or(end);
592 return (
593 start.format("%Y-%m-%d").to_string(),
594 end.format("%Y-%m-%d").to_string(),
595 );
596 }
597 let days = match range {
598 crate::TimeRange::OneDay => 1,
599 crate::TimeRange::FiveDays => 5,
600 crate::TimeRange::OneMonth => 30,
601 crate::TimeRange::ThreeMonths => 90,
602 crate::TimeRange::SixMonths => 180,
603 crate::TimeRange::OneYear => 365,
604 crate::TimeRange::TwoYears => 730,
605 crate::TimeRange::FiveYears => 1825,
606 crate::TimeRange::TenYears => 3650,
607 crate::TimeRange::Max => 36500,
608 crate::TimeRange::YearToDate => unreachable!("YTD handled by early return above"),
609 };
610 let start = end - chrono::Duration::days(days);
611 (
612 start.format("%Y-%m-%d").to_string(),
613 end.format("%Y-%m-%d").to_string(),
614 )
615}
616
617pub(crate) async fn build_providers(
618 ids: &[Provider],
619 config: &ClientConfig,
620 routes: Routes,
621) -> Result<ProviderSet> {
622 use yahoo::YahooProvider;
623 let mut providers: Vec<Arc<dyn ProviderAdapter>> = Vec::new();
624 let mut yahoo_client: Option<Arc<YahooClient>> = None;
625 for &id in ids {
626 match id {
627 Provider::Yahoo => {
628 let yp = YahooProvider::new(config).await?;
629 yahoo_client = Some(yp.client_arc());
630 providers.push(Arc::new(yp));
631 }
632 #[cfg(feature = "polygon")]
633 Provider::Polygon => {
634 let pp = polygon::PolygonProvider;
635 pp.initialize().await?;
636 providers.push(Arc::new(pp));
637 }
638 #[cfg(feature = "fmp")]
639 Provider::Fmp => {
640 let fp = fmp::FmpProvider;
641 fp.initialize().await?;
642 providers.push(Arc::new(fp));
643 }
644 #[cfg(feature = "alphavantage")]
645 Provider::AlphaVantage => {
646 let av = alphavantage::AlphaVantageProvider;
647 av.initialize().await?;
648 providers.push(Arc::new(av));
649 }
650 #[cfg(feature = "crypto")]
651 Provider::CoinGecko => providers.push(Arc::new(coingecko::CoinGeckoProvider)),
652 #[cfg(feature = "fred")]
653 Provider::Fred => {
654 let fp = fred::FredProvider;
655 fp.initialize().await?;
656 providers.push(Arc::new(fp));
657 }
658 Provider::Edgar => providers.push(Arc::new(edgar::EdgarProvider)),
659 }
660 }
661 let has_filings = providers
663 .iter()
664 .any(|p| p.capabilities().contains(Capability::FILINGS));
665 if !has_filings {
666 providers.push(Arc::new(edgar::EdgarProvider));
667 }
668 Ok(ProviderSet::new(providers, yahoo_client, routes))
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674
675 struct NoSparkProvider;
678
679 #[async_trait::async_trait]
680 impl ProviderAdapter for NoSparkProvider {
681 fn id(&self) -> &'static str {
682 "yahoo"
683 }
684 fn capabilities(&self) -> Capability {
685 Capability::CHART
686 }
687 }
688
689 #[tokio::test]
690 async fn fetch_spark_defaults_to_not_supported() {
691 let err = NoSparkProvider
692 .fetch_spark(
693 &["AAPL"],
694 crate::Interval::OneDay,
695 crate::TimeRange::FiveDays,
696 )
697 .await
698 .unwrap_err();
699 assert!(matches!(
700 err,
701 FinanceError::NotSupported {
702 operation: "spark",
703 ..
704 }
705 ));
706 }
707
708 #[tokio::test]
709 async fn spark_routes_through_provider_set() {
710 let set = ProviderSet::new(
714 vec![Arc::new(NoSparkProvider)],
715 None,
716 Routes::new(Fetch::Sequential),
717 );
718 let result = set
719 .fetch(Capability::CHART, |p| {
720 let p = p.clone();
721 async move {
722 p.fetch_spark(
723 &["AAPL"],
724 crate::Interval::OneDay,
725 crate::TimeRange::FiveDays,
726 )
727 .await
728 }
729 })
730 .await;
731 assert!(result.is_err());
732 }
733}