1use super::macros::{batch_fetch_cached, define_batch_response};
6use crate::adapters::yahoo::client::ClientConfig;
7#[cfg(feature = "backtesting")]
8use crate::backtesting;
9use crate::constants::{Frequency, Interval, Region, StatementType, TimeRange};
10use crate::error::{FinanceError, Result};
11use crate::format::Both;
12#[cfg(any(feature = "backtesting", feature = "indicators"))]
13use crate::indicators;
14use crate::models::chart::events::ChartEvents;
15use crate::models::chart::spark::Spark;
16use crate::models::chart::{CapitalGain, Chart, Dividend, Split};
17use crate::models::corporate::news::News;
18use crate::models::corporate::recommendation::Recommendation;
19use crate::models::format::Format;
20use crate::models::fundamentals::FinancialStatement;
21use crate::models::options::Options;
22use crate::models::quote::{Quote, QuoteSummaryResponse};
23
24use crate::providers::types::recommendation_from_similar;
25use crate::providers::yahoo::YahooProvider;
26use crate::providers::{
27 Capability, Fetch, Provider, ProviderAdapter, ProviderSet, Routes, build_providers,
28};
29use crate::ticker::ClientHandle;
30use crate::utils::{CacheEntry, EVICTION_THRESHOLD, filter_by_range};
31use futures::stream::{self, StreamExt};
32use std::collections::HashMap;
33use std::sync::Arc;
34use std::time::Duration;
35use tokio::sync::RwLock;
36
37type MapCache<K, V> = Arc<RwLock<HashMap<K, CacheEntry<V>>>>;
39type ChartCacheKey = (Arc<str>, Interval, TimeRange);
40type QuoteCache = MapCache<Arc<str>, Quote>;
41type ChartCache = MapCache<ChartCacheKey, Chart>;
42type EventsCache = MapCache<Arc<str>, ChartEvents>;
43type FinancialsCache = MapCache<(Arc<str>, StatementType, Frequency), FinancialStatement>;
44type NewsCache = MapCache<Arc<str>, Vec<News>>;
45type RecommendationsCache = MapCache<(Arc<str>, u32), Recommendation>;
46type OptionsCache = MapCache<(Arc<str>, Option<i64>), Options>;
47type SparkCacheKey = (Arc<str>, Interval, TimeRange);
48type SparkCache = MapCache<SparkCacheKey, Spark>;
49#[cfg(feature = "indicators")]
50type IndicatorsCache = MapCache<(Arc<str>, Interval, TimeRange), indicators::IndicatorsSummary>;
51
52type FetchGuard = Arc<tokio::sync::Mutex<()>>;
54type FetchGuardMap<K> = Arc<RwLock<HashMap<K, FetchGuard>>>;
55
56define_batch_response! {
58 BatchQuotesResponse => quotes: Quote
60}
61
62define_batch_response! {
63 BatchChartsResponse => charts: Chart
65}
66
67define_batch_response! {
68 BatchSparksResponse => sparks: Spark
73}
74
75define_batch_response! {
76 BatchDividendsResponse => dividends: Vec<Dividend>
78}
79
80define_batch_response! {
81 BatchSplitsResponse => splits: Vec<Split>
83}
84
85define_batch_response! {
86 BatchCapitalGainsResponse => capital_gains: Vec<CapitalGain>
88}
89
90define_batch_response! {
91 BatchFinancialsResponse => financials: FinancialStatement
93}
94
95define_batch_response! {
96 BatchNewsResponse => news: Vec<News>
98}
99
100define_batch_response! {
101 BatchRecommendationsResponse => recommendations: Recommendation
103}
104
105define_batch_response! {
106 BatchOptionsResponse => options: Options
108}
109
110#[cfg(feature = "indicators")]
111define_batch_response! {
112 BatchIndicatorsResponse => indicators: indicators::IndicatorsSummary
114}
115
116const DEFAULT_MAX_CONCURRENCY: usize = 10;
118
119pub struct TickersBuilder {
121 symbols: Vec<Arc<str>>,
122 config: ClientConfig,
123 shared_client: Option<ClientHandle>,
124 injected_providers: Option<Arc<ProviderSet>>,
125 max_concurrency: usize,
126 cache_ttl: Option<Duration>,
127 include_logo: bool,
128}
129
130impl TickersBuilder {
131 fn new<S, I>(symbols: I) -> Self
132 where
133 S: Into<String>,
134 I: IntoIterator<Item = S>,
135 {
136 Self {
137 symbols: symbols.into_iter().map(|s| s.into().into()).collect(),
138 config: ClientConfig::default(),
139 shared_client: None,
140 injected_providers: None,
141 max_concurrency: DEFAULT_MAX_CONCURRENCY,
142 cache_ttl: None,
143 include_logo: false,
144 }
145 }
146
147 pub fn region(mut self, region: Region) -> Self {
149 self.config.lang = region.lang().to_string();
150 self.config.region = region.region().to_string();
151 self
152 }
153
154 pub fn lang(mut self, lang: impl Into<String>) -> Self {
156 self.config.lang = lang.into();
157 self
158 }
159
160 pub fn region_code(mut self, region: impl Into<String>) -> Self {
162 self.config.region = region.into();
163 self
164 }
165
166 pub fn timeout(mut self, timeout: Duration) -> Self {
168 self.config.timeout = timeout;
169 self
170 }
171
172 pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
174 self.config.proxy = Some(proxy.into());
175 self
176 }
177
178 #[allow(dead_code)]
179 pub(crate) fn config(mut self, config: ClientConfig) -> Self {
180 self.config = config;
181 self
182 }
183
184 pub fn max_concurrency(mut self, n: usize) -> Self {
193 self.max_concurrency = n.max(1);
194 self
195 }
196
197 pub fn cache(mut self, ttl: Duration) -> Self {
218 self.cache_ttl = Some(ttl);
219 self
220 }
221
222 pub fn logo(mut self) -> Self {
227 self.include_logo = true;
228 self
229 }
230
231 pub(crate) fn with_provider_set(mut self, set: Arc<ProviderSet>) -> Self {
233 self.injected_providers = Some(set);
234 self
235 }
236
237 pub fn client(mut self, handle: ClientHandle) -> Self {
243 self.shared_client = Some(handle);
244 self
245 }
246
247 pub async fn build(self) -> Result<Tickers> {
249 #[cfg(feature = "translation")]
250 let translate_lang = {
251 let lang = crate::translation::Lang::parse(&self.config.lang)?;
252 (!lang.is_english()).then_some(lang)
253 };
254 let providers = if let Some(set) = self.injected_providers {
255 set
256 } else if let Some(handle) = self.shared_client {
257 let yahoo = YahooProvider::from_client(handle.0);
258 let client = yahoo.client_arc();
259 Arc::new(ProviderSet::new(
260 vec![Arc::new(yahoo) as Arc<dyn ProviderAdapter>],
261 Some(client),
262 Routes::new(Fetch::Sequential),
263 ))
264 } else {
265 Arc::new(
266 build_providers(
267 &[Provider::Yahoo],
268 &self.config,
269 Routes::new(Fetch::Sequential),
270 )
271 .await?,
272 )
273 };
274
275 Ok(Tickers {
276 symbols: self.symbols,
277 providers,
278 max_concurrency: self.max_concurrency,
279 cache_ttl: self.cache_ttl,
280 include_logo: self.include_logo,
281 #[cfg(feature = "translation")]
282 translate_lang,
283 quote_cache: Default::default(),
284 chart_cache: Default::default(),
285 events_cache: Default::default(),
286 financials_cache: Default::default(),
287 news_cache: Default::default(),
288 recommendations_cache: Default::default(),
289 options_cache: Default::default(),
290 spark_cache: Default::default(),
291 #[cfg(feature = "indicators")]
292 indicators_cache: Default::default(),
293
294 quotes_fetch: Arc::new(tokio::sync::Mutex::new(())),
296 charts_fetch: Default::default(),
297 financials_fetch: Default::default(),
298 news_fetch: Arc::new(tokio::sync::Mutex::new(())),
299 recommendations_fetch: Default::default(),
300 options_fetch: Default::default(),
301 spark_fetch: Default::default(),
302 #[cfg(feature = "indicators")]
303 indicators_fetch: Default::default(),
304 })
305 }
306}
307
308pub struct Tickers {
339 symbols: Vec<Arc<str>>,
340 providers: Arc<ProviderSet>,
341 max_concurrency: usize,
342 cache_ttl: Option<Duration>,
343 include_logo: bool,
344 #[cfg(feature = "translation")]
345 translate_lang: Option<crate::translation::Lang>,
346 quote_cache: QuoteCache,
347 chart_cache: ChartCache,
348 events_cache: EventsCache,
349 financials_cache: FinancialsCache,
350 news_cache: NewsCache,
351 recommendations_cache: RecommendationsCache,
352 options_cache: OptionsCache,
353 spark_cache: SparkCache,
354 #[cfg(feature = "indicators")]
355 indicators_cache: IndicatorsCache,
356
357 quotes_fetch: FetchGuard,
359 charts_fetch: FetchGuardMap<(Interval, TimeRange)>,
360 financials_fetch: FetchGuardMap<(StatementType, Frequency)>,
361 news_fetch: FetchGuard,
362 recommendations_fetch: FetchGuardMap<u32>,
363 options_fetch: FetchGuardMap<Option<i64>>,
364 spark_fetch: FetchGuardMap<(Interval, TimeRange)>,
365 #[cfg(feature = "indicators")]
366 indicators_fetch: FetchGuardMap<(Interval, TimeRange)>,
367}
368
369impl std::fmt::Debug for Tickers {
370 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371 f.debug_struct("Tickers")
372 .field("symbols", &self.symbols)
373 .field("max_concurrency", &self.max_concurrency)
374 .field("cache_ttl", &self.cache_ttl)
375 .finish_non_exhaustive()
376 }
377}
378
379impl Tickers {
380 pub async fn new<S, I>(symbols: I) -> Result<Self>
397 where
398 S: Into<String>,
399 I: IntoIterator<Item = S>,
400 {
401 Self::builder(symbols).build().await
402 }
403
404 pub fn builder<S, I>(symbols: I) -> TickersBuilder
406 where
407 S: Into<String>,
408 I: IntoIterator<Item = S>,
409 {
410 TickersBuilder::new(symbols)
411 }
412
413 pub fn symbols(&self) -> Vec<&str> {
415 self.symbols.iter().map(|s| &**s).collect()
416 }
417
418 pub fn len(&self) -> usize {
420 self.symbols.len()
421 }
422
423 pub fn is_empty(&self) -> bool {
425 self.symbols.is_empty()
426 }
427
428 pub fn client_handle(&self) -> ClientHandle {
440 ClientHandle(
441 self.providers
442 .first_yahoo()
443 .expect("Tickers always uses a Yahoo session"),
444 )
445 }
446
447 #[inline]
449 fn is_cache_fresh<T>(&self, entry: Option<&CacheEntry<T>>) -> bool {
450 CacheEntry::is_fresh_with_ttl(entry, self.cache_ttl)
451 }
452
453 #[cfg(feature = "translation")]
456 pub(crate) async fn translate_response<T: crate::translation::Translatable>(
457 &self,
458 value: &mut T,
459 ) -> Result<()> {
460 if let Some(lang) = &self.translate_lang {
461 crate::translation::translate_with(value, lang).await?;
462 }
463 Ok(())
464 }
465
466 fn all_cached<K: Eq + std::hash::Hash, V>(
468 &self,
469 map: &HashMap<K, CacheEntry<V>>,
470 keys: impl Iterator<Item = K>,
471 ) -> bool {
472 let Some(ttl) = self.cache_ttl else {
473 return false;
474 };
475 keys.into_iter()
476 .all(|k| map.get(&k).map(|e| e.is_fresh(ttl)).unwrap_or(false))
477 }
478
479 #[inline]
484 fn cache_insert<K: Eq + std::hash::Hash, V>(
485 &self,
486 map: &mut HashMap<K, CacheEntry<V>>,
487 key: K,
488 value: V,
489 ) {
490 if let Some(ttl) = self.cache_ttl {
491 if map.len() >= EVICTION_THRESHOLD {
492 map.retain(|_, entry| entry.is_fresh(ttl));
493 }
494 map.insert(key, CacheEntry::new(value));
495 }
496 }
497
498 pub async fn quotes(&self) -> Result<BatchQuotesResponse> {
506 {
508 let cache = self.quote_cache.read().await;
509 if self.all_cached(&cache, self.symbols.iter().cloned()) {
510 let mut response = BatchQuotesResponse::with_capacity(self.symbols.len());
511 for symbol in &self.symbols {
512 if let Some(entry) = cache.get(symbol) {
513 response
514 .quotes
515 .insert(symbol.to_string(), entry.value.clone());
516 }
517 }
518 return Ok(response);
519 }
520 }
521
522 let _fetch_guard = self.quotes_fetch.lock().await;
523
524 {
526 let cache = self.quote_cache.read().await;
527 if self.all_cached(&cache, self.symbols.iter().cloned()) {
528 let mut response = BatchQuotesResponse::with_capacity(self.symbols.len());
529 for symbol in &self.symbols {
530 if let Some(entry) = cache.get(symbol) {
531 response
532 .quotes
533 .insert(symbol.to_string(), entry.value.clone());
534 }
535 }
536 return Ok(response);
537 }
538 }
539
540 let symbol_strings: Vec<String> = self.symbols.iter().map(|s| s.to_string()).collect();
541 let mut response = BatchQuotesResponse::with_capacity(self.symbols.len());
542
543 let (quote_data, logos) = if self.include_logo {
544 let providers_logo = Arc::clone(&self.providers);
546 let syms_logo = symbol_strings.clone();
547 let logo_future = async move {
548 if let Ok(client) = providers_logo.first_yahoo() {
549 let syms_ref: Vec<&str> = syms_logo.iter().map(String::as_str).collect();
550 crate::adapters::yahoo::quote::quotes::fetch_with_fields(
551 &client,
552 &syms_ref,
553 Some(&["logoUrl", "companyLogoUrl"]),
554 true,
555 true,
556 )
557 .await
558 .ok()
559 } else {
560 None
561 }
562 };
563
564 let providers_quote = Arc::clone(&self.providers);
565 let syms_quote = symbol_strings.clone();
566 let quote_future = async move {
567 providers_quote
568 .fetch(Capability::QUOTE, |p| {
569 let syms = syms_quote.clone();
570 let p = p.clone();
571 async move {
572 let syms_ref: Vec<&str> = syms.iter().map(String::as_str).collect();
573 p.fetch_quotes_batch(&syms_ref).await
574 }
575 })
576 .await
577 };
578
579 let (batch_result, logo_result) = tokio::join!(quote_future, logo_future);
580 let quote_data = match batch_result {
581 Ok(data) => data,
582 Err(_) => {
583 self.fetch_quotes_per_symbol(&symbol_strings, &mut response)
584 .await
585 }
586 };
587 (quote_data, logo_result)
588 } else {
589 let providers = Arc::clone(&self.providers);
590 let syms = symbol_strings.clone();
591 let batch_result = providers
592 .fetch(Capability::QUOTE, |p| {
593 let syms = syms.clone();
594 let p = p.clone();
595 async move {
596 let syms_ref: Vec<&str> = syms.iter().map(String::as_str).collect();
597 p.fetch_quotes_batch(&syms_ref).await
598 }
599 })
600 .await;
601 let data = match batch_result {
602 Ok(data) => data,
603 Err(_) => {
604 self.fetch_quotes_per_symbol(&symbol_strings, &mut response)
605 .await
606 }
607 };
608 (data, None)
609 };
610
611 let logo_map: HashMap<String, (Option<String>, Option<String>)> = logos
612 .and_then(|l| l.get("quoteResponse")?.get("result")?.as_array().cloned())
613 .map(|results| {
614 results
615 .iter()
616 .filter_map(|r| {
617 let symbol = r.get("symbol")?.as_str()?.to_string();
618 let logo_url = r.get("logoUrl").and_then(|v| v.as_str()).map(String::from);
619 let company_logo_url = r
620 .get("companyLogoUrl")
621 .and_then(|v| v.as_str())
622 .map(String::from);
623 Some((symbol, (logo_url, company_logo_url)))
624 })
625 .collect()
626 })
627 .unwrap_or_default();
628
629 let mut parsed_quotes: Vec<(String, Quote)> = Vec::new();
630
631 for (symbol, summary) in quote_data {
632 let logo_url = logo_map.get(&symbol).and_then(|(l, _)| l.clone());
633 let company_logo_url = logo_map.get(&symbol).and_then(|(_, c)| c.clone());
634 let quote = Quote::from_response(&summary, logo_url, company_logo_url);
635 parsed_quotes.push((symbol, quote));
636 }
637
638 for (symbol, quote) in parsed_quotes {
639 response.quotes.insert(symbol, quote);
640 }
641
642 #[cfg(feature = "translation")]
645 self.translate_response(&mut response).await?;
646
647 if self.cache_ttl.is_some() {
648 let mut cache = self.quote_cache.write().await;
649 for (symbol, quote) in &response.quotes {
650 self.cache_insert(&mut cache, symbol.as_str().into(), quote.clone());
651 }
652 }
653
654 for symbol in &self.symbols {
656 let s = &**symbol;
657 if !response.quotes.contains_key(s) && !response.errors.contains_key(s) {
658 response.errors.insert(
659 symbol.to_string(),
660 "Symbol not found in response".to_string(),
661 );
662 }
663 }
664
665 Ok(response)
666 }
667
668 async fn fetch_quotes_per_symbol(
671 &self,
672 symbols: &[String],
673 response: &mut BatchQuotesResponse,
674 ) -> Vec<(String, QuoteSummaryResponse)> {
675 let futures: Vec<_> = symbols
676 .iter()
677 .map(|sym| {
678 let providers = Arc::clone(&self.providers);
679 let sym = sym.clone();
680 async move {
681 let result = providers
682 .fetch(Capability::QUOTE, |p| {
683 let sym = sym.clone();
684 let p = p.clone();
685 async move { p.fetch_quote(&sym).await }
686 })
687 .await;
688 (sym, result)
689 }
690 })
691 .collect();
692
693 let results: Vec<_> = stream::iter(futures)
694 .buffer_unordered(self.max_concurrency)
695 .collect()
696 .await;
697
698 let mut successes = Vec::new();
699 for (sym, result) in results {
700 match result {
701 Ok(resp) => successes.push((sym, resp)),
702 Err(e) => {
703 response.errors.insert(sym, e.to_string());
704 }
705 }
706 }
707 successes
708 }
709
710 pub async fn quote<F>(&self, symbol: &str) -> Result<Quote<F>>
712 where
713 F: Format,
714 Quote<Both>: Into<Quote<F>>,
715 {
716 {
717 let cache = self.quote_cache.read().await;
718 if let Some(entry) = cache.get(symbol)
719 && self.is_cache_fresh(Some(entry))
720 {
721 return Ok(entry.value.clone().into());
722 }
723 }
724
725 let response = self.quotes().await?;
726
727 response
728 .quotes
729 .get(symbol)
730 .cloned()
731 .map(Into::into)
732 .ok_or_else(|| FinanceError::SymbolNotFound {
733 symbol: Some(symbol.to_string()),
734 context: response
735 .errors
736 .get(symbol)
737 .cloned()
738 .unwrap_or_else(|| "Symbol not found".to_string()),
739 })
740 }
741
742 async fn get_fetch_guard<K: Clone + Eq + std::hash::Hash>(
747 guard_map: &FetchGuardMap<K>,
748 key: K,
749 ) -> FetchGuard {
750 {
751 let guards = guard_map.read().await;
752 if let Some(guard) = guards.get(&key) {
753 return Arc::clone(guard);
754 }
755 }
756
757 let mut guards = guard_map.write().await;
758 Arc::clone(
759 guards
760 .entry(key)
761 .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))),
762 )
763 }
764
765 pub async fn charts(
770 &self,
771 interval: Interval,
772 range: TimeRange,
773 ) -> Result<BatchChartsResponse> {
774 {
776 let cache = self.chart_cache.read().await;
777 if self.all_cached(
778 &cache,
779 self.symbols.iter().map(|s| (s.clone(), interval, range)),
780 ) {
781 let mut response = BatchChartsResponse::with_capacity(self.symbols.len());
782 for symbol in &self.symbols {
783 if let Some(entry) = cache.get(&(symbol.clone(), interval, range)) {
784 response
785 .charts
786 .insert(symbol.to_string(), entry.value.clone());
787 }
788 }
789 return Ok(response);
790 }
791 }
792
793 let fetch_guard = Self::get_fetch_guard(&self.charts_fetch, (interval, range)).await;
795 let _guard = fetch_guard.lock().await;
796
797 {
799 let cache = self.chart_cache.read().await;
800 if self.all_cached(
801 &cache,
802 self.symbols.iter().map(|s| (s.clone(), interval, range)),
803 ) {
804 let mut response = BatchChartsResponse::with_capacity(self.symbols.len());
805 for symbol in &self.symbols {
806 if let Some(entry) = cache.get(&(symbol.clone(), interval, range)) {
807 response
808 .charts
809 .insert(symbol.to_string(), entry.value.clone());
810 }
811 }
812 return Ok(response);
813 }
814 }
815
816 let futures: Vec<_> = self
818 .symbols
819 .iter()
820 .map(|symbol| {
821 let providers = Arc::clone(&self.providers);
822 let symbol = Arc::clone(symbol);
823 async move {
824 let sym = symbol.to_string();
825 let result = providers
826 .fetch(Capability::CHART, |p| {
827 let sym = sym.clone();
828 let p = p.clone();
829 async move { p.fetch_chart(&sym, interval, range).await }
830 })
831 .await;
832 (symbol, result)
833 }
834 })
835 .collect();
836
837 let results: Vec<_> = stream::iter(futures)
838 .buffer_unordered(self.max_concurrency)
839 .collect()
840 .await;
841
842 let mut response = BatchChartsResponse::with_capacity(self.symbols.len());
843 let mut parsed_charts: Vec<(Arc<str>, Chart)> = Vec::new();
844
845 for (symbol, result) in results {
846 match result {
847 Ok(data) => {
848 let chart = data;
849 parsed_charts.push((symbol, chart));
850 }
851 Err(e) => {
852 response.errors.insert(symbol.to_string(), e.to_string());
853 }
854 }
855 }
856
857 if self.cache_ttl.is_some() {
859 let mut cache = self.chart_cache.write().await;
860 let cache_keys: Vec<_> = parsed_charts
861 .into_iter()
862 .map(|(symbol, chart)| {
863 self.cache_insert(&mut cache, (symbol.clone(), interval, range), chart);
864 symbol
865 })
866 .collect();
867 for symbol in cache_keys {
868 if let Some(cached) = cache.get(&(symbol.clone(), interval, range)) {
869 response
870 .charts
871 .insert(symbol.to_string(), cached.value.clone());
872 }
873 }
874 } else {
875 for (symbol, chart) in parsed_charts {
876 response.charts.insert(symbol.to_string(), chart);
877 }
878 }
879
880 Ok(response)
881 }
882
883 pub async fn chart(&self, symbol: &str, interval: Interval, range: TimeRange) -> Result<Chart> {
885 {
886 let cache = self.chart_cache.read().await;
887 let key: Arc<str> = symbol.into();
888 if let Some(entry) = cache.get(&(key, interval, range))
889 && self.is_cache_fresh(Some(entry))
890 {
891 return Ok(entry.value.clone());
892 }
893 }
894
895 let response = self.charts(interval, range).await?;
896
897 response
898 .charts
899 .get(symbol)
900 .cloned()
901 .ok_or_else(|| FinanceError::SymbolNotFound {
902 symbol: Some(symbol.to_string()),
903 context: response
904 .errors
905 .get(symbol)
906 .cloned()
907 .unwrap_or_else(|| "Symbol not found".to_string()),
908 })
909 }
910
911 pub async fn charts_range(
923 &self,
924 interval: Interval,
925 start: i64,
926 end: i64,
927 ) -> Result<BatchChartsResponse> {
928 let futures: Vec<_> = self
929 .symbols
930 .iter()
931 .map(|symbol| {
932 let providers = Arc::clone(&self.providers);
933 let symbol = Arc::clone(symbol);
934 async move {
935 let sym = symbol.to_string();
936 let result = providers
937 .fetch(Capability::CHART, |p| {
938 let sym = sym.clone();
939 let p = p.clone();
940 async move { p.fetch_chart_range(&sym, interval, start, end).await }
941 })
942 .await;
943 (symbol, result)
944 }
945 })
946 .collect();
947
948 let results: Vec<_> = stream::iter(futures)
949 .buffer_unordered(self.max_concurrency)
950 .collect()
951 .await;
952
953 let mut response = BatchChartsResponse::with_capacity(self.symbols.len());
954
955 for (symbol, result) in results {
956 match result {
957 Ok(data) => {
958 let chart = data;
959 response.charts.insert(symbol.to_string(), chart);
960 }
961 Err(e) => {
962 response.errors.insert(symbol.to_string(), e.to_string());
963 }
964 }
965 }
966
967 Ok(response)
968 }
969
970 async fn ensure_events_loaded(&self) -> Result<()> {
980 let symbols_to_fetch: Vec<Arc<str>> = {
982 let cache = self.events_cache.read().await;
983 self.symbols
984 .iter()
985 .filter(|sym| !cache.contains_key(*sym))
986 .cloned()
987 .collect()
988 };
989
990 if symbols_to_fetch.is_empty() {
991 return Ok(());
992 }
993
994 let futures: Vec<_> = symbols_to_fetch
996 .iter()
997 .map(|symbol| {
998 let providers = Arc::clone(&self.providers);
999 let symbol = Arc::clone(symbol);
1000 async move {
1001 let sym = symbol.to_string();
1002 let result = providers
1003 .fetch(Capability::CORPORATE, |p| {
1004 let sym = sym.clone();
1005 let p = p.clone();
1006 async move { p.fetch_events(&sym).await }
1007 })
1008 .await;
1009 (symbol, result)
1010 }
1011 })
1012 .collect();
1013
1014 let results: Vec<_> = stream::iter(futures)
1015 .buffer_unordered(self.max_concurrency)
1016 .collect()
1017 .await;
1018
1019 let mut parsed_events: Vec<(Arc<str>, ChartEvents)> = Vec::new();
1020
1021 for (symbol, result) in results {
1022 if let Ok(events_data) = result {
1023 parsed_events.push((symbol, events_data));
1024 }
1025 }
1026
1027 if !parsed_events.is_empty() {
1029 let mut events_cache = self.events_cache.write().await;
1030 for (symbol, events) in parsed_events {
1031 events_cache.insert(symbol, CacheEntry::new(events));
1032 }
1033 }
1034
1035 Ok(())
1036 }
1037
1038 pub async fn spark(&self, interval: Interval, range: TimeRange) -> Result<BatchSparksResponse> {
1067 {
1069 let cache = self.spark_cache.read().await;
1070 if self.all_cached(
1071 &cache,
1072 self.symbols.iter().map(|s| (s.clone(), interval, range)),
1073 ) {
1074 let mut response = BatchSparksResponse::with_capacity(self.symbols.len());
1075 for symbol in &self.symbols {
1076 if let Some(entry) = cache.get(&(symbol.clone(), interval, range)) {
1077 response
1078 .sparks
1079 .insert(symbol.to_string(), entry.value.clone());
1080 }
1081 }
1082 return Ok(response);
1083 }
1084 }
1085
1086 let fetch_guard = Self::get_fetch_guard(&self.spark_fetch, (interval, range)).await;
1088 let _guard = fetch_guard.lock().await;
1089
1090 {
1092 let cache = self.spark_cache.read().await;
1093 if self.all_cached(
1094 &cache,
1095 self.symbols.iter().map(|s| (s.clone(), interval, range)),
1096 ) {
1097 let mut response = BatchSparksResponse::with_capacity(self.symbols.len());
1098 for symbol in &self.symbols {
1099 if let Some(entry) = cache.get(&(symbol.clone(), interval, range)) {
1100 response
1101 .sparks
1102 .insert(symbol.to_string(), entry.value.clone());
1103 }
1104 }
1105 return Ok(response);
1106 }
1107 }
1108
1109 let providers = Arc::clone(&self.providers);
1112 let syms: Vec<String> = self.symbols.iter().map(|s| s.to_string()).collect();
1113 let spark_result = providers
1114 .fetch(Capability::CHART, |p| {
1115 let syms = syms.clone();
1116 let p = p.clone();
1117 async move {
1118 let syms_ref: Vec<&str> = syms.iter().map(String::as_str).collect();
1119 p.fetch_spark(&syms_ref, interval, range).await
1120 }
1121 })
1122 .await;
1123
1124 let mut response = BatchSparksResponse::with_capacity(self.symbols.len());
1125
1126 match spark_result {
1127 Ok(parsed_sparks) => {
1128 if self.cache_ttl.is_some() {
1130 let mut cache = self.spark_cache.write().await;
1131 for (symbol, spark) in &parsed_sparks {
1132 let key: Arc<str> = symbol.as_str().into();
1133 self.cache_insert(&mut cache, (key, interval, range), spark.clone());
1134 }
1135 }
1136
1137 for (symbol, spark) in parsed_sparks {
1139 response.sparks.insert(symbol, spark);
1140 }
1141
1142 for symbol in &self.symbols {
1144 let symbol_str = &**symbol;
1145 if !response.sparks.contains_key(symbol_str)
1146 && !response.errors.contains_key(symbol_str)
1147 {
1148 response.errors.insert(
1149 symbol.to_string(),
1150 "Symbol not found in response".to_string(),
1151 );
1152 }
1153 }
1154 }
1155 Err(e) => {
1156 for symbol in &self.symbols {
1157 response.errors.insert(symbol.to_string(), e.to_string());
1158 }
1159 }
1160 }
1161
1162 Ok(response)
1163 }
1164
1165 pub async fn dividends(&self, range: TimeRange) -> Result<BatchDividendsResponse> {
1190 let mut response = BatchDividendsResponse::with_capacity(self.symbols.len());
1191
1192 self.ensure_events_loaded().await?;
1194
1195 let events_cache = self.events_cache.read().await;
1196
1197 for symbol in &self.symbols {
1198 if let Some(entry) = events_cache.get(symbol) {
1199 let all_dividends = entry.value.to_dividends();
1200 let filtered = filter_by_range(all_dividends, range);
1201 response.dividends.insert(symbol.to_string(), filtered);
1202 } else {
1203 response
1204 .errors
1205 .insert(symbol.to_string(), "No events data available".to_string());
1206 }
1207 }
1208
1209 Ok(response)
1210 }
1211
1212 pub async fn splits(&self, range: TimeRange) -> Result<BatchSplitsResponse> {
1239 let mut response = BatchSplitsResponse::with_capacity(self.symbols.len());
1240
1241 self.ensure_events_loaded().await?;
1243
1244 let events_cache = self.events_cache.read().await;
1245
1246 for symbol in &self.symbols {
1247 if let Some(entry) = events_cache.get(symbol) {
1248 let all_splits = entry.value.to_splits();
1249 let filtered = filter_by_range(all_splits, range);
1250 response.splits.insert(symbol.to_string(), filtered);
1251 } else {
1252 response
1253 .errors
1254 .insert(symbol.to_string(), "No events data available".to_string());
1255 }
1256 }
1257
1258 Ok(response)
1259 }
1260
1261 pub async fn capital_gains(&self, range: TimeRange) -> Result<BatchCapitalGainsResponse> {
1287 let mut response = BatchCapitalGainsResponse::with_capacity(self.symbols.len());
1288
1289 self.ensure_events_loaded().await?;
1291
1292 let events_cache = self.events_cache.read().await;
1293
1294 for symbol in &self.symbols {
1295 if let Some(entry) = events_cache.get(symbol) {
1296 let all_gains = entry.value.to_capital_gains();
1297 let filtered = filter_by_range(all_gains, range);
1298 response.capital_gains.insert(symbol.to_string(), filtered);
1299 } else {
1300 response
1301 .errors
1302 .insert(symbol.to_string(), "No events data available".to_string());
1303 }
1304 }
1305
1306 Ok(response)
1307 }
1308
1309 pub async fn financials(
1337 &self,
1338 statement_type: StatementType,
1339 frequency: Frequency,
1340 ) -> Result<BatchFinancialsResponse> {
1341 batch_fetch_cached!(self;
1342 cache: financials_cache,
1343 guard: map(financials_fetch, (statement_type, frequency)),
1344 key: |s| (s.clone(), statement_type, frequency),
1345 response: BatchFinancialsResponse.financials,
1346 fetch: |providers, symbol| {
1347 let sym = symbol.to_string();
1348 providers.fetch(Capability::FUNDAMENTALS, move |p| {
1349 let sym = sym.clone();
1350 let p = p.clone();
1351 async move {
1352 p.fetch_financials(&sym, statement_type, frequency)
1353 .await
1354 }
1355 }).await
1356 },
1357 )
1358 }
1359
1360 pub async fn news(&self) -> Result<BatchNewsResponse> {
1384 batch_fetch_cached!(self;
1385 cache: news_cache,
1386 guard: simple(news_fetch),
1387 key: |s| s.clone(),
1388 response: BatchNewsResponse.news,
1389 fetch: |providers, symbol| {
1390 let sym = symbol.to_string();
1391 providers.fetch(Capability::CORPORATE, move |p| {
1392 let sym = sym.clone();
1393 let p = p.clone();
1394 async move {
1395 p.fetch_news(&sym)
1396 .await
1397 .map(|data| data.into_iter().collect::<Vec<News>>())
1398 }
1399 }).await
1400 },
1401 )
1402 }
1403
1404 pub async fn recommendations(&self, limit: u32) -> Result<BatchRecommendationsResponse> {
1433 batch_fetch_cached!(self;
1434 cache: recommendations_cache,
1435 guard: map(recommendations_fetch, limit),
1436 key: |s| (s.clone(), limit),
1437 response: BatchRecommendationsResponse.recommendations,
1438 fetch: |providers, symbol| {
1439 let sym = symbol.to_string();
1440 providers.fetch(Capability::CORPORATE, move |p| {
1441 let sym = sym.clone();
1442 let p = p.clone();
1443 async move {
1444 let items = p.fetch_similar_symbols(&sym, limit).await?;
1445 Ok(recommendation_from_similar(
1446 sym,
1447 Some(p.id()),
1448 items,
1449 Some(limit),
1450 ))
1451 }
1452 }).await
1453 },
1454 )
1455 }
1456
1457 pub async fn options(&self, date: Option<i64>) -> Result<BatchOptionsResponse> {
1482 batch_fetch_cached!(self;
1483 cache: options_cache,
1484 guard: map(options_fetch, date),
1485 key: |s| (s.clone(), date),
1486 response: BatchOptionsResponse.options,
1487 fetch: |providers, symbol| {
1488 let sym = symbol.to_string();
1489 providers.fetch(Capability::OPTIONS, move |p| {
1490 let sym = sym.clone();
1491 let p = p.clone();
1492 async move {
1493 p.fetch_options(&sym, date).await
1494 }
1495 }).await
1496 },
1497 )
1498 }
1499
1500 pub async fn calendar(
1526 &self,
1527 range: TimeRange,
1528 ) -> Result<Vec<crate::models::calendar::CalendarEvent>> {
1529 let now = chrono::Utc::now().timestamp();
1530 let window = (now, now + range.approx_duration_secs());
1531
1532 let symbol_strings: Vec<String> = self.symbols.iter().map(|s| s.to_string()).collect();
1533 let providers = Arc::clone(&self.providers);
1534
1535 let per_symbol = symbol_strings.into_iter().map(|sym| {
1536 let providers = Arc::clone(&providers);
1537 async move {
1538 let quote_fut = {
1539 let sym = sym.clone();
1540 providers.fetch(Capability::QUOTE, move |p| {
1541 let sym = sym.clone();
1542 let p = p.clone();
1543 async move { p.fetch_quote(&sym).await }
1544 })
1545 };
1546 let opts_fut = {
1547 let sym = sym.clone();
1548 providers.fetch(Capability::OPTIONS, move |p| {
1549 let sym = sym.clone();
1550 let p = p.clone();
1551 async move { p.fetch_options(&sym, None).await }
1552 })
1553 };
1554 let (quote, options) = tokio::join!(quote_fut, opts_fut);
1555 let calendar_events = quote.ok().and_then(|q| q.calendar_events);
1556 let options = options.ok();
1557 crate::models::calendar::build_symbol_events(
1558 &sym,
1559 calendar_events.as_ref(),
1560 options.as_ref(),
1561 window,
1562 )
1563 }
1564 });
1565
1566 let per_symbol_fut = stream::iter(per_symbol)
1567 .buffer_unordered(self.max_concurrency)
1568 .collect::<Vec<_>>();
1569
1570 #[cfg(feature = "fred")]
1573 let (per_symbol_events, releases) =
1574 tokio::join!(per_symbol_fut, crate::adapters::fred::release_dates());
1575 #[cfg(not(feature = "fred"))]
1576 let per_symbol_events = per_symbol_fut.await;
1577
1578 let mut events: Vec<crate::models::calendar::CalendarEvent> =
1579 per_symbol_events.into_iter().flatten().collect();
1580
1581 #[cfg(feature = "fred")]
1582 if let Ok(releases) = releases {
1583 events.extend(crate::models::calendar::build_economic_events(
1584 releases, window,
1585 ));
1586 }
1587
1588 crate::models::calendar::sort_events(&mut events);
1589 Ok(events)
1590 }
1591
1592 #[cfg(feature = "indicators")]
1618 pub async fn indicators(
1619 &self,
1620 interval: Interval,
1621 range: TimeRange,
1622 ) -> Result<BatchIndicatorsResponse> {
1623 let cache_key_for = |symbol: &Arc<str>| (symbol.clone(), interval, range);
1624
1625 {
1627 let cache = self.indicators_cache.read().await;
1628 if self.all_cached(&cache, self.symbols.iter().map(&cache_key_for)) {
1629 let mut response = BatchIndicatorsResponse::with_capacity(self.symbols.len());
1630 for symbol in &self.symbols {
1631 if let Some(entry) = cache.get(&cache_key_for(symbol)) {
1632 response
1633 .indicators
1634 .insert(symbol.to_string(), entry.value.clone());
1635 }
1636 }
1637 return Ok(response);
1638 }
1639 }
1640
1641 let fetch_guard = Self::get_fetch_guard(&self.indicators_fetch, (interval, range)).await;
1643 let _guard = fetch_guard.lock().await;
1644
1645 {
1647 let cache = self.indicators_cache.read().await;
1648 if self.all_cached(&cache, self.symbols.iter().map(&cache_key_for)) {
1649 let mut response = BatchIndicatorsResponse::with_capacity(self.symbols.len());
1650 for symbol in &self.symbols {
1651 if let Some(entry) = cache.get(&cache_key_for(symbol)) {
1652 response
1653 .indicators
1654 .insert(symbol.to_string(), entry.value.clone());
1655 }
1656 }
1657 return Ok(response);
1658 }
1659 }
1660
1661 let charts_response = self.charts(interval, range).await?;
1663
1664 let mut response = BatchIndicatorsResponse::with_capacity(self.symbols.len());
1665
1666 let mut calculated_indicators: Vec<(String, indicators::IndicatorsSummary)> = Vec::new();
1668
1669 for (symbol, chart) in &charts_response.charts {
1670 let indicators = indicators::summary::calculate_indicators(&chart.candles);
1671 calculated_indicators.push((symbol.to_string(), indicators));
1672 }
1673
1674 if self.cache_ttl.is_some() {
1676 let mut cache = self.indicators_cache.write().await;
1677 for (symbol, indicators) in &calculated_indicators {
1678 let key: Arc<str> = symbol.as_str().into();
1679 self.cache_insert(&mut cache, cache_key_for(&key), indicators.clone());
1680 }
1681 }
1682
1683 for (symbol, indicators) in calculated_indicators {
1685 response.indicators.insert(symbol, indicators);
1686 }
1687
1688 for (symbol, error) in &charts_response.errors {
1690 response.errors.insert(symbol.to_string(), error.clone());
1691 }
1692
1693 Ok(response)
1694 }
1695
1696 pub fn add_symbols<S, I>(&mut self, symbols: I)
1717 where
1718 S: Into<String>,
1719 I: IntoIterator<Item = S>,
1720 {
1721 use std::collections::HashSet;
1723
1724 let existing: HashSet<&str> = self.symbols.iter().map(|s| &**s).collect();
1725 let to_add: Vec<Arc<str>> = symbols
1726 .into_iter()
1727 .map(Into::into)
1728 .filter(|s| !existing.contains(s.as_str()))
1729 .map(|s| s.into())
1730 .collect();
1731
1732 self.symbols.extend(to_add);
1733 }
1734
1735 #[cfg(feature = "backtesting")]
1774 pub async fn backtest<S, F>(
1775 &self,
1776 interval: Interval,
1777 range: TimeRange,
1778 config: Option<backtesting::portfolio::PortfolioConfig>,
1779 factory: F,
1780 ) -> backtesting::Result<backtesting::portfolio::PortfolioResult>
1781 where
1782 S: backtesting::Strategy,
1783 F: Fn(&str) -> S,
1784 {
1785 use crate::backtesting::portfolio::{PortfolioEngine, SymbolData};
1786
1787 let config = config.unwrap_or_default();
1788 config.validate(self.symbols.len())?;
1789
1790 let charts = self
1792 .charts(interval, range)
1793 .await
1794 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
1795
1796 let dividends_map = self
1799 .dividends(range)
1800 .await
1801 .map(|b| b.dividends)
1802 .unwrap_or_default();
1803
1804 let symbol_data: Vec<SymbolData> = self
1806 .symbols
1807 .iter()
1808 .filter_map(|sym| {
1809 charts.charts.get(sym.as_ref()).map(|chart| {
1810 let divs = dividends_map.get(sym.as_ref()).cloned().unwrap_or_default();
1811 SymbolData::new(sym.as_ref(), chart.candles.clone()).with_dividends(divs)
1812 })
1813 })
1814 .collect();
1815
1816 let engine = PortfolioEngine::new(config);
1817 engine.run(&symbol_data, factory)
1818 }
1819
1820 pub async fn remove_symbols<S, I>(&mut self, symbols: I)
1837 where
1838 S: Into<String>,
1839 I: IntoIterator<Item = S>,
1840 {
1841 use std::collections::HashSet;
1842 let owned: Vec<String> = symbols.into_iter().map(Into::into).collect();
1843 let to_remove: HashSet<&str> = owned.iter().map(|s| s.as_str()).collect();
1844
1845 self.symbols.retain(|s| !to_remove.contains(&**s));
1847
1848 let (
1850 mut quote_cache,
1851 mut chart_cache,
1852 mut events_cache,
1853 mut financials_cache,
1854 mut news_cache,
1855 mut recommendations_cache,
1856 mut options_cache,
1857 mut spark_cache,
1858 ) = tokio::join!(
1859 self.quote_cache.write(),
1860 self.chart_cache.write(),
1861 self.events_cache.write(),
1862 self.financials_cache.write(),
1863 self.news_cache.write(),
1864 self.recommendations_cache.write(),
1865 self.options_cache.write(),
1866 self.spark_cache.write(),
1867 );
1868
1869 for symbol in &to_remove {
1871 let key: Arc<str> = (*symbol).into();
1872 quote_cache.remove(&key);
1873 events_cache.remove(&key);
1874 news_cache.remove(&key);
1875 }
1876
1877 chart_cache.retain(|(sym, _, _), _| !to_remove.contains(&**sym));
1879 financials_cache.retain(|(sym, _, _), _| !to_remove.contains(&**sym));
1880 recommendations_cache.retain(|(sym, _), _| !to_remove.contains(&**sym));
1881 options_cache.retain(|(sym, _), _| !to_remove.contains(&**sym));
1882 spark_cache.retain(|(sym, _, _), _| !to_remove.contains(&**sym));
1883
1884 drop((
1886 quote_cache,
1887 chart_cache,
1888 events_cache,
1889 financials_cache,
1890 news_cache,
1891 recommendations_cache,
1892 options_cache,
1893 spark_cache,
1894 ));
1895
1896 #[cfg(feature = "indicators")]
1897 self.indicators_cache
1898 .write()
1899 .await
1900 .retain(|(sym, _, _), _| !to_remove.contains(&**sym));
1901 }
1902
1903 pub async fn clear_cache(&self) {
1908 tokio::join!(
1909 async { self.quote_cache.write().await.clear() },
1911 async { self.chart_cache.write().await.clear() },
1912 async { self.events_cache.write().await.clear() },
1913 async { self.financials_cache.write().await.clear() },
1914 async { self.news_cache.write().await.clear() },
1915 async { self.recommendations_cache.write().await.clear() },
1916 async { self.options_cache.write().await.clear() },
1917 async { self.spark_cache.write().await.clear() },
1918 async {
1919 #[cfg(feature = "indicators")]
1920 self.indicators_cache.write().await.clear();
1921 },
1922 async { self.charts_fetch.write().await.clear() },
1924 async { self.financials_fetch.write().await.clear() },
1925 async { self.recommendations_fetch.write().await.clear() },
1926 async { self.options_fetch.write().await.clear() },
1927 async { self.spark_fetch.write().await.clear() },
1928 async {
1929 #[cfg(feature = "indicators")]
1930 self.indicators_fetch.write().await.clear();
1931 },
1932 );
1933 }
1934
1935 pub async fn clear_quote_cache(&self) {
1939 self.quote_cache.write().await.clear();
1940 }
1941
1942 pub async fn clear_chart_cache(&self) {
1947 tokio::join!(
1948 async { self.chart_cache.write().await.clear() },
1949 async { self.events_cache.write().await.clear() },
1950 async { self.spark_cache.write().await.clear() },
1951 async {
1952 #[cfg(feature = "indicators")]
1953 self.indicators_cache.write().await.clear();
1954 },
1955 );
1956 }
1957}
1958
1959#[cfg(test)]
1960mod tests {
1961 use super::*;
1962
1963 #[tokio::test]
1964 #[ignore = "requires network access"]
1965 async fn test_tickers_quotes() {
1966 let tickers = Tickers::new(["AAPL", "MSFT", "GOOGL"]).await.unwrap();
1967 let result = tickers.quotes().await.unwrap();
1968
1969 assert!(result.success_count() > 0);
1970 }
1971
1972 #[tokio::test]
1973 #[ignore = "requires network access"]
1974 async fn test_tickers_charts() {
1975 let tickers = Tickers::new(["AAPL", "MSFT"]).await.unwrap();
1976 let result = tickers
1977 .charts(Interval::OneDay, TimeRange::FiveDays)
1978 .await
1979 .unwrap();
1980
1981 assert!(result.success_count() > 0);
1982 }
1983
1984 #[tokio::test]
1985 #[ignore = "requires network access"]
1986 async fn test_tickers_spark() {
1987 let tickers = Tickers::new(["AAPL", "MSFT", "GOOGL"]).await.unwrap();
1988 let result = tickers
1989 .spark(Interval::FiveMinutes, TimeRange::OneDay)
1990 .await
1991 .unwrap();
1992
1993 assert!(result.success_count() > 0);
1994
1995 if let Some(spark) = result.sparks.get("AAPL") {
1997 assert!(!spark.closes.is_empty());
1998 assert_eq!(spark.symbol, "AAPL");
1999 assert!(spark.percent_change().is_some());
2001 }
2002 }
2003
2004 #[tokio::test]
2005 #[ignore = "requires network access"]
2006 async fn test_tickers_dividends() {
2007 let tickers = Tickers::new(["AAPL", "MSFT"]).await.unwrap();
2008 let result = tickers.dividends(TimeRange::OneYear).await.unwrap();
2009
2010 assert!(result.success_count() > 0);
2011
2012 if let Some(dividends) = result.dividends.get("AAPL")
2014 && !dividends.is_empty()
2015 {
2016 let div = ÷nds[0];
2017 assert!(div.timestamp > 0);
2018 assert!(div.amount > 0.0);
2019 }
2020 }
2021
2022 #[tokio::test]
2023 #[ignore = "requires network access"]
2024 async fn test_tickers_splits() {
2025 let tickers = Tickers::new(["NVDA", "TSLA"]).await.unwrap();
2026 let result = tickers.splits(TimeRange::FiveYears).await.unwrap();
2027
2028 assert!(result.success_count() > 0);
2030
2031 for splits in result.splits.values() {
2033 for split in splits {
2034 assert!(split.timestamp > 0);
2035 assert!(split.numerator > 0.0);
2036 assert!(split.denominator > 0.0);
2037 assert!(!split.ratio.is_empty());
2038 }
2039 }
2040 }
2041
2042 #[tokio::test]
2043 #[ignore = "requires network access"]
2044 async fn test_tickers_capital_gains() {
2045 let tickers = Tickers::new(["VFIAX", "VTI"]).await.unwrap();
2046 let result = tickers.capital_gains(TimeRange::TwoYears).await.unwrap();
2047
2048 assert!(result.success_count() > 0);
2050
2051 for gains in result.capital_gains.values() {
2053 for gain in gains {
2054 assert!(gain.timestamp > 0);
2055 assert!(gain.amount >= 0.0);
2056 }
2057 }
2058 }
2059
2060 #[tokio::test]
2061 #[ignore = "requires network access"]
2062 async fn test_tickers_financials() {
2063 let tickers = Tickers::new(["AAPL", "MSFT"]).await.unwrap();
2064 let result = tickers
2065 .financials(StatementType::Income, Frequency::Annual)
2066 .await
2067 .unwrap();
2068
2069 assert!(result.success_count() > 0);
2070
2071 for (symbol, stmt) in &result.financials {
2073 assert_eq!(stmt.symbol, *symbol);
2074 assert_eq!(stmt.statement_type, "income");
2075 assert_eq!(stmt.frequency, "annual");
2076 assert!(!stmt.statement.is_empty());
2077
2078 if let Some(revenue) = stmt.statement.get("TotalRevenue") {
2080 assert!(!revenue.is_empty());
2081 }
2082 }
2083 }
2084
2085 #[tokio::test]
2086 #[ignore = "requires network access"]
2087 async fn test_tickers_news() {
2088 let tickers = Tickers::new(["AAPL", "TSLA"]).await.unwrap();
2089 let result = tickers.news().await.unwrap();
2090
2091 assert!(result.success_count() > 0);
2092
2093 for articles in result.news.values() {
2095 if !articles.is_empty() {
2096 let article = &articles[0];
2097 assert!(!article.title.is_empty());
2098 assert!(!article.link.is_empty());
2099 assert!(!article.source.is_empty());
2100 }
2101 }
2102 }
2103
2104 #[tokio::test]
2105 #[ignore = "requires network access"]
2106 async fn test_tickers_recommendations() {
2107 let tickers = Tickers::new(["AAPL", "MSFT"]).await.unwrap();
2108 let result = tickers.recommendations(5).await.unwrap();
2109
2110 assert!(result.success_count() > 0);
2111
2112 for (symbol, rec) in &result.recommendations {
2114 assert_eq!(rec.symbol, *symbol);
2115 assert!(rec.count() > 0);
2116 for similar in &rec.recommendations {
2117 assert!(!similar.symbol.is_empty());
2118 }
2119 }
2120 }
2121
2122 #[tokio::test]
2123 #[ignore = "requires network access"]
2124 async fn test_tickers_options() {
2125 let tickers = Tickers::new(["AAPL", "MSFT"]).await.unwrap();
2126 let result = tickers.options(None).await.unwrap();
2127
2128 assert!(result.success_count() > 0);
2129
2130 for opts in result.options.values() {
2132 assert!(!opts.expiration_dates().is_empty());
2133 }
2134 }
2135
2136 #[tokio::test]
2137 #[ignore = "requires network access"]
2138 #[cfg(feature = "indicators")]
2139 async fn test_tickers_indicators() {
2140 let tickers = Tickers::new(["AAPL", "MSFT"]).await.unwrap();
2141 let result = tickers
2142 .indicators(Interval::OneDay, TimeRange::ThreeMonths)
2143 .await
2144 .unwrap();
2145
2146 assert!(result.success_count() > 0);
2147
2148 for ind in result.indicators.values() {
2150 assert!(ind.rsi_14.is_some() || ind.sma_20.is_some());
2152 }
2153 }
2154
2155 #[tokio::test]
2156 async fn test_tickers_add_symbols() {
2157 let mut tickers = Tickers::new(["AAPL"]).await.unwrap();
2158 assert_eq!(tickers.len(), 1);
2159 assert_eq!(tickers.symbols(), &["AAPL"]);
2160
2161 tickers.add_symbols(["MSFT", "GOOGL"]);
2162 assert_eq!(tickers.len(), 3);
2163 assert!(tickers.symbols().contains(&"AAPL"));
2164 assert!(tickers.symbols().contains(&"MSFT"));
2165 assert!(tickers.symbols().contains(&"GOOGL"));
2166
2167 tickers.add_symbols(["AAPL"]);
2169 assert_eq!(tickers.len(), 3);
2170 }
2171
2172 #[tokio::test]
2173 #[ignore = "requires network access"]
2174 async fn test_tickers_remove_symbols() {
2175 let mut tickers = Tickers::new(["AAPL", "MSFT", "GOOGL"]).await.unwrap();
2176 assert_eq!(tickers.len(), 3);
2177
2178 let _ = tickers.quotes().await;
2180
2181 tickers.remove_symbols(["MSFT"]).await;
2183 assert_eq!(tickers.len(), 2);
2184 assert!(tickers.symbols().contains(&"AAPL"));
2185 assert!(!tickers.symbols().contains(&"MSFT"));
2186 assert!(tickers.symbols().contains(&"GOOGL"));
2187
2188 let quotes = tickers.quotes().await.unwrap();
2190 assert!(!quotes.quotes.contains_key("MSFT"));
2191 assert_eq!(quotes.quotes.len(), 2);
2192 }
2193}