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 let providers = if let Some(set) = self.injected_providers {
154 set
155 } else if let Some(handle) = self.shared_client {
156 let yahoo = YahooProvider::from_client(handle.0);
157 let client = yahoo.client_arc();
158 Arc::new(ProviderSet::new(
159 vec![Arc::new(yahoo) as Arc<dyn ProviderAdapter>],
160 Some(client),
161 Routes::new(Fetch::Sequential),
162 ))
163 } else {
164 Arc::new(
165 build_providers(
166 &[Provider::Yahoo],
167 &self.config,
168 Routes::new(Fetch::Sequential),
169 )
170 .await?,
171 )
172 };
173 Ok(Ticker {
174 symbol: self.symbol,
175 providers,
176 cache_ttl: self.cache_ttl,
177 include_logo: self.include_logo,
178 quote_cache: Default::default(),
179 quote_fetch: Arc::new(tokio::sync::Mutex::new(())),
180 chart_cache: Default::default(),
181 events_cache: Default::default(),
182 news_cache: Default::default(),
183 options_cache: Default::default(),
184 financials_cache: Default::default(),
185 #[cfg(feature = "indicators")]
186 indicators_cache: Default::default(),
187 edgar_submissions_cache: Default::default(),
188 edgar_facts_cache: Default::default(),
189 })
190 }
191}
192
193pub struct Ticker {
198 symbol: Arc<str>,
199 providers: Arc<ProviderSet>,
200 cache_ttl: Option<Duration>,
201 include_logo: bool,
202 quote_cache: Cache<QuoteSummaryResponse>,
203 quote_fetch: Arc<tokio::sync::Mutex<()>>,
204 chart_cache: MapCache<(Interval, TimeRange), Chart>,
205 events_cache: Cache<ChartEvents>,
206 news_cache: Cache<Vec<News>>,
207 options_cache: MapCache<Option<i64>, Options>,
208 financials_cache: MapCache<(StatementType, Frequency), FinancialStatement>,
209 #[cfg(feature = "indicators")]
210 indicators_cache: MapCache<(Interval, TimeRange), indicators::IndicatorsSummary>,
211 edgar_submissions_cache: Cache<EdgarSubmissions>,
212 edgar_facts_cache: Cache<CompanyFacts>,
213}
214
215impl Ticker {
216 pub async fn new(symbol: impl Into<String>) -> Result<Self> {
218 Self::builder(symbol).build().await
219 }
220 pub fn builder(symbol: impl Into<String>) -> TickerBuilder {
222 TickerBuilder::new(symbol)
223 }
224 pub fn symbol(&self) -> &str {
226 &self.symbol
227 }
228
229 pub fn client_handle(&self) -> ClientHandle {
240 ClientHandle(
241 self.providers
242 .first_yahoo()
243 .expect("client_handle requires a Yahoo session; use Providers::ticker() for multi-provider tickers"),
244 )
245 }
246
247 #[allow(dead_code)]
248 pub(crate) fn provider_set(&self) -> &Arc<ProviderSet> {
249 &self.providers
250 }
251
252 fn is_cache_fresh<T>(&self, entry: Option<&CacheEntry<T>>) -> bool {
253 CacheEntry::is_fresh_with_ttl(entry, self.cache_ttl)
254 }
255
256 fn is_shared_cache_fresh<T>(&self, entry: Option<&CacheEntry<T>>) -> bool {
260 match (self.cache_ttl, entry) {
261 (Some(ttl), Some(e)) => e.is_fresh(ttl),
262 _ => false,
263 }
264 }
265 fn cache_insert<K: Eq + std::hash::Hash, V>(
266 &self,
267 map: &mut HashMap<K, CacheEntry<V>>,
268 key: K,
269 value: V,
270 ) {
271 if let Some(ttl) = self.cache_ttl {
272 if map.len() >= EVICTION_THRESHOLD {
273 map.retain(|_, entry| entry.is_fresh(ttl));
274 }
275 map.insert(key, CacheEntry::new(value));
276 }
277 }
278
279 pub async fn quote<F>(&self) -> Result<Quote<F>>
281 where
282 F: Format,
283 Quote<Both>: Into<Quote<F>>,
284 {
285 let cache = self.ensure_quote().await?;
286 let summary = cache.as_ref().ok_or_else(|| {
287 FinanceError::ApiError("Quote summary cache was empty after fetch".to_string())
288 })?;
289 let (logo_url, company_logo_url) = if self.include_logo {
290 if let Ok(yahoo) = self.providers.first_yahoo() {
291 let logos = yahoo.get_logo_url(&self.symbol).await;
292 (logos.0, logos.1)
293 } else {
294 (None, None)
295 }
296 } else {
297 (None, None)
298 };
299 let quote = Quote::from_response(&summary.value, logo_url, company_logo_url);
300 Ok(quote.into())
301 }
302
303 fn chart_from_provider_data(
304 mut data: Chart,
305 interval: Option<Interval>,
306 range: Option<TimeRange>,
307 ) -> Chart {
308 data.interval = interval;
309 data.range = range;
310 data
311 }
312
313 pub async fn chart(&self, interval: Interval, range: TimeRange) -> Result<Chart> {
315 {
316 let cache = self.chart_cache.read().await;
317 if let Some(entry) = cache.get(&(interval, range))
318 && self.is_cache_fresh(Some(entry))
319 {
320 return Ok(entry.value.clone());
321 }
322 }
323 let sym = self.symbol.clone();
324 let data = self
325 .providers
326 .fetch(Capability::CHART, move |p| {
327 let sym = sym.clone();
328 let p = p.clone();
329 async move { p.fetch_chart(&sym, interval, range).await }
330 })
331 .await?;
332 let chart = Self::chart_from_provider_data(data, Some(interval), Some(range));
333 if self.cache_ttl.is_some() {
334 let mut cache = self.chart_cache.write().await;
335 self.cache_insert(&mut cache, (interval, range), chart.clone());
336 }
337 Ok(chart)
338 }
339
340 pub async fn chart_range(&self, interval: Interval, start: i64, end: i64) -> Result<Chart> {
342 if start >= end {
343 return Err(FinanceError::InvalidParameter {
344 param: "end".into(),
345 reason: format!("end ({end}) must be > start ({start})"),
346 });
347 }
348 let sym = self.symbol.clone();
349 let data = self
350 .providers
351 .fetch(Capability::CHART, move |p| {
352 let sym = sym.clone();
353 let p = p.clone();
354 async move { p.fetch_chart_range(&sym, interval, start, end).await }
355 })
356 .await?;
357 Ok(Self::chart_from_provider_data(data, Some(interval), None))
358 }
359
360 async fn ensure_events(&self) -> Result<()> {
361 {
362 let cache = self.events_cache.read().await;
363 if self.is_shared_cache_fresh(cache.as_ref()) {
364 return Ok(());
365 }
366 }
367 let sym = self.symbol.clone();
368 let events = self
369 .providers
370 .fetch(Capability::CORPORATE, move |p| {
371 let sym = sym.clone();
372 let p = p.clone();
373 async move { p.fetch_events(&sym).await }
374 })
375 .await?;
376 let mut cache = self.events_cache.write().await;
377 *cache = Some(CacheEntry::new(events));
378 Ok(())
379 }
380
381 pub async fn dividends(&self, range: TimeRange) -> Result<Vec<Dividend>> {
383 self.ensure_events().await?;
384 let cache = self.events_cache.read().await;
385 let all = cache
386 .as_ref()
387 .map(|e| e.value.to_dividends())
388 .unwrap_or_default();
389 Ok(filter_by_range(all, range))
390 }
391 pub async fn dividend_analytics(&self, range: TimeRange) -> Result<DividendAnalytics> {
393 let divs = self.dividends(range).await?;
394 Ok(DividendAnalytics::from_dividends(&divs))
395 }
396 pub async fn splits(&self, range: TimeRange) -> Result<Vec<Split>> {
398 self.ensure_events().await?;
399 let cache = self.events_cache.read().await;
400 let all = cache
401 .as_ref()
402 .map(|e| e.value.to_splits())
403 .unwrap_or_default();
404 Ok(filter_by_range(all, range))
405 }
406 pub async fn capital_gains(&self, range: TimeRange) -> Result<Vec<CapitalGain>> {
408 self.ensure_events().await?;
409 let cache = self.events_cache.read().await;
410 let all = cache
411 .as_ref()
412 .map(|e| e.value.to_capital_gains())
413 .unwrap_or_default();
414 Ok(filter_by_range(all, range))
415 }
416
417 pub async fn recommendations(&self, limit: u32) -> Result<Recommendation> {
419 if limit == 0 {
420 return Err(FinanceError::InvalidParameter {
421 param: "limit".into(),
422 reason: "limit must be > 0".into(),
423 });
424 }
425 let sym = self.symbol.clone();
426 let (provider_id, items) = self
427 .providers
428 .fetch(Capability::CORPORATE, move |p| {
429 let sym = sym.clone();
430 let p = p.clone();
431 async move {
432 let r = p.fetch_similar_symbols(&sym, limit).await?;
433 let provider = Provider::from_id_str(p.id()).ok_or_else(|| {
434 FinanceError::InternalError(format!("unknown provider id: {}", p.id()))
435 })?;
436 Ok((provider, r))
437 }
438 })
439 .await?;
440 Ok(recommendation_from_similar(
441 self.symbol.to_string(),
442 Some(provider_id),
443 items,
444 Some(limit),
445 ))
446 }
447
448 pub async fn news(&self) -> Result<Vec<News>> {
450 {
451 let cache = self.news_cache.read().await;
452 if let Some(e) = cache.as_ref()
453 && self.is_cache_fresh(Some(e))
454 {
455 return Ok(e.value.clone());
456 }
457 }
458 let sym = self.symbol.clone();
459 let data = self
460 .providers
461 .fetch(Capability::CORPORATE, move |p| {
462 let sym = sym.clone();
463 let p = p.clone();
464 async move { p.fetch_news(&sym).await }
465 })
466 .await?;
467 let news = data;
468 if self.cache_ttl.is_some() {
469 let mut c = self.news_cache.write().await;
470 *c = Some(CacheEntry::new(news.clone()));
471 }
472 Ok(news)
473 }
474
475 pub async fn options(&self, date: Option<i64>) -> Result<Options> {
477 {
478 let cache = self.options_cache.read().await;
479 if let Some(e) = cache.get(&date)
480 && self.is_cache_fresh(Some(e))
481 {
482 return Ok(e.value.clone());
483 }
484 }
485 let sym = self.symbol.clone();
486 let opts = self
487 .providers
488 .fetch(Capability::OPTIONS, move |p| {
489 let sym = sym.clone();
490 let p = p.clone();
491 async move { p.fetch_options(&sym, date).await }
492 })
493 .await?;
494 if self.cache_ttl.is_some() {
495 let mut c = self.options_cache.write().await;
496 self.cache_insert(&mut c, date, opts.clone());
497 }
498 Ok(opts)
499 }
500
501 pub async fn financials(
503 &self,
504 stmt_type: StatementType,
505 frequency: Frequency,
506 ) -> Result<FinancialStatement> {
507 let key = (stmt_type, frequency);
508 {
509 let cache = self.financials_cache.read().await;
510 if let Some(e) = cache.get(&key)
511 && self.is_cache_fresh(Some(e))
512 {
513 return Ok(e.value.clone());
514 }
515 }
516 let sym = self.symbol.clone();
517 let stmt = self
518 .providers
519 .fetch(Capability::FUNDAMENTALS, move |p| {
520 let sym = sym.clone();
521 let p = p.clone();
522 async move { p.fetch_financials(&sym, stmt_type, frequency).await }
523 })
524 .await?;
525 if self.cache_ttl.is_some() {
526 let mut c = self.financials_cache.write().await;
527 self.cache_insert(&mut c, key, stmt.clone());
528 }
529 Ok(stmt)
530 }
531
532 #[cfg(feature = "indicators")]
533 pub async fn indicators(
535 &self,
536 interval: Interval,
537 range: TimeRange,
538 ) -> Result<indicators::IndicatorsSummary> {
539 {
540 let cache = self.indicators_cache.read().await;
541 if let Some(e) = cache.get(&(interval, range))
542 && self.is_cache_fresh(Some(e))
543 {
544 return Ok(e.value.clone());
545 }
546 }
547 let chart = self.chart(interval, range).await?;
548 let ind = indicators::summary::calculate_indicators(&chart.candles);
549 if self.cache_ttl.is_some() {
550 let mut c = self.indicators_cache.write().await;
551 self.cache_insert(&mut c, (interval, range), ind.clone());
552 }
553 Ok(ind)
554 }
555
556 pub async fn edgar_submissions(&self) -> Result<EdgarSubmissions> {
562 {
563 let cache = self.edgar_submissions_cache.read().await;
564 if let Some(e) = cache.as_ref()
565 && self.is_cache_fresh(Some(e))
566 {
567 return Ok(e.value.clone());
568 }
569 }
570 let cik = edgar::resolve_cik(&self.symbol).await?;
571 let subs = edgar::submissions(cik).await?;
572 if self.cache_ttl.is_some() {
573 let mut c = self.edgar_submissions_cache.write().await;
574 *c = Some(CacheEntry::new(subs.clone()));
575 }
576 Ok(subs)
577 }
578
579 pub async fn edgar_company_facts(&self) -> Result<CompanyFacts> {
584 {
585 let cache = self.edgar_facts_cache.read().await;
586 if let Some(e) = cache.as_ref()
587 && self.is_cache_fresh(Some(e))
588 {
589 return Ok(e.value.clone());
590 }
591 }
592 let cik = edgar::resolve_cik(&self.symbol).await?;
593 let facts = edgar::company_facts(cik).await?;
594 if self.cache_ttl.is_some() {
595 let mut c = self.edgar_facts_cache.write().await;
596 *c = Some(CacheEntry::new(facts.clone()));
597 }
598 Ok(facts)
599 }
600
601 pub async fn filings(&self) -> Result<ProviderFilings> {
610 let symbol = self.symbol.clone();
611 self.providers
612 .fetch(Capability::FILINGS, move |p| {
613 let symbol = symbol.clone();
614 let p = p.clone();
615 async move { p.fetch_filings(&symbol).await }
616 })
617 .await
618 }
619
620 #[cfg(feature = "indicators")]
621 pub async fn indicator(
623 &self,
624 indicator: indicators::Indicator,
625 interval: Interval,
626 range: TimeRange,
627 ) -> Result<indicators::IndicatorResult> {
628 let chart = self.chart(interval, range).await?;
629 let o = chart.open_prices();
630 let h = chart.high_prices();
631 let l = chart.low_prices();
632 let c = chart.close_prices();
633 let v = chart.volumes();
634 use indicators::{Indicator, IndicatorResult};
635 Ok(match indicator {
636 Indicator::Sma(p) => IndicatorResult::Series(chart.sma(p)),
637 Indicator::Ema(p) => IndicatorResult::Series(chart.ema(p)),
638 Indicator::Rsi(p) => IndicatorResult::Series(chart.rsi(p)?),
639 Indicator::Macd { fast, slow, signal } => {
640 IndicatorResult::Macd(chart.macd(fast, slow, signal)?)
641 }
642 Indicator::Bollinger { period, std_dev } => {
643 IndicatorResult::Bollinger(chart.bollinger_bands(period, std_dev)?)
644 }
645 Indicator::Atr(p) => IndicatorResult::Series(chart.atr(p)?),
646 Indicator::Vwap => IndicatorResult::Series(crate::indicators::vwap(&h, &l, &c, &v)?),
647 Indicator::Wma(p) => IndicatorResult::Series(crate::indicators::wma(&c, p)?),
648 Indicator::Obv => IndicatorResult::Series(crate::indicators::obv(&c, &v)?),
649 Indicator::Dema(p) => IndicatorResult::Series(crate::indicators::dema(&c, p)?),
650 Indicator::Tema(p) => IndicatorResult::Series(crate::indicators::tema(&c, p)?),
651 Indicator::Hma(p) => IndicatorResult::Series(crate::indicators::hma(&c, p)?),
652 Indicator::Vwma(p) => IndicatorResult::Series(crate::indicators::vwma(&c, &v, p)?),
653 Indicator::Alma {
654 period,
655 offset,
656 sigma,
657 } => IndicatorResult::Series(crate::indicators::alma(&c, period, offset, sigma)?),
658 Indicator::McginleyDynamic(p) => {
659 IndicatorResult::Series(crate::indicators::mcginley_dynamic(&c, p)?)
660 }
661 Indicator::Stochastic {
662 k_period,
663 k_slow,
664 d_period,
665 } => IndicatorResult::Stochastic(crate::indicators::stochastic(
666 &h, &l, &c, k_period, k_slow, d_period,
667 )?),
668 Indicator::StochasticRsi {
669 rsi_period,
670 stoch_period,
671 k_period,
672 d_period,
673 } => IndicatorResult::Stochastic(crate::indicators::stochastic_rsi(
674 &c,
675 rsi_period,
676 stoch_period,
677 k_period,
678 d_period,
679 )?),
680 Indicator::Cci(p) => IndicatorResult::Series(crate::indicators::cci(&h, &l, &c, p)?),
681 Indicator::WilliamsR(p) => {
682 IndicatorResult::Series(crate::indicators::williams_r(&h, &l, &c, p)?)
683 }
684 Indicator::Roc(p) => IndicatorResult::Series(crate::indicators::roc(&c, p)?),
685 Indicator::Momentum(p) => IndicatorResult::Series(crate::indicators::momentum(&c, p)?),
686 Indicator::Cmo(p) => IndicatorResult::Series(crate::indicators::cmo(&c, p)?),
687 Indicator::AwesomeOscillator { fast, slow } => {
688 IndicatorResult::Series(crate::indicators::awesome_oscillator(&h, &l, fast, slow)?)
689 }
690 Indicator::CoppockCurve {
691 long_roc,
692 short_roc,
693 wma_period,
694 } => IndicatorResult::Series(crate::indicators::coppock_curve(
695 &c, long_roc, short_roc, wma_period,
696 )?),
697 Indicator::Adx(p) => IndicatorResult::Series(crate::indicators::adx(&h, &l, &c, p)?),
698 Indicator::Aroon(p) => IndicatorResult::Aroon(crate::indicators::aroon(&h, &l, p)?),
699 Indicator::Supertrend { period, multiplier } => IndicatorResult::SuperTrend(
700 crate::indicators::supertrend(&h, &l, &c, period, multiplier)?,
701 ),
702 Indicator::Ichimoku {
703 conversion,
704 base,
705 lagging,
706 displacement,
707 } => IndicatorResult::Ichimoku(crate::indicators::ichimoku(
708 &h,
709 &l,
710 &c,
711 conversion,
712 base,
713 lagging,
714 displacement,
715 )?),
716 Indicator::ParabolicSar { step, max } => {
717 IndicatorResult::Series(crate::indicators::parabolic_sar(&h, &l, &c, step, max)?)
718 }
719 Indicator::BullBearPower(p) => {
720 IndicatorResult::BullBearPower(crate::indicators::bull_bear_power(&h, &l, &c, p)?)
721 }
722 Indicator::ElderRay(p) => {
723 IndicatorResult::ElderRay(crate::indicators::elder_ray(&h, &l, &c, p)?)
724 }
725 Indicator::KeltnerChannels {
726 period,
727 multiplier,
728 atr_period,
729 } => IndicatorResult::Keltner(crate::indicators::keltner_channels(
730 &h, &l, &c, period, atr_period, multiplier,
731 )?),
732 Indicator::DonchianChannels(p) => {
733 IndicatorResult::Donchian(crate::indicators::donchian_channels(&h, &l, p)?)
734 }
735 Indicator::TrueRange => {
736 IndicatorResult::Series(crate::indicators::true_range(&h, &l, &c)?)
737 }
738 Indicator::ChoppinessIndex(p) => {
739 IndicatorResult::Series(crate::indicators::choppiness_index(&h, &l, &c, p)?)
740 }
741 Indicator::Mfi(p) => {
742 IndicatorResult::Series(crate::indicators::mfi(&h, &l, &c, &v, p)?)
743 }
744 Indicator::Cmf(p) => {
745 IndicatorResult::Series(crate::indicators::cmf(&h, &l, &c, &v, p)?)
746 }
747 Indicator::ChaikinOscillator => {
748 IndicatorResult::Series(crate::indicators::chaikin_oscillator(&h, &l, &c, &v)?)
749 }
750 Indicator::AccumulationDistribution => IndicatorResult::Series(
751 crate::indicators::accumulation_distribution(&h, &l, &c, &v)?,
752 ),
753 Indicator::BalanceOfPower(p) => {
754 IndicatorResult::Series(crate::indicators::balance_of_power(&o, &h, &l, &c, p)?)
755 }
756 })
757 }
758
759 #[cfg(feature = "backtesting")]
760 pub async fn backtest<S: backtesting::Strategy>(
762 &self,
763 strategy: S,
764 interval: Interval,
765 range: TimeRange,
766 config: Option<backtesting::BacktestConfig>,
767 ) -> backtesting::Result<backtesting::BacktestResult> {
768 let config = config.unwrap_or_default();
769 config.validate()?;
770 let chart = self
771 .chart(interval, range)
772 .await
773 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
774 let dividends = self.dividends(range).await.unwrap_or_default();
775 backtesting::BacktestEngine::new(config).run_with_dividends(
776 &self.symbol,
777 &chart.candles,
778 strategy,
779 ÷nds,
780 )
781 }
782
783 #[cfg(feature = "backtesting")]
784 pub async fn backtest_with_benchmark<S: backtesting::Strategy>(
786 &self,
787 strategy: S,
788 interval: Interval,
789 range: TimeRange,
790 config: Option<backtesting::BacktestConfig>,
791 benchmark: &str,
792 ) -> backtesting::Result<backtesting::BacktestResult> {
793 let config = config.unwrap_or_default();
794 config.validate()?;
795 let bench_ticker = Ticker::new(benchmark)
796 .await
797 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
798 let (chart, bench_chart) = tokio::try_join!(
799 self.chart(interval, range),
800 bench_ticker.chart(interval, range)
801 )
802 .map_err(|e| backtesting::BacktestError::ChartError(e.to_string()))?;
803 let dividends = self.dividends(range).await.unwrap_or_default();
804 backtesting::BacktestEngine::new(config).run_with_benchmark(
805 &self.symbol,
806 &chart.candles,
807 strategy,
808 ÷nds,
809 benchmark,
810 &bench_chart.candles,
811 )
812 }
813
814 #[cfg(feature = "risk")]
815 pub async fn risk(
817 &self,
818 interval: Interval,
819 range: TimeRange,
820 benchmark: Option<&str>,
821 ) -> Result<risk::RiskSummary> {
822 let chart = self.chart(interval, range).await?;
823 let bench_returns = if let Some(sym) = benchmark {
824 let bt = Ticker::new(sym).await?;
825 Some(risk::candles_to_returns(
826 &bt.chart(interval, range).await?.candles,
827 ))
828 } else {
829 None
830 };
831 Ok(risk::compute_risk_summary(
832 &chart.candles,
833 bench_returns.as_deref(),
834 ))
835 }
836
837 async fn ensure_quote(
838 &self,
839 ) -> Result<tokio::sync::RwLockReadGuard<'_, Option<CacheEntry<QuoteSummaryResponse>>>> {
840 {
841 let cache = self.quote_cache.read().await;
842 if self.is_shared_cache_fresh(cache.as_ref()) {
843 return Ok(cache);
844 }
845 }
846 let _guard = self.quote_fetch.lock().await;
847 {
848 let cache = self.quote_cache.read().await;
849 if self.is_shared_cache_fresh(cache.as_ref()) {
850 return Ok(cache);
851 }
852 }
853 let sym = self.symbol.clone();
854 let summary = self
855 .providers
856 .fetch(Capability::QUOTE, move |p| {
857 let sym = sym.clone();
858 let p = p.clone();
859 async move { p.fetch_quote(&sym).await }
860 })
861 .await?;
862 {
863 let mut cache = self.quote_cache.write().await;
864 *cache = Some(CacheEntry::new(summary));
865 }
866 Ok(self.quote_cache.read().await)
867 }
868}
869
870super::macros::define_quote_accessors! {
871 price -> Price, price,
872 summary_detail -> SummaryDetail, summary_detail,
873 financial_data -> FinancialData, financial_data,
874 key_stats -> DefaultKeyStatistics, default_key_statistics,
875 asset_profile -> AssetProfile, asset_profile,
876 calendar_events -> CalendarEvents, calendar_events,
877 earnings -> Earnings, earnings,
878 earnings_trend -> EarningsTrend, earnings_trend,
879 earnings_history -> EarningsHistory, earnings_history,
880 recommendation_trend -> RecommendationTrend, recommendation_trend,
881 insider_holders -> InsiderHolders, insider_holders,
882 insider_transactions -> InsiderTransactions, insider_transactions,
883 institution_ownership -> InstitutionOwnership, institution_ownership,
884 fund_ownership -> FundOwnership, fund_ownership,
885 major_holders -> MajorHoldersBreakdown, major_holders_breakdown,
886 share_purchase_activity -> NetSharePurchaseActivity, net_share_purchase_activity,
887 quote_type -> QuoteTypeData, quote_type,
888 summary_profile -> SummaryProfile, summary_profile,
889 sec_filings -> SecFilings, sec_filings,
890 grading_history -> UpgradeDowngradeHistory, upgrade_downgrade_history,
891 fund_performance -> FundPerformance, fund_performance,
892 fund_profile -> FundProfile, fund_profile,
893 top_holdings -> TopHoldings, top_holdings,
894 index_trend -> IndexTrend, index_trend,
895 industry_trend -> IndustryTrend, industry_trend,
896 sector_trend -> SectorTrend, sector_trend,
897 equity_performance -> EquityPerformance, equity_performance,
898}