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 let provider = Provider::from_id_str(p.id()).ok_or_else(|| {
463 FinanceError::InternalError(format!("unknown provider id: {}", p.id()))
464 })?;
465 Ok((provider, r))
466 }
467 })
468 .await?;
469 Ok(recommendation_from_similar(
470 self.symbol.to_string(),
471 Some(provider_id),
472 items,
473 Some(limit),
474 ))
475 }
476
477 pub async fn news(&self) -> Result<Vec<News>> {
479 {
480 let cache = self.news_cache.read().await;
481 if let Some(e) = cache.as_ref()
482 && self.is_cache_fresh(Some(e))
483 {
484 return Ok(e.value.clone());
485 }
486 }
487 let sym = self.symbol.clone();
488 let data = self
489 .providers
490 .fetch(Capability::CORPORATE, move |p| {
491 let sym = sym.clone();
492 let p = p.clone();
493 async move { p.fetch_news(&sym).await }
494 })
495 .await?;
496 let news = data;
497 #[cfg(feature = "sentiment")]
499 let news = {
500 let mut news = news;
501 for article in news.iter_mut() {
502 article.sentiment = Some(crate::models::sentiment::analyze(&article.title));
503 }
504 news
505 };
506 #[cfg(feature = "translation")]
507 let news = {
508 let mut news = news;
509 self.translate_response(&mut news).await?;
510 news
511 };
512 if self.cache_ttl.is_some() {
513 let mut c = self.news_cache.write().await;
514 *c = Some(CacheEntry::new(news.clone()));
515 }
516 Ok(news)
517 }
518
519 #[cfg(feature = "sentiment")]
526 pub async fn news_sentiment(&self) -> Result<crate::models::sentiment::Sentiment> {
527 let news = self.news().await?;
528 let scores: Vec<f64> = news
529 .iter()
530 .filter_map(|n| n.sentiment.as_ref().map(|s| s.score))
531 .collect();
532 Ok(crate::models::sentiment::aggregate(&scores)
533 .unwrap_or_else(crate::models::sentiment::Sentiment::neutral))
534 }
535
536 pub async fn options(&self, date: Option<i64>) -> Result<Options> {
538 {
539 let cache = self.options_cache.read().await;
540 if let Some(e) = cache.get(&date)
541 && self.is_cache_fresh(Some(e))
542 {
543 return Ok(e.value.clone());
544 }
545 }
546 let sym = self.symbol.clone();
547 let opts = self
548 .providers
549 .fetch(Capability::OPTIONS, move |p| {
550 let sym = sym.clone();
551 let p = p.clone();
552 async move { p.fetch_options(&sym, date).await }
553 })
554 .await?;
555 if self.cache_ttl.is_some() {
556 let mut c = self.options_cache.write().await;
557 self.cache_insert(&mut c, date, opts.clone());
558 }
559 Ok(opts)
560 }
561
562 pub async fn financials(
564 &self,
565 stmt_type: StatementType,
566 frequency: Frequency,
567 ) -> Result<FinancialStatement> {
568 let key = (stmt_type, frequency);
569 {
570 let cache = self.financials_cache.read().await;
571 if let Some(e) = cache.get(&key)
572 && self.is_cache_fresh(Some(e))
573 {
574 return Ok(e.value.clone());
575 }
576 }
577 let sym = self.symbol.clone();
578 let stmt = self
579 .providers
580 .fetch(Capability::FUNDAMENTALS, move |p| {
581 let sym = sym.clone();
582 let p = p.clone();
583 async move { p.fetch_financials(&sym, stmt_type, frequency).await }
584 })
585 .await?;
586 if self.cache_ttl.is_some() {
587 let mut c = self.financials_cache.write().await;
588 self.cache_insert(&mut c, key, stmt.clone());
589 }
590 Ok(stmt)
591 }
592
593 #[cfg(feature = "indicators")]
594 pub async fn indicators(
596 &self,
597 interval: Interval,
598 range: TimeRange,
599 ) -> Result<indicators::IndicatorsSummary> {
600 {
601 let cache = self.indicators_cache.read().await;
602 if let Some(e) = cache.get(&(interval, range))
603 && self.is_cache_fresh(Some(e))
604 {
605 return Ok(e.value.clone());
606 }
607 }
608 let chart = self.chart(interval, range).await?;
609 let ind = indicators::summary::calculate_indicators(&chart.candles);
610 if self.cache_ttl.is_some() {
611 let mut c = self.indicators_cache.write().await;
612 self.cache_insert(&mut c, (interval, range), ind.clone());
613 }
614 Ok(ind)
615 }
616
617 pub async fn edgar_submissions(&self) -> Result<EdgarSubmissions> {
623 {
624 let cache = self.edgar_submissions_cache.read().await;
625 if let Some(e) = cache.as_ref()
626 && self.is_cache_fresh(Some(e))
627 {
628 return Ok(e.value.clone());
629 }
630 }
631 let cik = edgar::resolve_cik(&self.symbol).await?;
632 let subs = edgar::submissions(cik).await?;
633 if self.cache_ttl.is_some() {
634 let mut c = self.edgar_submissions_cache.write().await;
635 *c = Some(CacheEntry::new(subs.clone()));
636 }
637 Ok(subs)
638 }
639
640 pub async fn edgar_company_facts(&self) -> Result<CompanyFacts> {
645 {
646 let cache = self.edgar_facts_cache.read().await;
647 if let Some(e) = cache.as_ref()
648 && self.is_cache_fresh(Some(e))
649 {
650 return Ok(e.value.clone());
651 }
652 }
653 let cik = edgar::resolve_cik(&self.symbol).await?;
654 let facts = edgar::company_facts(cik).await?;
655 if self.cache_ttl.is_some() {
656 let mut c = self.edgar_facts_cache.write().await;
657 *c = Some(CacheEntry::new(facts.clone()));
658 }
659 Ok(facts)
660 }
661
662 pub async fn filings(&self) -> Result<ProviderFilings> {
671 let symbol = self.symbol.clone();
672 self.providers
673 .fetch(Capability::FILINGS, move |p| {
674 let symbol = symbol.clone();
675 let p = p.clone();
676 async move { p.fetch_filings(&symbol).await }
677 })
678 .await
679 }
680
681 #[cfg(feature = "indicators")]
682 pub async fn indicator(
684 &self,
685 indicator: indicators::Indicator,
686 interval: Interval,
687 range: TimeRange,
688 ) -> Result<indicators::IndicatorResult> {
689 let chart = self.chart(interval, range).await?;
690 let o = chart.open_prices();
691 let h = chart.high_prices();
692 let l = chart.low_prices();
693 let c = chart.close_prices();
694 let v = chart.volumes();
695 use indicators::{Indicator, IndicatorResult};
696 Ok(match indicator {
697 Indicator::Sma(p) => IndicatorResult::Series(chart.sma(p)),
698 Indicator::Ema(p) => IndicatorResult::Series(chart.ema(p)),
699 Indicator::Rsi(p) => IndicatorResult::Series(chart.rsi(p)?),
700 Indicator::Macd { fast, slow, signal } => {
701 IndicatorResult::Macd(chart.macd(fast, slow, signal)?)
702 }
703 Indicator::Bollinger { period, std_dev } => {
704 IndicatorResult::Bollinger(chart.bollinger_bands(period, std_dev)?)
705 }
706 Indicator::Atr(p) => IndicatorResult::Series(chart.atr(p)?),
707 Indicator::Vwap => IndicatorResult::Series(crate::indicators::vwap(&h, &l, &c, &v)?),
708 Indicator::Wma(p) => IndicatorResult::Series(crate::indicators::wma(&c, p)?),
709 Indicator::Obv => IndicatorResult::Series(crate::indicators::obv(&c, &v)?),
710 Indicator::Dema(p) => IndicatorResult::Series(crate::indicators::dema(&c, p)?),
711 Indicator::Tema(p) => IndicatorResult::Series(crate::indicators::tema(&c, p)?),
712 Indicator::Hma(p) => IndicatorResult::Series(crate::indicators::hma(&c, p)?),
713 Indicator::Vwma(p) => IndicatorResult::Series(crate::indicators::vwma(&c, &v, p)?),
714 Indicator::Alma {
715 period,
716 offset,
717 sigma,
718 } => IndicatorResult::Series(crate::indicators::alma(&c, period, offset, sigma)?),
719 Indicator::McginleyDynamic(p) => {
720 IndicatorResult::Series(crate::indicators::mcginley_dynamic(&c, p)?)
721 }
722 Indicator::Stochastic {
723 k_period,
724 k_slow,
725 d_period,
726 } => IndicatorResult::Stochastic(crate::indicators::stochastic(
727 &h, &l, &c, k_period, k_slow, d_period,
728 )?),
729 Indicator::StochasticRsi {
730 rsi_period,
731 stoch_period,
732 k_period,
733 d_period,
734 } => IndicatorResult::Stochastic(crate::indicators::stochastic_rsi(
735 &c,
736 rsi_period,
737 stoch_period,
738 k_period,
739 d_period,
740 )?),
741 Indicator::Cci(p) => IndicatorResult::Series(crate::indicators::cci(&h, &l, &c, p)?),
742 Indicator::WilliamsR(p) => {
743 IndicatorResult::Series(crate::indicators::williams_r(&h, &l, &c, p)?)
744 }
745 Indicator::Roc(p) => IndicatorResult::Series(crate::indicators::roc(&c, p)?),
746 Indicator::Momentum(p) => IndicatorResult::Series(crate::indicators::momentum(&c, p)?),
747 Indicator::Cmo(p) => IndicatorResult::Series(crate::indicators::cmo(&c, p)?),
748 Indicator::AwesomeOscillator { fast, slow } => {
749 IndicatorResult::Series(crate::indicators::awesome_oscillator(&h, &l, fast, slow)?)
750 }
751 Indicator::CoppockCurve {
752 long_roc,
753 short_roc,
754 wma_period,
755 } => IndicatorResult::Series(crate::indicators::coppock_curve(
756 &c, long_roc, short_roc, wma_period,
757 )?),
758 Indicator::Adx(p) => IndicatorResult::Series(crate::indicators::adx(&h, &l, &c, p)?),
759 Indicator::Aroon(p) => IndicatorResult::Aroon(crate::indicators::aroon(&h, &l, p)?),
760 Indicator::Supertrend { period, multiplier } => IndicatorResult::SuperTrend(
761 crate::indicators::supertrend(&h, &l, &c, period, multiplier)?,
762 ),
763 Indicator::Ichimoku {
764 conversion,
765 base,
766 lagging,
767 displacement,
768 } => IndicatorResult::Ichimoku(crate::indicators::ichimoku(
769 &h,
770 &l,
771 &c,
772 conversion,
773 base,
774 lagging,
775 displacement,
776 )?),
777 Indicator::ParabolicSar { step, max } => {
778 IndicatorResult::Series(crate::indicators::parabolic_sar(&h, &l, &c, step, max)?)
779 }
780 Indicator::BullBearPower(p) => {
781 IndicatorResult::BullBearPower(crate::indicators::bull_bear_power(&h, &l, &c, p)?)
782 }
783 Indicator::ElderRay(p) => {
784 IndicatorResult::ElderRay(crate::indicators::elder_ray(&h, &l, &c, p)?)
785 }
786 Indicator::KeltnerChannels {
787 period,
788 multiplier,
789 atr_period,
790 } => IndicatorResult::Keltner(crate::indicators::keltner_channels(
791 &h, &l, &c, period, atr_period, multiplier,
792 )?),
793 Indicator::DonchianChannels(p) => {
794 IndicatorResult::Donchian(crate::indicators::donchian_channels(&h, &l, p)?)
795 }
796 Indicator::TrueRange => {
797 IndicatorResult::Series(crate::indicators::true_range(&h, &l, &c)?)
798 }
799 Indicator::ChoppinessIndex(p) => {
800 IndicatorResult::Series(crate::indicators::choppiness_index(&h, &l, &c, p)?)
801 }
802 Indicator::Mfi(p) => {
803 IndicatorResult::Series(crate::indicators::mfi(&h, &l, &c, &v, p)?)
804 }
805 Indicator::Cmf(p) => {
806 IndicatorResult::Series(crate::indicators::cmf(&h, &l, &c, &v, p)?)
807 }
808 Indicator::ChaikinOscillator => {
809 IndicatorResult::Series(crate::indicators::chaikin_oscillator(&h, &l, &c, &v)?)
810 }
811 Indicator::AccumulationDistribution => IndicatorResult::Series(
812 crate::indicators::accumulation_distribution(&h, &l, &c, &v)?,
813 ),
814 Indicator::BalanceOfPower(p) => {
815 IndicatorResult::Series(crate::indicators::balance_of_power(&o, &h, &l, &c, p)?)
816 }
817 })
818 }
819
820 #[cfg(feature = "backtesting")]
821 pub async fn backtest<S: backtesting::Strategy>(
823 &self,
824 strategy: S,
825 interval: Interval,
826 range: TimeRange,
827 config: Option<backtesting::BacktestConfig>,
828 ) -> backtesting::Result<backtesting::BacktestResult> {
829 let config = config.unwrap_or_default();
830 config.validate()?;
831 let chart = self
832 .chart(interval, range)
833 .await
834 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
835 let dividends = self.dividends(range).await.unwrap_or_default();
836 backtesting::BacktestEngine::new(config).run_with_dividends(
837 &self.symbol,
838 &chart.candles,
839 strategy,
840 ÷nds,
841 )
842 }
843
844 #[cfg(feature = "backtesting")]
845 pub async fn backtest_with_benchmark<S: backtesting::Strategy>(
847 &self,
848 strategy: S,
849 interval: Interval,
850 range: TimeRange,
851 config: Option<backtesting::BacktestConfig>,
852 benchmark: &str,
853 ) -> backtesting::Result<backtesting::BacktestResult> {
854 let config = config.unwrap_or_default();
855 config.validate()?;
856 let bench_ticker = Ticker::new(benchmark)
857 .await
858 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
859 let (chart, bench_chart) = tokio::try_join!(
860 self.chart(interval, range),
861 bench_ticker.chart(interval, range)
862 )
863 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
864 let dividends = self.dividends(range).await.unwrap_or_default();
865 backtesting::BacktestEngine::new(config).run_with_benchmark(
866 &self.symbol,
867 &chart.candles,
868 strategy,
869 ÷nds,
870 benchmark,
871 &bench_chart.candles,
872 )
873 }
874
875 #[cfg(feature = "risk")]
876 pub async fn risk(
878 &self,
879 interval: Interval,
880 range: TimeRange,
881 benchmark: Option<&str>,
882 ) -> Result<risk::RiskSummary> {
883 let chart = self.chart(interval, range).await?;
884 let bench_returns = if let Some(sym) = benchmark {
885 let bt = Ticker::new(sym).await?;
886 Some(risk::candles_to_returns(
887 &bt.chart(interval, range).await?.candles,
888 ))
889 } else {
890 None
891 };
892 Ok(risk::compute_risk_summary(
893 &chart.candles,
894 bench_returns.as_deref(),
895 ))
896 }
897
898 async fn ensure_quote(
899 &self,
900 ) -> Result<tokio::sync::RwLockReadGuard<'_, Option<CacheEntry<QuoteSummaryResponse>>>> {
901 {
902 let cache = self.quote_cache.read().await;
903 if self.is_shared_cache_fresh(cache.as_ref()) {
904 return Ok(cache);
905 }
906 }
907 let _guard = self.quote_fetch.lock().await;
908 {
909 let cache = self.quote_cache.read().await;
910 if self.is_shared_cache_fresh(cache.as_ref()) {
911 return Ok(cache);
912 }
913 }
914 let sym = self.symbol.clone();
915 let summary = self
916 .providers
917 .fetch(Capability::QUOTE, move |p| {
918 let sym = sym.clone();
919 let p = p.clone();
920 async move { p.fetch_quote(&sym).await }
921 })
922 .await?;
923 {
924 let mut cache = self.quote_cache.write().await;
925 *cache = Some(CacheEntry::new(summary));
926 }
927 Ok(self.quote_cache.read().await)
928 }
929}
930
931super::macros::define_quote_accessors! {
932 price -> Price, price,
933 summary_detail -> SummaryDetail, summary_detail,
934 financial_data -> FinancialData, financial_data,
935 key_stats -> DefaultKeyStatistics, default_key_statistics,
936 asset_profile -> AssetProfile, asset_profile,
937 calendar_events -> CalendarEvents, calendar_events,
938 earnings -> Earnings, earnings,
939 earnings_trend -> EarningsTrend, earnings_trend,
940 earnings_history -> EarningsHistory, earnings_history,
941 recommendation_trend -> RecommendationTrend, recommendation_trend,
942 insider_holders -> InsiderHolders, insider_holders,
943 insider_transactions -> InsiderTransactions, insider_transactions,
944 institution_ownership -> InstitutionOwnership, institution_ownership,
945 fund_ownership -> FundOwnership, fund_ownership,
946 major_holders -> MajorHoldersBreakdown, major_holders_breakdown,
947 share_purchase_activity -> NetSharePurchaseActivity, net_share_purchase_activity,
948 quote_type -> QuoteTypeData, quote_type,
949 summary_profile -> SummaryProfile, summary_profile,
950 sec_filings -> SecFilings, sec_filings,
951 grading_history -> UpgradeDowngradeHistory, upgrade_downgrade_history,
952 fund_performance -> FundPerformance, fund_performance,
953 fund_profile -> FundProfile, fund_profile,
954 top_holdings -> TopHoldings, top_holdings,
955 index_trend -> IndexTrend, index_trend,
956 industry_trend -> IndustryTrend, industry_trend,
957 sector_trend -> SectorTrend, sector_trend,
958 equity_performance -> EquityPerformance, equity_performance,
959}