1use crate::adapters::yahoo::client::{ClientConfig, YahooClient};
4#[cfg(feature = "backtesting")]
5use crate::backtesting;
6use crate::constants::{Frequency, Interval, Region, StatementType, TimeRange};
7use crate::edgar;
8use crate::error::{FinanceError, Result};
9use crate::format::Both;
10#[cfg(any(feature = "backtesting", feature = "indicators"))]
11use crate::indicators;
12use crate::models::chart::events::ChartEvents;
13use crate::models::chart::{CapitalGain, Chart, Dividend, DividendAnalytics, Split};
14use crate::models::corporate::news::News;
15use crate::models::corporate::recommendation::Recommendation;
16use crate::models::filings::{CompanyFacts, EdgarSubmissions, ProviderFilings};
17use crate::models::format::Format;
18use crate::models::fundamentals::FinancialStatement;
19use crate::models::options::Options;
20use crate::models::quote::{
21 AssetProfile, CalendarEvents, DefaultKeyStatistics, Earnings, EarningsHistory, EarningsTrend,
22 EquityPerformance, FinancialData, FundOwnership, FundPerformance, FundProfile, IndexTrend,
23 IndustryTrend, InsiderHolders, InsiderTransactions, InstitutionOwnership,
24 MajorHoldersBreakdown, NetSharePurchaseActivity, Price, Quote, QuoteSummaryResponse,
25 QuoteTypeData, RecommendationTrend, SecFilings, SectorTrend, SummaryDetail, SummaryProfile,
26 TopHoldings, UpgradeDowngradeHistory,
27};
28
29use crate::providers::types::recommendation_from_similar;
30use crate::providers::yahoo::YahooProvider;
31use crate::providers::{
32 Capability, Fetch, Provider, ProviderAdapter, ProviderSet, Routes, build_providers,
33};
34#[cfg(feature = "risk")]
35use crate::risk;
36use crate::utils::{CacheEntry, EVICTION_THRESHOLD, filter_by_range};
37use std::collections::HashMap;
38use std::sync::Arc;
39use std::time::Duration;
40use tokio::sync::RwLock;
41
42type Cache<T> = Arc<RwLock<Option<CacheEntry<T>>>>;
43type MapCache<K, V> = Arc<RwLock<HashMap<K, CacheEntry<V>>>>;
44
45#[derive(Clone)]
68pub struct ClientHandle(pub(crate) Arc<YahooClient>);
69pub struct TickerBuilder {
74 symbol: Arc<str>,
75 config: ClientConfig,
76 shared_client: Option<ClientHandle>,
77 injected_providers: Option<Arc<ProviderSet>>,
78 cache_ttl: Option<Duration>,
79 include_logo: bool,
80}
81
82impl TickerBuilder {
83 fn new(symbol: impl Into<String>) -> Self {
84 Self {
85 symbol: symbol.into().into(),
86 config: ClientConfig::default(),
87 shared_client: None,
88 injected_providers: None,
89 cache_ttl: None,
90 include_logo: false,
91 }
92 }
93 pub fn region(mut self, region: Region) -> Self {
95 self.config.lang = region.lang().to_string();
96 self.config.region = region.region().to_string();
97 self
98 }
99 pub fn lang(mut self, lang: impl Into<String>) -> Self {
101 self.config.lang = lang.into();
102 self
103 }
104 pub fn region_code(mut self, r: impl Into<String>) -> Self {
106 self.config.region = r.into();
107 self
108 }
109 pub fn timeout(mut self, t: Duration) -> Self {
111 self.config.timeout = t;
112 self
113 }
114 pub fn proxy(mut self, p: impl Into<String>) -> Self {
116 self.config.proxy = Some(p.into());
117 self
118 }
119 #[allow(dead_code)]
120 pub(crate) fn config(mut self, c: ClientConfig) -> Self {
121 self.config = c;
122 self
123 }
124 pub(crate) fn with_provider_set(mut self, set: Arc<ProviderSet>) -> Self {
126 self.injected_providers = Some(set);
127 self
128 }
129 pub fn client(mut self, handle: ClientHandle) -> Self {
137 self.shared_client = Some(handle);
138 self
139 }
140 pub fn cache(mut self, ttl: Duration) -> Self {
142 self.cache_ttl = Some(ttl);
143 self
144 }
145 pub fn logo(mut self) -> Self {
147 self.include_logo = true;
148 self
149 }
150
151 pub async fn build(self) -> Result<Ticker> {
153 #[cfg(feature = "translation")]
154 let translate_lang = {
155 let lang = crate::translation::Lang::parse(&self.config.lang)?;
156 (!lang.is_english()).then_some(lang)
157 };
158 let providers = if let Some(set) = self.injected_providers {
159 set
160 } else if let Some(handle) = self.shared_client {
161 let yahoo = YahooProvider::from_client(handle.0);
162 let client = yahoo.client_arc();
163 Arc::new(ProviderSet::new(
164 vec![Arc::new(yahoo) as Arc<dyn ProviderAdapter>],
165 Some(client),
166 Routes::new(Fetch::Sequential),
167 ))
168 } else {
169 Arc::new(
170 build_providers(
171 &[Provider::Yahoo],
172 &self.config,
173 Routes::new(Fetch::Sequential),
174 )
175 .await?,
176 )
177 };
178 Ok(Ticker {
179 symbol: self.symbol,
180 providers,
181 cache_ttl: self.cache_ttl,
182 include_logo: self.include_logo,
183 #[cfg(feature = "translation")]
184 translate_lang,
185 quote_cache: Default::default(),
186 quote_fetch: Arc::new(tokio::sync::Mutex::new(())),
187 chart_cache: Default::default(),
188 events_cache: Default::default(),
189 news_cache: Default::default(),
190 options_cache: Default::default(),
191 financials_cache: Default::default(),
192 #[cfg(feature = "indicators")]
193 indicators_cache: Default::default(),
194 edgar_submissions_cache: Default::default(),
195 edgar_facts_cache: Default::default(),
196 })
197 }
198}
199
200pub struct Ticker {
205 symbol: Arc<str>,
206 providers: Arc<ProviderSet>,
207 cache_ttl: Option<Duration>,
208 include_logo: bool,
209 #[cfg(feature = "translation")]
210 translate_lang: Option<crate::translation::Lang>,
211 quote_cache: Cache<QuoteSummaryResponse>,
212 quote_fetch: Arc<tokio::sync::Mutex<()>>,
213 chart_cache: MapCache<(Interval, TimeRange), Chart>,
214 events_cache: Cache<ChartEvents>,
215 news_cache: Cache<Vec<News>>,
216 options_cache: MapCache<Option<i64>, Options>,
217 financials_cache: MapCache<(StatementType, Frequency), FinancialStatement>,
218 #[cfg(feature = "indicators")]
219 indicators_cache: MapCache<(Interval, TimeRange), indicators::IndicatorsSummary>,
220 edgar_submissions_cache: Cache<EdgarSubmissions>,
221 edgar_facts_cache: Cache<CompanyFacts>,
222}
223
224impl Ticker {
225 pub async fn new(symbol: impl Into<String>) -> Result<Self> {
227 Self::builder(symbol).build().await
228 }
229 pub fn builder(symbol: impl Into<String>) -> TickerBuilder {
231 TickerBuilder::new(symbol)
232 }
233 pub fn symbol(&self) -> &str {
235 &self.symbol
236 }
237
238 pub fn client_handle(&self) -> ClientHandle {
249 ClientHandle(
250 self.providers
251 .first_yahoo()
252 .expect("client_handle requires a Yahoo session; use Providers::ticker() for multi-provider tickers"),
253 )
254 }
255
256 #[allow(dead_code)]
257 pub(crate) fn provider_set(&self) -> &Arc<ProviderSet> {
258 &self.providers
259 }
260
261 #[cfg(feature = "translation")]
264 pub(crate) async fn translate_response<T: crate::translation::Translatable>(
265 &self,
266 value: &mut T,
267 ) -> Result<()> {
268 if let Some(lang) = &self.translate_lang {
269 crate::translation::translate_with(value, lang).await?;
270 }
271 Ok(())
272 }
273
274 fn is_cache_fresh<T>(&self, entry: Option<&CacheEntry<T>>) -> bool {
275 CacheEntry::is_fresh_with_ttl(entry, self.cache_ttl)
276 }
277
278 fn is_shared_cache_fresh<T>(&self, entry: Option<&CacheEntry<T>>) -> bool {
282 match (self.cache_ttl, entry) {
283 (Some(ttl), Some(e)) => e.is_fresh(ttl),
284 _ => false,
285 }
286 }
287 fn cache_insert<K: Eq + std::hash::Hash, V>(
288 &self,
289 map: &mut HashMap<K, CacheEntry<V>>,
290 key: K,
291 value: V,
292 ) {
293 if let Some(ttl) = self.cache_ttl {
294 if map.len() >= EVICTION_THRESHOLD {
295 map.retain(|_, entry| entry.is_fresh(ttl));
296 }
297 map.insert(key, CacheEntry::new(value));
298 }
299 }
300
301 pub async fn quote<F>(&self) -> Result<Quote<F>>
303 where
304 F: Format,
305 Quote<Both>: Into<Quote<F>>,
306 {
307 let cache = self.ensure_quote().await?;
308 let summary = cache.as_ref().ok_or_else(|| {
309 FinanceError::ApiError("Quote summary cache was empty after fetch".to_string())
310 })?;
311 let (logo_url, company_logo_url) = if self.include_logo {
312 if let Ok(yahoo) = self.providers.first_yahoo() {
313 let logos = yahoo.get_logo_url(&self.symbol).await;
314 (logos.0, logos.1)
315 } else {
316 (None, None)
317 }
318 } else {
319 (None, None)
320 };
321 let quote = Quote::from_response(&summary.value, logo_url, company_logo_url);
322 #[cfg(feature = "translation")]
323 let quote = {
324 drop(cache);
325 let mut quote = quote;
326 self.translate_response(&mut quote).await?;
327 quote
328 };
329 Ok(quote.into())
330 }
331
332 fn chart_from_provider_data(
333 mut data: Chart,
334 interval: Option<Interval>,
335 range: Option<TimeRange>,
336 ) -> Chart {
337 data.interval = interval;
338 data.range = range;
339 data
340 }
341
342 pub async fn chart(&self, interval: Interval, range: TimeRange) -> Result<Chart> {
344 {
345 let cache = self.chart_cache.read().await;
346 if let Some(entry) = cache.get(&(interval, range))
347 && self.is_cache_fresh(Some(entry))
348 {
349 return Ok(entry.value.clone());
350 }
351 }
352 let sym = self.symbol.clone();
353 let data = self
354 .providers
355 .fetch(Capability::CHART, move |p| {
356 let sym = sym.clone();
357 let p = p.clone();
358 async move { p.fetch_chart(&sym, interval, range).await }
359 })
360 .await?;
361 let chart = Self::chart_from_provider_data(data, Some(interval), Some(range));
362 if self.cache_ttl.is_some() {
363 let mut cache = self.chart_cache.write().await;
364 self.cache_insert(&mut cache, (interval, range), chart.clone());
365 }
366 Ok(chart)
367 }
368
369 pub async fn chart_range(&self, interval: Interval, start: i64, end: i64) -> Result<Chart> {
371 if start >= end {
372 return Err(FinanceError::InvalidParameter {
373 param: "end".into(),
374 reason: format!("end ({end}) must be > start ({start})"),
375 });
376 }
377 let sym = self.symbol.clone();
378 let data = self
379 .providers
380 .fetch(Capability::CHART, move |p| {
381 let sym = sym.clone();
382 let p = p.clone();
383 async move { p.fetch_chart_range(&sym, interval, start, end).await }
384 })
385 .await?;
386 Ok(Self::chart_from_provider_data(data, Some(interval), None))
387 }
388
389 async fn ensure_events(&self) -> Result<()> {
390 {
391 let cache = self.events_cache.read().await;
392 if self.is_shared_cache_fresh(cache.as_ref()) {
393 return Ok(());
394 }
395 }
396 let sym = self.symbol.clone();
397 let events = self
398 .providers
399 .fetch(Capability::CORPORATE, move |p| {
400 let sym = sym.clone();
401 let p = p.clone();
402 async move { p.fetch_events(&sym).await }
403 })
404 .await?;
405 let mut cache = self.events_cache.write().await;
406 *cache = Some(CacheEntry::new(events));
407 Ok(())
408 }
409
410 pub async fn dividends(&self, range: TimeRange) -> Result<Vec<Dividend>> {
412 self.ensure_events().await?;
413 let cache = self.events_cache.read().await;
414 let all = cache
415 .as_ref()
416 .map(|e| e.value.to_dividends())
417 .unwrap_or_default();
418 Ok(filter_by_range(all, range))
419 }
420 pub async fn dividend_analytics(&self, range: TimeRange) -> Result<DividendAnalytics> {
422 let divs = self.dividends(range).await?;
423 Ok(DividendAnalytics::from_dividends(&divs))
424 }
425 pub async fn splits(&self, range: TimeRange) -> Result<Vec<Split>> {
427 self.ensure_events().await?;
428 let cache = self.events_cache.read().await;
429 let all = cache
430 .as_ref()
431 .map(|e| e.value.to_splits())
432 .unwrap_or_default();
433 Ok(filter_by_range(all, range))
434 }
435 pub async fn capital_gains(&self, range: TimeRange) -> Result<Vec<CapitalGain>> {
437 self.ensure_events().await?;
438 let cache = self.events_cache.read().await;
439 let all = cache
440 .as_ref()
441 .map(|e| e.value.to_capital_gains())
442 .unwrap_or_default();
443 Ok(filter_by_range(all, range))
444 }
445
446 pub async fn recommendations(&self, limit: u32) -> Result<Recommendation> {
448 if limit == 0 {
449 return Err(FinanceError::InvalidParameter {
450 param: "limit".into(),
451 reason: "limit must be > 0".into(),
452 });
453 }
454 let sym = self.symbol.clone();
455 let (provider_id, items) = self
456 .providers
457 .fetch(Capability::CORPORATE, move |p| {
458 let sym = sym.clone();
459 let p = p.clone();
460 async move {
461 let r = p.fetch_similar_symbols(&sym, limit).await?;
462 Ok((p.id(), r))
463 }
464 })
465 .await?;
466 Ok(recommendation_from_similar(
467 self.symbol.to_string(),
468 Some(provider_id),
469 items,
470 Some(limit),
471 ))
472 }
473
474 pub async fn news(&self) -> Result<Vec<News>> {
476 {
477 let cache = self.news_cache.read().await;
478 if let Some(e) = cache.as_ref()
479 && self.is_cache_fresh(Some(e))
480 {
481 return Ok(e.value.clone());
482 }
483 }
484 let sym = self.symbol.clone();
485 let data = self
486 .providers
487 .fetch(Capability::CORPORATE, move |p| {
488 let sym = sym.clone();
489 let p = p.clone();
490 async move { p.fetch_news(&sym).await }
491 })
492 .await?;
493 let news = data;
494 #[cfg(feature = "sentiment")]
496 let news = {
497 let mut news = news;
498 for article in news.iter_mut() {
499 article.sentiment = Some(crate::models::sentiment::analyze(&article.title));
500 }
501 news
502 };
503 #[cfg(feature = "translation")]
504 let news = {
505 let mut news = news;
506 self.translate_response(&mut news).await?;
507 news
508 };
509 if self.cache_ttl.is_some() {
510 let mut c = self.news_cache.write().await;
511 *c = Some(CacheEntry::new(news.clone()));
512 }
513 Ok(news)
514 }
515
516 #[cfg(feature = "sentiment")]
523 pub async fn news_sentiment(&self) -> Result<crate::models::sentiment::Sentiment> {
524 let news = self.news().await?;
525 let scores: Vec<f64> = news
526 .iter()
527 .filter_map(|n| n.sentiment.as_ref().map(|s| s.score))
528 .collect();
529 Ok(crate::models::sentiment::aggregate(&scores)
530 .unwrap_or_else(crate::models::sentiment::Sentiment::neutral))
531 }
532
533 pub async fn options(&self, date: Option<i64>) -> Result<Options> {
535 {
536 let cache = self.options_cache.read().await;
537 if let Some(e) = cache.get(&date)
538 && self.is_cache_fresh(Some(e))
539 {
540 return Ok(e.value.clone());
541 }
542 }
543 let sym = self.symbol.clone();
544 let opts = self
545 .providers
546 .fetch(Capability::OPTIONS, move |p| {
547 let sym = sym.clone();
548 let p = p.clone();
549 async move { p.fetch_options(&sym, date).await }
550 })
551 .await?;
552 if self.cache_ttl.is_some() {
553 let mut c = self.options_cache.write().await;
554 self.cache_insert(&mut c, date, opts.clone());
555 }
556 Ok(opts)
557 }
558
559 pub async fn financials(
561 &self,
562 stmt_type: StatementType,
563 frequency: Frequency,
564 ) -> Result<FinancialStatement> {
565 let key = (stmt_type, frequency);
566 {
567 let cache = self.financials_cache.read().await;
568 if let Some(e) = cache.get(&key)
569 && self.is_cache_fresh(Some(e))
570 {
571 return Ok(e.value.clone());
572 }
573 }
574 let sym = self.symbol.clone();
575 let stmt = self
576 .providers
577 .fetch(Capability::FUNDAMENTALS, move |p| {
578 let sym = sym.clone();
579 let p = p.clone();
580 async move { p.fetch_financials(&sym, stmt_type, frequency).await }
581 })
582 .await?;
583 if self.cache_ttl.is_some() {
584 let mut c = self.financials_cache.write().await;
585 self.cache_insert(&mut c, key, stmt.clone());
586 }
587 Ok(stmt)
588 }
589
590 #[cfg(feature = "indicators")]
591 pub async fn indicators(
593 &self,
594 interval: Interval,
595 range: TimeRange,
596 ) -> Result<indicators::IndicatorsSummary> {
597 {
598 let cache = self.indicators_cache.read().await;
599 if let Some(e) = cache.get(&(interval, range))
600 && self.is_cache_fresh(Some(e))
601 {
602 return Ok(e.value.clone());
603 }
604 }
605 let chart = self.chart(interval, range).await?;
606 let ind = indicators::summary::calculate_indicators(&chart.candles);
607 if self.cache_ttl.is_some() {
608 let mut c = self.indicators_cache.write().await;
609 self.cache_insert(&mut c, (interval, range), ind.clone());
610 }
611 Ok(ind)
612 }
613
614 pub async fn edgar_submissions(&self) -> Result<EdgarSubmissions> {
620 {
621 let cache = self.edgar_submissions_cache.read().await;
622 if let Some(e) = cache.as_ref()
623 && self.is_cache_fresh(Some(e))
624 {
625 return Ok(e.value.clone());
626 }
627 }
628 let cik = edgar::resolve_cik(&self.symbol).await?;
629 let subs = edgar::submissions(cik).await?;
630 if self.cache_ttl.is_some() {
631 let mut c = self.edgar_submissions_cache.write().await;
632 *c = Some(CacheEntry::new(subs.clone()));
633 }
634 Ok(subs)
635 }
636
637 pub async fn edgar_company_facts(&self) -> Result<CompanyFacts> {
642 {
643 let cache = self.edgar_facts_cache.read().await;
644 if let Some(e) = cache.as_ref()
645 && self.is_cache_fresh(Some(e))
646 {
647 return Ok(e.value.clone());
648 }
649 }
650 let cik = edgar::resolve_cik(&self.symbol).await?;
651 let facts = edgar::company_facts(cik).await?;
652 if self.cache_ttl.is_some() {
653 let mut c = self.edgar_facts_cache.write().await;
654 *c = Some(CacheEntry::new(facts.clone()));
655 }
656 Ok(facts)
657 }
658
659 pub async fn filings(&self) -> Result<ProviderFilings> {
668 let symbol = self.symbol.clone();
669 self.providers
670 .fetch(Capability::FILINGS, move |p| {
671 let symbol = symbol.clone();
672 let p = p.clone();
673 async move { p.fetch_filings(&symbol).await }
674 })
675 .await
676 }
677
678 #[cfg(feature = "indicators")]
679 pub async fn indicator(
681 &self,
682 indicator: indicators::Indicator,
683 interval: Interval,
684 range: TimeRange,
685 ) -> Result<indicators::IndicatorResult> {
686 let chart = self.chart(interval, range).await?;
687 Ok(indicators::compute_indicator(indicator, &chart)?)
688 }
689
690 #[cfg(feature = "backtesting")]
691 pub async fn backtest<S: backtesting::Strategy>(
693 &self,
694 strategy: S,
695 interval: Interval,
696 range: TimeRange,
697 config: Option<backtesting::BacktestConfig>,
698 ) -> backtesting::Result<backtesting::BacktestResult> {
699 let config = config.unwrap_or_default();
700 config.validate()?;
701 let chart = self
702 .chart(interval, range)
703 .await
704 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
705 let dividends = self.dividends(range).await.unwrap_or_default();
706 backtesting::BacktestEngine::new(config).run_with_dividends(
707 &self.symbol,
708 &chart.candles,
709 strategy,
710 ÷nds,
711 )
712 }
713
714 #[cfg(feature = "backtesting")]
715 pub async fn backtest_with_benchmark<S: backtesting::Strategy>(
717 &self,
718 strategy: S,
719 interval: Interval,
720 range: TimeRange,
721 config: Option<backtesting::BacktestConfig>,
722 benchmark: &str,
723 ) -> backtesting::Result<backtesting::BacktestResult> {
724 let config = config.unwrap_or_default();
725 config.validate()?;
726 let bench_ticker = Ticker::new(benchmark)
727 .await
728 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
729 let (chart, bench_chart) = tokio::try_join!(
730 self.chart(interval, range),
731 bench_ticker.chart(interval, range)
732 )
733 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
734 let dividends = self.dividends(range).await.unwrap_or_default();
735 backtesting::BacktestEngine::new(config).run_with_benchmark(
736 &self.symbol,
737 &chart.candles,
738 strategy,
739 ÷nds,
740 benchmark,
741 &bench_chart.candles,
742 )
743 }
744
745 #[cfg(feature = "risk")]
746 pub async fn risk(
748 &self,
749 interval: Interval,
750 range: TimeRange,
751 benchmark: Option<&str>,
752 ) -> Result<risk::RiskSummary> {
753 let chart = self.chart(interval, range).await?;
754 let bench_returns = if let Some(sym) = benchmark {
755 let bt = Ticker::new(sym).await?;
756 Some(risk::candles_to_returns(
757 &bt.chart(interval, range).await?.candles,
758 ))
759 } else {
760 None
761 };
762 Ok(risk::compute_risk_summary(
763 &chart.candles,
764 bench_returns.as_deref(),
765 ))
766 }
767
768 pub async fn calendar(
780 &self,
781 range: TimeRange,
782 ) -> Result<Vec<crate::models::calendar::CalendarEvent>> {
783 let now = chrono::Utc::now().timestamp();
784 let window = (now, now + range.approx_duration_secs());
785
786 #[cfg(feature = "fred")]
789 let (calendar_events, options, releases) = tokio::join!(
790 self.calendar_events(),
791 self.options(None),
792 crate::adapters::fred::release_dates(),
793 );
794 #[cfg(not(feature = "fred"))]
795 let (calendar_events, options) = tokio::join!(self.calendar_events(), self.options(None));
796
797 let calendar_events = calendar_events?;
798 let options = options.ok();
799
800 let mut events = crate::models::calendar::build_symbol_events(
801 &self.symbol,
802 calendar_events.as_ref(),
803 options.as_ref(),
804 window,
805 );
806
807 #[cfg(feature = "fred")]
808 if let Ok(releases) = releases {
809 events.extend(crate::models::calendar::build_economic_events(
810 releases, window,
811 ));
812 }
813
814 crate::models::calendar::sort_events(&mut events);
815 Ok(events)
816 }
817
818 async fn ensure_quote(
819 &self,
820 ) -> Result<tokio::sync::RwLockReadGuard<'_, Option<CacheEntry<QuoteSummaryResponse>>>> {
821 {
822 let cache = self.quote_cache.read().await;
823 if self.is_shared_cache_fresh(cache.as_ref()) {
824 return Ok(cache);
825 }
826 }
827 let _guard = self.quote_fetch.lock().await;
828 {
829 let cache = self.quote_cache.read().await;
830 if self.is_shared_cache_fresh(cache.as_ref()) {
831 return Ok(cache);
832 }
833 }
834 let sym = self.symbol.clone();
835 let summary = self
836 .providers
837 .fetch(Capability::QUOTE, move |p| {
838 let sym = sym.clone();
839 let p = p.clone();
840 async move { p.fetch_quote(&sym).await }
841 })
842 .await?;
843 {
844 let mut cache = self.quote_cache.write().await;
845 *cache = Some(CacheEntry::new(summary));
846 }
847 Ok(self.quote_cache.read().await)
848 }
849}
850
851super::macros::define_quote_accessors! {
852 price -> Price, price,
853 summary_detail -> SummaryDetail, summary_detail,
854 financial_data -> FinancialData, financial_data,
855 key_stats -> DefaultKeyStatistics, default_key_statistics,
856 asset_profile -> AssetProfile, asset_profile,
857 calendar_events -> CalendarEvents, calendar_events,
858 earnings -> Earnings, earnings,
859 earnings_trend -> EarningsTrend, earnings_trend,
860 earnings_history -> EarningsHistory, earnings_history,
861 recommendation_trend -> RecommendationTrend, recommendation_trend,
862 insider_holders -> InsiderHolders, insider_holders,
863 insider_transactions -> InsiderTransactions, insider_transactions,
864 institution_ownership -> InstitutionOwnership, institution_ownership,
865 fund_ownership -> FundOwnership, fund_ownership,
866 major_holders -> MajorHoldersBreakdown, major_holders_breakdown,
867 share_purchase_activity -> NetSharePurchaseActivity, net_share_purchase_activity,
868 quote_type -> QuoteTypeData, quote_type,
869 summary_profile -> SummaryProfile, summary_profile,
870 sec_filings -> SecFilings, sec_filings,
871 grading_history -> UpgradeDowngradeHistory, upgrade_downgrade_history,
872 fund_performance -> FundPerformance, fund_performance,
873 fund_profile -> FundProfile, fund_profile,
874 top_holdings -> TopHoldings, top_holdings,
875 index_trend -> IndexTrend, index_trend,
876 industry_trend -> IndustryTrend, industry_trend,
877 sector_trend -> SectorTrend, sector_trend,
878 equity_performance -> EquityPerformance, equity_performance,
879}