1use crate::adapters::yahoo::client::{ClientConfig, YahooClient};
4#[cfg(feature = "backtesting")]
5use crate::backtesting;
6use crate::constants::{Frequency, Interval, Region, StatementType, TimeRange, ValueFormat};
7use crate::edgar;
8use crate::error::{FinanceError, Result};
9#[cfg(any(feature = "backtesting", feature = "indicators"))]
10use crate::indicators;
11use crate::models::chart::events::ChartEvents;
12use crate::models::chart::{CapitalGain, Chart, Dividend, DividendAnalytics, Split};
13use crate::models::corporate::news::News;
14use crate::models::corporate::recommendation::Recommendation;
15use crate::models::filings::{CompanyFacts, EdgarSubmissions, ProviderFilings};
16use crate::models::fundamentals::FinancialStatement;
17use crate::models::options::Options;
18use crate::models::quote::{
19 AssetProfile, CalendarEvents, DefaultKeyStatistics, Earnings, EarningsHistory, EarningsTrend,
20 EquityPerformance, FinancialData, FundOwnership, FundPerformance, FundProfile, IndexTrend,
21 IndustryTrend, InsiderHolders, InsiderTransactions, InstitutionOwnership,
22 MajorHoldersBreakdown, NetSharePurchaseActivity, Price, Quote, QuoteSummaryResponse,
23 QuoteTypeData, RecommendationTrend, SecFilings, SectorTrend, SummaryDetail, SummaryProfile,
24 TopHoldings, UpgradeDowngradeHistory,
25};
26use crate::providers::types::recommendation_from_similar;
27use crate::providers::yahoo::YahooProvider;
28use crate::providers::{
29 Capability, Fetch, Provider, ProviderAdapter, ProviderSet, Routes, build_providers,
30};
31#[cfg(feature = "risk")]
32use crate::risk;
33use crate::utils::{CacheEntry, EVICTION_THRESHOLD, filter_by_range};
34use std::collections::HashMap;
35use std::sync::Arc;
36use std::time::Duration;
37use tokio::sync::RwLock;
38
39type Cache<T> = Arc<RwLock<Option<CacheEntry<T>>>>;
40type MapCache<K, V> = Arc<RwLock<HashMap<K, CacheEntry<V>>>>;
41
42#[derive(Clone)]
65pub struct ClientHandle(pub(crate) Arc<YahooClient>);
66pub struct TickerBuilder {
71 symbol: Arc<str>,
72 config: ClientConfig,
73 shared_client: Option<ClientHandle>,
74 injected_providers: Option<Arc<ProviderSet>>,
75 cache_ttl: Option<Duration>,
76 include_logo: bool,
77 value_format: ValueFormat,
78}
79
80impl TickerBuilder {
81 fn new(symbol: impl Into<String>) -> Self {
82 Self {
83 symbol: symbol.into().into(),
84 config: ClientConfig::default(),
85 shared_client: None,
86 injected_providers: None,
87 cache_ttl: None,
88 include_logo: false,
89 value_format: ValueFormat::default(),
90 }
91 }
92 pub fn region(mut self, region: Region) -> Self {
94 self.config.lang = region.lang().to_string();
95 self.config.region = region.region().to_string();
96 self
97 }
98 pub fn lang(mut self, lang: impl Into<String>) -> Self {
100 self.config.lang = lang.into();
101 self
102 }
103 pub fn region_code(mut self, r: impl Into<String>) -> Self {
105 self.config.region = r.into();
106 self
107 }
108 pub fn timeout(mut self, t: Duration) -> Self {
110 self.config.timeout = t;
111 self
112 }
113 pub fn proxy(mut self, p: impl Into<String>) -> Self {
115 self.config.proxy = Some(p.into());
116 self
117 }
118 #[allow(dead_code)]
119 pub(crate) fn config(mut self, c: ClientConfig) -> Self {
120 self.config = c;
121 self
122 }
123 pub(crate) fn with_provider_set(mut self, set: Arc<ProviderSet>) -> Self {
125 self.injected_providers = Some(set);
126 self
127 }
128 pub fn client(mut self, handle: ClientHandle) -> Self {
136 self.shared_client = Some(handle);
137 self
138 }
139 pub fn cache(mut self, ttl: Duration) -> Self {
141 self.cache_ttl = Some(ttl);
142 self
143 }
144 pub fn logo(mut self) -> Self {
146 self.include_logo = true;
147 self
148 }
149
150 pub fn format(mut self, format: ValueFormat) -> Self {
159 self.value_format = format;
160 self
161 }
162
163 pub async fn build(self) -> Result<Ticker> {
165 let providers = if let Some(set) = self.injected_providers {
166 set
167 } else if let Some(handle) = self.shared_client {
168 let yahoo = YahooProvider::from_client(handle.0);
169 let client = yahoo.client_arc();
170 Arc::new(ProviderSet::new(
171 vec![Arc::new(yahoo) as Arc<dyn ProviderAdapter>],
172 Some(client),
173 Routes::new(Fetch::Sequential),
174 ))
175 } else {
176 Arc::new(
177 build_providers(
178 &[Provider::Yahoo],
179 &self.config,
180 Routes::new(Fetch::Sequential),
181 )
182 .await?,
183 )
184 };
185 Ok(Ticker {
186 symbol: self.symbol,
187 providers,
188 cache_ttl: self.cache_ttl,
189 include_logo: self.include_logo,
190 value_format: self.value_format,
191 quote_cache: Default::default(),
192 quote_fetch: Arc::new(tokio::sync::Mutex::new(())),
193 chart_cache: Default::default(),
194 events_cache: Default::default(),
195 news_cache: Default::default(),
196 options_cache: Default::default(),
197 financials_cache: Default::default(),
198 #[cfg(feature = "indicators")]
199 indicators_cache: Default::default(),
200 edgar_submissions_cache: Default::default(),
201 edgar_facts_cache: Default::default(),
202 })
203 }
204}
205
206pub struct Ticker {
211 symbol: Arc<str>,
212 providers: Arc<ProviderSet>,
213 cache_ttl: Option<Duration>,
214 include_logo: bool,
215 value_format: ValueFormat,
216 quote_cache: Cache<QuoteSummaryResponse>,
217 quote_fetch: Arc<tokio::sync::Mutex<()>>,
218 chart_cache: MapCache<(Interval, TimeRange), Chart>,
219 events_cache: Cache<ChartEvents>,
220 news_cache: Cache<Vec<News>>,
221 options_cache: MapCache<Option<i64>, Options>,
222 financials_cache: MapCache<(StatementType, Frequency), FinancialStatement>,
223 #[cfg(feature = "indicators")]
224 indicators_cache: MapCache<(Interval, TimeRange), indicators::IndicatorsSummary>,
225 edgar_submissions_cache: Cache<EdgarSubmissions>,
226 edgar_facts_cache: Cache<CompanyFacts>,
227}
228
229impl Ticker {
230 pub async fn new(symbol: impl Into<String>) -> Result<Self> {
232 Self::builder(symbol).build().await
233 }
234 pub fn builder(symbol: impl Into<String>) -> TickerBuilder {
236 TickerBuilder::new(symbol)
237 }
238 pub fn symbol(&self) -> &str {
240 &self.symbol
241 }
242
243 pub fn client_handle(&self) -> ClientHandle {
254 ClientHandle(
255 self.providers
256 .first_yahoo()
257 .expect("client_handle requires a Yahoo session; use Providers::ticker() for multi-provider tickers"),
258 )
259 }
260
261 #[allow(dead_code)]
262 pub(crate) fn provider_set(&self) -> &Arc<ProviderSet> {
263 &self.providers
264 }
265
266 fn is_cache_fresh<T>(&self, entry: Option<&CacheEntry<T>>) -> bool {
267 CacheEntry::is_fresh_with_ttl(entry, self.cache_ttl)
268 }
269
270 fn is_shared_cache_fresh<T>(&self, entry: Option<&CacheEntry<T>>) -> bool {
274 match (self.cache_ttl, entry) {
275 (Some(ttl), Some(e)) => e.is_fresh(ttl),
276 _ => false,
277 }
278 }
279 fn cache_insert<K: Eq + std::hash::Hash, V>(
280 &self,
281 map: &mut HashMap<K, CacheEntry<V>>,
282 key: K,
283 value: V,
284 ) {
285 if let Some(ttl) = self.cache_ttl {
286 if map.len() >= EVICTION_THRESHOLD {
287 map.retain(|_, entry| entry.is_fresh(ttl));
288 }
289 map.insert(key, CacheEntry::new(value));
290 }
291 }
292
293 pub async fn quote(&self) -> Result<Quote> {
295 let cache = self.ensure_quote().await?;
296 let summary = cache.as_ref().ok_or_else(|| {
297 FinanceError::ApiError("Quote summary cache was empty after fetch".to_string())
298 })?;
299 let (logo_url, company_logo_url) = if self.include_logo {
300 if let Ok(yahoo) = self.providers.first_yahoo() {
301 let logos = yahoo.get_logo_url(&self.symbol).await;
302 (logos.0, logos.1)
303 } else {
304 (None, None)
305 }
306 } else {
307 (None, None)
308 };
309 let quote = Quote::from_response(&summary.value, logo_url, company_logo_url);
310 match self.value_format {
315 ValueFormat::Raw => {
316 let json = serde_json::to_value("e).map_err(FinanceError::JsonParseError)?;
317 let transformed = self.value_format.transform(json);
318 serde_json::from_value(transformed).map_err(FinanceError::JsonParseError)
319 }
320 ValueFormat::Pretty | ValueFormat::Both => Ok(quote),
321 }
322 }
323
324 pub async fn quote_value(&self) -> Result<serde_json::Value> {
333 let quote = self.quote().await?;
334 let json = serde_json::to_value("e).map_err(FinanceError::JsonParseError)?;
335 Ok(self.value_format.transform(json))
336 }
337
338 fn chart_from_provider_data(
339 mut data: Chart,
340 interval: Option<Interval>,
341 range: Option<TimeRange>,
342 ) -> Chart {
343 data.interval = interval;
344 data.range = range;
345 data
346 }
347
348 pub async fn chart(&self, interval: Interval, range: TimeRange) -> Result<Chart> {
350 {
351 let cache = self.chart_cache.read().await;
352 if let Some(entry) = cache.get(&(interval, range))
353 && self.is_cache_fresh(Some(entry))
354 {
355 return Ok(entry.value.clone());
356 }
357 }
358 let sym = self.symbol.clone();
359 let data = self
360 .providers
361 .fetch(Capability::CHART, move |p| {
362 let sym = sym.clone();
363 let p = p.clone();
364 async move { p.fetch_chart(&sym, interval, range).await }
365 })
366 .await?;
367 let chart = Self::chart_from_provider_data(data, Some(interval), Some(range));
368 if self.cache_ttl.is_some() {
369 let mut cache = self.chart_cache.write().await;
370 self.cache_insert(&mut cache, (interval, range), chart.clone());
371 }
372 Ok(chart)
373 }
374
375 pub async fn chart_range(&self, interval: Interval, start: i64, end: i64) -> Result<Chart> {
377 if start >= end {
378 return Err(FinanceError::InvalidParameter {
379 param: "end".into(),
380 reason: format!("end ({end}) must be > start ({start})"),
381 });
382 }
383 let sym = self.symbol.clone();
384 let data = self
385 .providers
386 .fetch(Capability::CHART, move |p| {
387 let sym = sym.clone();
388 let p = p.clone();
389 async move { p.fetch_chart_range(&sym, interval, start, end).await }
390 })
391 .await?;
392 Ok(Self::chart_from_provider_data(data, Some(interval), None))
393 }
394
395 async fn ensure_events(&self) -> Result<()> {
396 {
397 let cache = self.events_cache.read().await;
398 if self.is_shared_cache_fresh(cache.as_ref()) {
399 return Ok(());
400 }
401 }
402 let sym = self.symbol.clone();
403 let events = self
404 .providers
405 .fetch(Capability::CORPORATE, move |p| {
406 let sym = sym.clone();
407 let p = p.clone();
408 async move { p.fetch_events(&sym).await }
409 })
410 .await?;
411 let mut cache = self.events_cache.write().await;
412 *cache = Some(CacheEntry::new(events));
413 Ok(())
414 }
415
416 pub async fn dividends(&self, range: TimeRange) -> Result<Vec<Dividend>> {
418 self.ensure_events().await?;
419 let cache = self.events_cache.read().await;
420 let all = cache
421 .as_ref()
422 .map(|e| e.value.to_dividends())
423 .unwrap_or_default();
424 Ok(filter_by_range(all, range))
425 }
426 pub async fn dividend_analytics(&self, range: TimeRange) -> Result<DividendAnalytics> {
428 let divs = self.dividends(range).await?;
429 Ok(DividendAnalytics::from_dividends(&divs))
430 }
431 pub async fn splits(&self, range: TimeRange) -> Result<Vec<Split>> {
433 self.ensure_events().await?;
434 let cache = self.events_cache.read().await;
435 let all = cache
436 .as_ref()
437 .map(|e| e.value.to_splits())
438 .unwrap_or_default();
439 Ok(filter_by_range(all, range))
440 }
441 pub async fn capital_gains(&self, range: TimeRange) -> Result<Vec<CapitalGain>> {
443 self.ensure_events().await?;
444 let cache = self.events_cache.read().await;
445 let all = cache
446 .as_ref()
447 .map(|e| e.value.to_capital_gains())
448 .unwrap_or_default();
449 Ok(filter_by_range(all, range))
450 }
451
452 pub async fn recommendations(&self, limit: u32) -> Result<Recommendation> {
454 if limit == 0 {
455 return Err(FinanceError::InvalidParameter {
456 param: "limit".into(),
457 reason: "limit must be > 0".into(),
458 });
459 }
460 let sym = self.symbol.clone();
461 let (provider_id, items) = self
462 .providers
463 .fetch(Capability::CORPORATE, move |p| {
464 let sym = sym.clone();
465 let p = p.clone();
466 async move {
467 let r = p.fetch_similar_symbols(&sym, limit).await?;
468 let provider = Provider::from_id_str(p.id()).ok_or_else(|| {
469 FinanceError::InternalError(format!("unknown provider id: {}", p.id()))
470 })?;
471 Ok((provider, r))
472 }
473 })
474 .await?;
475 Ok(recommendation_from_similar(
476 self.symbol.to_string(),
477 Some(provider_id),
478 items,
479 Some(limit),
480 ))
481 }
482
483 pub async fn news(&self) -> Result<Vec<News>> {
485 {
486 let cache = self.news_cache.read().await;
487 if let Some(e) = cache.as_ref()
488 && self.is_cache_fresh(Some(e))
489 {
490 return Ok(e.value.clone());
491 }
492 }
493 let sym = self.symbol.clone();
494 let data = self
495 .providers
496 .fetch(Capability::CORPORATE, move |p| {
497 let sym = sym.clone();
498 let p = p.clone();
499 async move { p.fetch_news(&sym).await }
500 })
501 .await?;
502 let news = data;
503 if self.cache_ttl.is_some() {
504 let mut c = self.news_cache.write().await;
505 *c = Some(CacheEntry::new(news.clone()));
506 }
507 Ok(news)
508 }
509
510 pub async fn options(&self, date: Option<i64>) -> Result<Options> {
512 {
513 let cache = self.options_cache.read().await;
514 if let Some(e) = cache.get(&date)
515 && self.is_cache_fresh(Some(e))
516 {
517 return Ok(e.value.clone());
518 }
519 }
520 let sym = self.symbol.clone();
521 let opts = self
522 .providers
523 .fetch(Capability::OPTIONS, move |p| {
524 let sym = sym.clone();
525 let p = p.clone();
526 async move { p.fetch_options(&sym, date).await }
527 })
528 .await?;
529 if self.cache_ttl.is_some() {
530 let mut c = self.options_cache.write().await;
531 self.cache_insert(&mut c, date, opts.clone());
532 }
533 Ok(opts)
534 }
535
536 pub async fn financials(
538 &self,
539 stmt_type: StatementType,
540 frequency: Frequency,
541 ) -> Result<FinancialStatement> {
542 let key = (stmt_type, frequency);
543 {
544 let cache = self.financials_cache.read().await;
545 if let Some(e) = cache.get(&key)
546 && self.is_cache_fresh(Some(e))
547 {
548 return Ok(e.value.clone());
549 }
550 }
551 let sym = self.symbol.clone();
552 let stmt = self
553 .providers
554 .fetch(Capability::FUNDAMENTALS, move |p| {
555 let sym = sym.clone();
556 let p = p.clone();
557 async move { p.fetch_financials(&sym, stmt_type, frequency).await }
558 })
559 .await?;
560 if self.cache_ttl.is_some() {
561 let mut c = self.financials_cache.write().await;
562 self.cache_insert(&mut c, key, stmt.clone());
563 }
564 Ok(stmt)
565 }
566
567 #[cfg(feature = "indicators")]
568 pub async fn indicators(
570 &self,
571 interval: Interval,
572 range: TimeRange,
573 ) -> Result<indicators::IndicatorsSummary> {
574 {
575 let cache = self.indicators_cache.read().await;
576 if let Some(e) = cache.get(&(interval, range))
577 && self.is_cache_fresh(Some(e))
578 {
579 return Ok(e.value.clone());
580 }
581 }
582 let chart = self.chart(interval, range).await?;
583 let ind = indicators::summary::calculate_indicators(&chart.candles);
584 if self.cache_ttl.is_some() {
585 let mut c = self.indicators_cache.write().await;
586 self.cache_insert(&mut c, (interval, range), ind.clone());
587 }
588 Ok(ind)
589 }
590
591 pub async fn edgar_submissions(&self) -> Result<EdgarSubmissions> {
597 {
598 let cache = self.edgar_submissions_cache.read().await;
599 if let Some(e) = cache.as_ref()
600 && self.is_cache_fresh(Some(e))
601 {
602 return Ok(e.value.clone());
603 }
604 }
605 let cik = edgar::resolve_cik(&self.symbol).await?;
606 let subs = edgar::submissions(cik).await?;
607 if self.cache_ttl.is_some() {
608 let mut c = self.edgar_submissions_cache.write().await;
609 *c = Some(CacheEntry::new(subs.clone()));
610 }
611 Ok(subs)
612 }
613
614 pub async fn edgar_company_facts(&self) -> Result<CompanyFacts> {
619 {
620 let cache = self.edgar_facts_cache.read().await;
621 if let Some(e) = cache.as_ref()
622 && self.is_cache_fresh(Some(e))
623 {
624 return Ok(e.value.clone());
625 }
626 }
627 let cik = edgar::resolve_cik(&self.symbol).await?;
628 let facts = edgar::company_facts(cik).await?;
629 if self.cache_ttl.is_some() {
630 let mut c = self.edgar_facts_cache.write().await;
631 *c = Some(CacheEntry::new(facts.clone()));
632 }
633 Ok(facts)
634 }
635
636 pub async fn filings(&self) -> Result<ProviderFilings> {
645 let symbol = self.symbol.clone();
646 self.providers
647 .fetch(Capability::FILINGS, move |p| {
648 let symbol = symbol.clone();
649 let p = p.clone();
650 async move { p.fetch_filings(&symbol).await }
651 })
652 .await
653 }
654
655 #[cfg(feature = "indicators")]
656 pub async fn indicator(
658 &self,
659 indicator: indicators::Indicator,
660 interval: Interval,
661 range: TimeRange,
662 ) -> Result<indicators::IndicatorResult> {
663 let chart = self.chart(interval, range).await?;
664 let o = chart.open_prices();
665 let h = chart.high_prices();
666 let l = chart.low_prices();
667 let c = chart.close_prices();
668 let v = chart.volumes();
669 use indicators::{Indicator, IndicatorResult};
670 Ok(match indicator {
671 Indicator::Sma(p) => IndicatorResult::Series(chart.sma(p)),
672 Indicator::Ema(p) => IndicatorResult::Series(chart.ema(p)),
673 Indicator::Rsi(p) => IndicatorResult::Series(chart.rsi(p)?),
674 Indicator::Macd { fast, slow, signal } => {
675 IndicatorResult::Macd(chart.macd(fast, slow, signal)?)
676 }
677 Indicator::Bollinger { period, std_dev } => {
678 IndicatorResult::Bollinger(chart.bollinger_bands(period, std_dev)?)
679 }
680 Indicator::Atr(p) => IndicatorResult::Series(chart.atr(p)?),
681 Indicator::Vwap => IndicatorResult::Series(crate::indicators::vwap(&h, &l, &c, &v)?),
682 Indicator::Wma(p) => IndicatorResult::Series(crate::indicators::wma(&c, p)?),
683 Indicator::Obv => IndicatorResult::Series(crate::indicators::obv(&c, &v)?),
684 Indicator::Dema(p) => IndicatorResult::Series(crate::indicators::dema(&c, p)?),
685 Indicator::Tema(p) => IndicatorResult::Series(crate::indicators::tema(&c, p)?),
686 Indicator::Hma(p) => IndicatorResult::Series(crate::indicators::hma(&c, p)?),
687 Indicator::Vwma(p) => IndicatorResult::Series(crate::indicators::vwma(&c, &v, p)?),
688 Indicator::Alma {
689 period,
690 offset,
691 sigma,
692 } => IndicatorResult::Series(crate::indicators::alma(&c, period, offset, sigma)?),
693 Indicator::McginleyDynamic(p) => {
694 IndicatorResult::Series(crate::indicators::mcginley_dynamic(&c, p)?)
695 }
696 Indicator::Stochastic {
697 k_period,
698 k_slow,
699 d_period,
700 } => IndicatorResult::Stochastic(crate::indicators::stochastic(
701 &h, &l, &c, k_period, k_slow, d_period,
702 )?),
703 Indicator::StochasticRsi {
704 rsi_period,
705 stoch_period,
706 k_period,
707 d_period,
708 } => IndicatorResult::Stochastic(crate::indicators::stochastic_rsi(
709 &c,
710 rsi_period,
711 stoch_period,
712 k_period,
713 d_period,
714 )?),
715 Indicator::Cci(p) => IndicatorResult::Series(crate::indicators::cci(&h, &l, &c, p)?),
716 Indicator::WilliamsR(p) => {
717 IndicatorResult::Series(crate::indicators::williams_r(&h, &l, &c, p)?)
718 }
719 Indicator::Roc(p) => IndicatorResult::Series(crate::indicators::roc(&c, p)?),
720 Indicator::Momentum(p) => IndicatorResult::Series(crate::indicators::momentum(&c, p)?),
721 Indicator::Cmo(p) => IndicatorResult::Series(crate::indicators::cmo(&c, p)?),
722 Indicator::AwesomeOscillator { fast, slow } => {
723 IndicatorResult::Series(crate::indicators::awesome_oscillator(&h, &l, fast, slow)?)
724 }
725 Indicator::CoppockCurve {
726 long_roc,
727 short_roc,
728 wma_period,
729 } => IndicatorResult::Series(crate::indicators::coppock_curve(
730 &c, long_roc, short_roc, wma_period,
731 )?),
732 Indicator::Adx(p) => IndicatorResult::Series(crate::indicators::adx(&h, &l, &c, p)?),
733 Indicator::Aroon(p) => IndicatorResult::Aroon(crate::indicators::aroon(&h, &l, p)?),
734 Indicator::Supertrend { period, multiplier } => IndicatorResult::SuperTrend(
735 crate::indicators::supertrend(&h, &l, &c, period, multiplier)?,
736 ),
737 Indicator::Ichimoku {
738 conversion,
739 base,
740 lagging,
741 displacement,
742 } => IndicatorResult::Ichimoku(crate::indicators::ichimoku(
743 &h,
744 &l,
745 &c,
746 conversion,
747 base,
748 lagging,
749 displacement,
750 )?),
751 Indicator::ParabolicSar { step, max } => {
752 IndicatorResult::Series(crate::indicators::parabolic_sar(&h, &l, &c, step, max)?)
753 }
754 Indicator::BullBearPower(p) => {
755 IndicatorResult::BullBearPower(crate::indicators::bull_bear_power(&h, &l, &c, p)?)
756 }
757 Indicator::ElderRay(p) => {
758 IndicatorResult::ElderRay(crate::indicators::elder_ray(&h, &l, &c, p)?)
759 }
760 Indicator::KeltnerChannels {
761 period,
762 multiplier,
763 atr_period,
764 } => IndicatorResult::Keltner(crate::indicators::keltner_channels(
765 &h, &l, &c, period, atr_period, multiplier,
766 )?),
767 Indicator::DonchianChannels(p) => {
768 IndicatorResult::Donchian(crate::indicators::donchian_channels(&h, &l, p)?)
769 }
770 Indicator::TrueRange => {
771 IndicatorResult::Series(crate::indicators::true_range(&h, &l, &c)?)
772 }
773 Indicator::ChoppinessIndex(p) => {
774 IndicatorResult::Series(crate::indicators::choppiness_index(&h, &l, &c, p)?)
775 }
776 Indicator::Mfi(p) => {
777 IndicatorResult::Series(crate::indicators::mfi(&h, &l, &c, &v, p)?)
778 }
779 Indicator::Cmf(p) => {
780 IndicatorResult::Series(crate::indicators::cmf(&h, &l, &c, &v, p)?)
781 }
782 Indicator::ChaikinOscillator => {
783 IndicatorResult::Series(crate::indicators::chaikin_oscillator(&h, &l, &c, &v)?)
784 }
785 Indicator::AccumulationDistribution => IndicatorResult::Series(
786 crate::indicators::accumulation_distribution(&h, &l, &c, &v)?,
787 ),
788 Indicator::BalanceOfPower(p) => {
789 IndicatorResult::Series(crate::indicators::balance_of_power(&o, &h, &l, &c, p)?)
790 }
791 })
792 }
793
794 #[cfg(feature = "backtesting")]
795 pub async fn backtest<S: backtesting::Strategy>(
797 &self,
798 strategy: S,
799 interval: Interval,
800 range: TimeRange,
801 config: Option<backtesting::BacktestConfig>,
802 ) -> backtesting::Result<backtesting::BacktestResult> {
803 let config = config.unwrap_or_default();
804 config.validate()?;
805 let chart = self
806 .chart(interval, range)
807 .await
808 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
809 let dividends = self.dividends(range).await.unwrap_or_default();
810 backtesting::BacktestEngine::new(config).run_with_dividends(
811 &self.symbol,
812 &chart.candles,
813 strategy,
814 ÷nds,
815 )
816 }
817
818 #[cfg(feature = "backtesting")]
819 pub async fn backtest_with_benchmark<S: backtesting::Strategy>(
821 &self,
822 strategy: S,
823 interval: Interval,
824 range: TimeRange,
825 config: Option<backtesting::BacktestConfig>,
826 benchmark: &str,
827 ) -> backtesting::Result<backtesting::BacktestResult> {
828 let config = config.unwrap_or_default();
829 config.validate()?;
830 let bench_ticker = Ticker::new(benchmark)
831 .await
832 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
833 let (chart, bench_chart) = tokio::try_join!(
834 self.chart(interval, range),
835 bench_ticker.chart(interval, range)
836 )
837 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
838 let dividends = self.dividends(range).await.unwrap_or_default();
839 backtesting::BacktestEngine::new(config).run_with_benchmark(
840 &self.symbol,
841 &chart.candles,
842 strategy,
843 ÷nds,
844 benchmark,
845 &bench_chart.candles,
846 )
847 }
848
849 #[cfg(feature = "risk")]
850 pub async fn risk(
852 &self,
853 interval: Interval,
854 range: TimeRange,
855 benchmark: Option<&str>,
856 ) -> Result<risk::RiskSummary> {
857 let chart = self.chart(interval, range).await?;
858 let bench_returns = if let Some(sym) = benchmark {
859 let bt = Ticker::new(sym).await?;
860 Some(risk::candles_to_returns(
861 &bt.chart(interval, range).await?.candles,
862 ))
863 } else {
864 None
865 };
866 Ok(risk::compute_risk_summary(
867 &chart.candles,
868 bench_returns.as_deref(),
869 ))
870 }
871
872 async fn ensure_quote(
873 &self,
874 ) -> Result<tokio::sync::RwLockReadGuard<'_, Option<CacheEntry<QuoteSummaryResponse>>>> {
875 {
876 let cache = self.quote_cache.read().await;
877 if self.is_shared_cache_fresh(cache.as_ref()) {
878 return Ok(cache);
879 }
880 }
881 let _guard = self.quote_fetch.lock().await;
882 {
883 let cache = self.quote_cache.read().await;
884 if self.is_shared_cache_fresh(cache.as_ref()) {
885 return Ok(cache);
886 }
887 }
888 let sym = self.symbol.clone();
889 let summary = self
890 .providers
891 .fetch(Capability::QUOTE, move |p| {
892 let sym = sym.clone();
893 let p = p.clone();
894 async move { p.fetch_quote(&sym).await }
895 })
896 .await?;
897 {
898 let mut cache = self.quote_cache.write().await;
899 *cache = Some(CacheEntry::new(summary));
900 }
901 Ok(self.quote_cache.read().await)
902 }
903}
904
905super::macros::define_quote_accessors! {
906 price -> Price, price,
907 summary_detail -> SummaryDetail, summary_detail,
908 financial_data -> FinancialData, financial_data,
909 key_stats -> DefaultKeyStatistics, default_key_statistics,
910 asset_profile -> AssetProfile, asset_profile,
911 calendar_events -> CalendarEvents, calendar_events,
912 earnings -> Earnings, earnings,
913 earnings_trend -> EarningsTrend, earnings_trend,
914 earnings_history -> EarningsHistory, earnings_history,
915 recommendation_trend -> RecommendationTrend, recommendation_trend,
916 insider_holders -> InsiderHolders, insider_holders,
917 insider_transactions -> InsiderTransactions, insider_transactions,
918 institution_ownership -> InstitutionOwnership, institution_ownership,
919 fund_ownership -> FundOwnership, fund_ownership,
920 major_holders -> MajorHoldersBreakdown, major_holders_breakdown,
921 share_purchase_activity -> NetSharePurchaseActivity, net_share_purchase_activity,
922 quote_type -> QuoteTypeData, quote_type,
923 summary_profile -> SummaryProfile, summary_profile,
924 sec_filings -> SecFilings, sec_filings,
925 grading_history -> UpgradeDowngradeHistory, upgrade_downgrade_history,
926 fund_performance -> FundPerformance, fund_performance,
927 fund_profile -> FundProfile, fund_profile,
928 top_holdings -> TopHoldings, top_holdings,
929 index_trend -> IndexTrend, index_trend,
930 industry_trend -> IndustryTrend, industry_trend,
931 sector_trend -> SectorTrend, sector_trend,
932 equity_performance -> EquityPerformance, equity_performance,
933}