finance_query/finance.rs
1//! Non-symbol-specific Yahoo Finance operations
2//!
3//! This module provides functions for operations that don't require a specific stock symbol,
4//! such as searching for symbols and fetching screener data.
5
6use crate::adapters::yahoo::client::ClientConfig;
7use crate::constants::Region;
8use crate::constants::screeners::Screener;
9use crate::constants::sectors::Sector;
10use crate::error::Result;
11use crate::models::corporate::transcript::{Transcript, TranscriptWithMeta};
12use crate::models::discovery::screeners::ScreenerResults;
13use crate::models::discovery::search::SearchResults;
14use crate::models::market::industries::IndustryData;
15use crate::models::market::sectors::SectorData;
16
17#[cfg(any(feature = "fmp", feature = "alphavantage"))]
18use serde::{Deserialize, Serialize};
19
20// Re-export options for convenience
21pub use crate::adapters::yahoo::discovery::lookup::{LookupOptions, LookupType};
22pub use crate::adapters::yahoo::discovery::search::SearchOptions;
23
24/// Search for stock symbols and companies
25///
26/// # Arguments
27///
28/// * `query` - Search term (company name, symbol, etc.)
29/// * `options` - Search configuration options
30///
31/// # Examples
32///
33/// ```no_run
34/// use finance_query::{finance, SearchOptions, Region};
35///
36/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
37/// // Simple search with defaults
38/// let results = finance::search("Apple", &SearchOptions::default()).await?;
39/// println!("Found {} results", results.result_count());
40///
41/// // Search with custom options
42/// let options = SearchOptions::new()
43/// .quotes_count(10)
44/// .news_count(5)
45/// .enable_research_reports(true)
46/// .region(Region::Canada);
47/// let results = finance::search("NVDA", &options).await?;
48/// println!("Found {} quotes", results.quotes.len());
49/// # Ok(())
50/// # }
51/// ```
52pub async fn search(query: &str, options: &SearchOptions) -> Result<SearchResults> {
53 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
54 let results = client.search(query, options).await;
55 #[cfg(feature = "sentiment")]
56 let results = results.map(|mut r| {
57 for article in r.news.0.iter_mut() {
58 if let Some(title) = article.title.as_deref() {
59 article.sentiment = Some(crate::models::sentiment::analyze(title));
60 }
61 }
62 r
63 });
64 results
65}
66
67/// Look up symbols by type (equity, ETF, mutual fund, index, future, currency, cryptocurrency)
68///
69/// Unlike search, lookup specializes in discovering tickers filtered by asset type.
70/// Optionally fetches logo URLs via an additional API call.
71///
72/// # Arguments
73///
74/// * `query` - Search term (company name, symbol, etc.)
75/// * `options` - Lookup configuration options
76///
77/// # Examples
78///
79/// ```no_run
80/// use finance_query::{finance, LookupOptions, LookupType, Region};
81///
82/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
83/// // Simple lookup with defaults
84/// let results = finance::lookup("Apple", &LookupOptions::default()).await?;
85/// println!("Found {} results", results.result_count());
86///
87/// // Lookup equities with logos
88/// let options = LookupOptions::new()
89/// .lookup_type(LookupType::Equity)
90/// .count(10)
91/// .include_logo(true);
92/// let results = finance::lookup("NVDA", &options).await?;
93/// for quote in &results.quotes {
94/// println!("{}: {:?}", quote.symbol, quote.logo_url);
95/// }
96/// # Ok(())
97/// # }
98/// ```
99pub async fn lookup(
100 query: &str,
101 options: &LookupOptions,
102) -> Result<crate::models::discovery::lookup::LookupResults> {
103 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
104 client.lookup(query, options).await
105}
106
107/// Fetch data from a predefined Yahoo Finance screener
108///
109/// Returns stocks/funds matching the criteria of the specified screener type.
110///
111/// # Arguments
112///
113/// * `screener_type` - The predefined screener to use
114/// * `count` - Number of results to return (max 250)
115///
116/// # Examples
117///
118/// ```no_run
119/// use finance_query::{finance, Screener};
120///
121/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
122/// // Get top gainers
123/// let gainers = finance::screener(Screener::DayGainers, 25).await?;
124/// println!("Top gainers: {:#?}", gainers);
125///
126/// // Get most shorted stocks
127/// let shorted = finance::screener(Screener::MostShortedStocks, 25).await?;
128///
129/// // Get growth technology stocks
130/// let tech = finance::screener(Screener::GrowthTechnologyStocks, 25).await?;
131/// # Ok(())
132/// # }
133/// ```
134pub async fn screener(screener_type: Screener, count: u32) -> Result<ScreenerResults> {
135 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
136 crate::adapters::yahoo::discovery::screeners::fetch(&client, screener_type, count).await
137}
138
139/// Execute a custom screener query
140///
141/// Allows flexible filtering of stocks/funds/ETFs based on various criteria.
142/// Use [`EquityScreenerQuery`][crate::EquityScreenerQuery] for stock screeners
143/// or [`FundScreenerQuery`][crate::FundScreenerQuery] for mutual fund screeners.
144///
145/// # Arguments
146///
147/// * `query` - The custom screener query to execute
148///
149/// # Examples
150///
151/// ```no_run
152/// use finance_query::{finance, EquityField, EquityScreenerQuery, ScreenerFieldExt};
153///
154/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
155/// // Find US large-cap stocks with high volume
156/// let query = EquityScreenerQuery::new()
157/// .size(25)
158/// .sort_by(EquityField::IntradayMarketCap, false)
159/// .add_condition(EquityField::Region.eq_str("us"))
160/// .add_condition(EquityField::AvgDailyVol3M.gt(200_000.0))
161/// .add_condition(EquityField::IntradayMarketCap.gt(10_000_000_000.0));
162///
163/// let result = finance::custom_screener(query).await?;
164/// println!("Found {} stocks", result.quotes.len());
165/// # Ok(())
166/// # }
167/// ```
168pub async fn custom_screener<F: crate::models::discovery::screeners::ScreenerField>(
169 query: crate::models::discovery::screeners::ScreenerQuery<F>,
170) -> Result<ScreenerResults> {
171 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
172 crate::adapters::yahoo::discovery::screeners::fetch_custom(&client, query).await
173}
174
175/// Get general market news
176///
177/// # Examples
178///
179/// ```no_run
180/// use finance_query::finance;
181///
182/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
183/// let news = finance::news().await?;
184/// for article in news {
185/// println!("{}: {}", article.source, article.title);
186/// }
187/// # Ok(())
188/// # }
189/// ```
190pub async fn news() -> Result<Vec<crate::models::corporate::news::News>> {
191 let news = crate::scrapers::stockanalysis::scrape_general_news().await;
192 #[cfg(feature = "sentiment")]
193 let news = news.map(|mut articles| {
194 for article in articles.iter_mut() {
195 article.sentiment = Some(crate::models::sentiment::analyze(&article.title));
196 }
197 articles
198 });
199 news
200}
201
202/// Get earnings transcript for a symbol
203///
204/// Fetches the earnings call transcript, handling all the complexity internally:
205/// 1. Gets the company ID (quartrId) from the quote_type endpoint
206/// 2. Scrapes available earnings calls
207/// 3. Fetches the requested transcript
208///
209/// # Arguments
210///
211/// * `symbol` - Stock symbol (e.g., "AAPL", "MSFT")
212/// * `quarter` - Optional fiscal quarter (Q1, Q2, Q3, Q4). If None, gets latest.
213/// * `year` - Optional fiscal year. If None, gets latest.
214///
215/// # Examples
216///
217/// ```no_run
218/// use finance_query::finance;
219///
220/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
221/// // Get the latest transcript
222/// let latest = finance::earnings_transcript("AAPL", None, None).await?;
223/// println!("Quarter: {} {}", latest.quarter(), latest.year());
224///
225/// // Get a specific quarter
226/// let q4_2024 = finance::earnings_transcript("AAPL", Some("Q4"), Some(2024)).await?;
227/// # Ok(())
228/// # }
229/// ```
230pub async fn earnings_transcript(
231 symbol: &str,
232 quarter: Option<&str>,
233 year: Option<i32>,
234) -> Result<Transcript> {
235 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
236 let transcript = crate::adapters::yahoo::corporate::transcripts::fetch_for_symbol(
237 &client, symbol, quarter, year,
238 )
239 .await;
240 #[cfg(feature = "sentiment")]
241 let transcript = transcript.map(|mut t| {
242 t.score_sentiment();
243 t
244 });
245 transcript
246}
247
248/// Get all earnings transcripts for a symbol
249///
250/// Fetches transcripts for all available earnings calls.
251///
252/// # Arguments
253///
254/// * `symbol` - Stock symbol (e.g., "AAPL", "MSFT")
255/// * `limit` - Optional maximum number of transcripts. If None, fetches all.
256///
257/// # Examples
258///
259/// ```no_run
260/// use finance_query::finance;
261///
262/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
263/// // Get all transcripts
264/// let all = finance::earnings_transcripts("AAPL", None).await?;
265///
266/// // Get only the 5 most recent
267/// let recent = finance::earnings_transcripts("AAPL", Some(5)).await?;
268/// for t in &recent {
269/// println!("{}: {} {}", t.title, t.transcript.quarter(), t.transcript.year());
270/// }
271/// # Ok(())
272/// # }
273/// ```
274pub async fn earnings_transcripts(
275 symbol: &str,
276 limit: Option<usize>,
277) -> Result<Vec<TranscriptWithMeta>> {
278 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
279 let transcripts = crate::adapters::yahoo::corporate::transcripts::fetch_all_for_symbol(
280 &client, symbol, limit,
281 )
282 .await;
283 #[cfg(feature = "sentiment")]
284 let transcripts = transcripts.map(|mut list| {
285 for t in list.iter_mut() {
286 t.transcript.score_sentiment();
287 }
288 list
289 });
290 transcripts
291}
292
293/// Get market hours/status
294///
295/// Returns the current status for various markets.
296///
297/// # Arguments
298///
299/// * `region` - Optional region override. If None, uses default (US).
300///
301/// # Examples
302///
303/// ```no_run
304/// use finance_query::{finance, Region};
305///
306/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
307/// // Get US market hours (default)
308/// let hours = finance::hours(None).await?;
309///
310/// // Get Japan market hours
311/// let jp_hours = finance::hours(Some(Region::Japan)).await?;
312/// # Ok(())
313/// # }
314/// ```
315pub async fn hours(region: Option<Region>) -> Result<crate::models::market::hours::MarketHours> {
316 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
317 crate::adapters::yahoo::market::hours::fetch(&client, region.map(|r| r.region())).await
318}
319
320/// Get world market indices quotes
321///
322/// Returns quotes for major world indices, optionally filtered by region.
323///
324/// # Arguments
325///
326/// * `region` - Optional region filter. If None, returns all world indices.
327///
328/// # Examples
329///
330/// ```no_run
331/// use finance_query::{finance, IndicesRegion};
332///
333/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
334/// // Get all world indices
335/// let all = finance::indices(None).await?;
336/// println!("Fetched {} indices", all.success_count());
337///
338/// // Get only Americas indices
339/// let americas = finance::indices(Some(IndicesRegion::Americas)).await?;
340/// # Ok(())
341/// # }
342/// ```
343pub async fn indices(
344 region: Option<crate::constants::indices::Region>,
345) -> Result<crate::tickers::BatchQuotesResponse> {
346 use crate::Tickers;
347 use crate::constants::indices::all_symbols;
348
349 let symbols: Vec<&str> = match region {
350 Some(r) => r.symbols().to_vec(),
351 None => all_symbols(),
352 };
353
354 let tickers = Tickers::new(symbols).await?;
355 tickers.quotes().await
356}
357
358/// Fetch detailed sector data from Yahoo Finance
359///
360/// Returns comprehensive sector information including overview, performance,
361/// top companies, ETFs, mutual funds, industries, and research reports.
362///
363/// # Arguments
364///
365/// * `sector_type` - The sector to fetch data for
366///
367/// # Examples
368///
369/// ```no_run
370/// use finance_query::{finance, Sector};
371///
372/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
373/// let sector = finance::sector(Sector::Technology).await?;
374/// println!("Sector: {} ({} companies)", sector.name,
375/// sector.overview.as_ref().map(|o| o.companies_count.unwrap_or(0)).unwrap_or(0));
376///
377/// for company in sector.top_companies.iter().take(5) {
378/// println!(" {} - {:?}", company.symbol, company.name);
379/// }
380/// # Ok(())
381/// # }
382/// ```
383pub async fn sector(sector_type: Sector) -> Result<SectorData> {
384 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
385 crate::adapters::yahoo::market::sectors::fetch(&client, sector_type).await
386}
387
388/// Fetch detailed industry data from Yahoo Finance
389///
390/// Returns comprehensive industry information including overview, performance,
391/// top companies, top performing companies, top growth companies, and research reports.
392///
393/// # Arguments
394///
395/// * `industry_key` - The industry key/slug (e.g., "semiconductors", "software-infrastructure")
396///
397/// # Examples
398///
399/// ```no_run
400/// use finance_query::finance;
401///
402/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
403/// let industry = finance::industry("semiconductors").await?;
404/// println!("Industry: {} ({} companies)", industry.name,
405/// industry.overview.as_ref().map(|o| o.companies_count.unwrap_or(0)).unwrap_or(0));
406///
407/// for company in industry.top_companies.iter().take(5) {
408/// println!(" {} - {:?}", company.symbol, company.name);
409/// }
410/// # Ok(())
411/// # }
412/// ```
413pub async fn industry(industry_key: impl AsRef<str>) -> Result<IndustryData> {
414 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
415 crate::adapters::yahoo::market::industries::fetch(&client, industry_key.as_ref()).await
416}
417
418/// Get list of available currencies
419///
420/// Returns currency information from Yahoo Finance.
421///
422/// # Examples
423///
424/// ```no_run
425/// use finance_query::finance;
426///
427/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
428/// let currencies = finance::currencies().await?;
429/// # Ok(())
430/// # }
431/// ```
432pub async fn currencies() -> Result<Vec<crate::models::market::currencies::Currency>> {
433 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
434 crate::adapters::yahoo::market::currencies::fetch(&client).await
435}
436
437/// Get list of supported exchanges
438///
439/// Scrapes the Yahoo Finance help page for a list of supported exchanges
440/// with their symbol suffixes and data delay information.
441///
442/// # Examples
443///
444/// ```no_run
445/// use finance_query::finance;
446///
447/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
448/// let exchanges = finance::exchanges().await?;
449/// for exchange in &exchanges {
450/// println!("{} - {} ({})", exchange.country, exchange.market, exchange.suffix);
451/// }
452/// # Ok(())
453/// # }
454/// ```
455pub async fn exchanges() -> Result<Vec<crate::models::market::exchanges::Exchange>> {
456 crate::scrapers::yahoo_exchanges::scrape_exchanges().await
457}
458
459/// Get market summary
460///
461/// Returns market summary with major indices, currencies, and commodities.
462///
463/// # Arguments
464///
465/// * `region` - Optional region for localization. If None, uses default (US).
466///
467/// # Examples
468///
469/// ```no_run
470/// use finance_query::{finance, Region};
471///
472/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
473/// // Use default (US)
474/// let summary = finance::market_summary(None).await?;
475/// // Or specify a region
476/// let summary = finance::market_summary(Some(Region::Canada)).await?;
477/// # Ok(())
478/// # }
479/// ```
480pub async fn market_summary(
481 region: Option<Region>,
482) -> Result<Vec<crate::models::market::market_summary::MarketSummaryQuote>> {
483 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
484 crate::adapters::yahoo::market::market_summary::fetch(&client, region).await
485}
486
487/// Get trending tickers for a region
488///
489/// Returns trending stocks for a specific region.
490///
491/// # Arguments
492///
493/// * `region` - Optional region for localization. If None, uses default (US).
494///
495/// # Examples
496///
497/// ```no_run
498/// use finance_query::{finance, Region};
499///
500/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
501/// // Use default (US)
502/// let trending = finance::trending(None).await?;
503/// // Or specify a region
504/// let trending = finance::trending(Some(Region::Canada)).await?;
505/// # Ok(())
506/// # }
507/// ```
508pub async fn trending(
509 region: Option<Region>,
510) -> Result<Vec<crate::models::discovery::trending::TrendingQuote>> {
511 let client = crate::adapters::yahoo::session::get_or_auth(&ClientConfig::default()).await?;
512 crate::adapters::yahoo::market::trending::fetch(&client, region).await
513}
514
515/// Fetch the current CNN Fear & Greed Index from Alternative.me.
516///
517/// Returns a 0–100 sentiment score and its classification. No API key required.
518///
519/// # Examples
520///
521/// ```no_run
522/// use finance_query::finance;
523///
524/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
525/// let fg = finance::fear_and_greed().await?;
526/// println!("Fear & Greed: {} ({})", fg.value, fg.classification.as_str());
527/// # Ok(())
528/// # }
529/// ```
530pub async fn fear_and_greed() -> Result<crate::models::sentiment::FearAndGreed> {
531 crate::adapters::yahoo::market::fear_and_greed::fetch().await
532}
533
534/// Fetch the crypto Fear & Greed Index from Alternative.me — current value
535/// plus up to `limit - 1` historical readings (newest first).
536///
537/// Alternative.me's index specifically tracks crypto (Bitcoin) market
538/// sentiment from volatility, momentum, social media, dominance, and
539/// Google Trends signals. No API key required.
540///
541/// # Arguments
542///
543/// * `limit` - Number of readings to return, newest first (`1` for just the
544/// current value; e.g. `30` for the trailing month).
545///
546/// # Examples
547///
548/// ```no_run
549/// use finance_query::finance;
550///
551/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
552/// let history = finance::fear_and_greed_crypto(7).await?;
553/// let latest = &history[0];
554/// println!("Crypto Fear & Greed: {} ({})", latest.value, latest.classification.as_str());
555/// # Ok(())
556/// # }
557/// ```
558pub async fn fear_and_greed_crypto(
559 limit: u32,
560) -> Result<Vec<crate::models::sentiment::FearAndGreed>> {
561 crate::adapters::yahoo::market::fear_and_greed::fetch_history(limit).await
562}
563
564// ── Financial Modeling Prep (FMP) ───────────────────────────────────
565
566/// Time period for analyst estimates.
567#[cfg(feature = "fmp")]
568#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
569pub enum Period {
570 /// Annual (yearly) estimates.
571 Annual,
572 /// Quarterly estimates.
573 Quarter,
574}
575
576#[cfg(feature = "fmp")]
577impl From<Period> for crate::adapters::fmp::models::Period {
578 fn from(p: Period) -> Self {
579 match p {
580 Period::Annual => Self::Annual,
581 Period::Quarter => Self::Quarter,
582 }
583 }
584}
585
586/// An insider trading transaction record.
587#[cfg(feature = "fmp")]
588#[non_exhaustive]
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct InsiderTransaction {
591 /// Ticker symbol.
592 pub symbol: Option<String>,
593 /// Filing date (YYYY-MM-DD).
594 pub filing_date: Option<String>,
595 /// Transaction date (YYYY-MM-DD).
596 pub transaction_date: Option<String>,
597 /// Reporting person name.
598 pub reporting_name: Option<String>,
599 /// Transaction type (e.g., "P-Purchase", "S-Sale").
600 pub transaction_type: Option<String>,
601 /// Number of securities transacted.
602 pub securities_transacted: Option<f64>,
603 /// Price per share.
604 pub price: Option<f64>,
605 /// Securities owned after transaction.
606 pub securities_owned: Option<f64>,
607 /// Form type / owner type description.
608 pub type_of_owner: Option<String>,
609 /// Link to SEC filing.
610 pub link: Option<String>,
611}
612
613#[cfg(feature = "fmp")]
614impl From<crate::adapters::fmp::corporate::insider_trading::InsiderTradeDTO>
615 for InsiderTransaction
616{
617 fn from(d: crate::adapters::fmp::corporate::insider_trading::InsiderTradeDTO) -> Self {
618 use crate::adapters::fmp::corporate::insider_trading::InsiderTradeDTO;
619 let InsiderTradeDTO {
620 symbol,
621 filing_date,
622 transaction_date,
623 reporting_name,
624 transaction_type,
625 securities_transacted,
626 price,
627 securities_owned,
628 type_of_owner,
629 link,
630 ..
631 } = d;
632 Self {
633 symbol,
634 filing_date,
635 transaction_date,
636 reporting_name,
637 transaction_type,
638 securities_transacted,
639 price,
640 securities_owned,
641 type_of_owner,
642 link,
643 }
644 }
645}
646
647/// An analyst estimate entry (revenue, EBITDA, EPS forecasts).
648#[cfg(feature = "fmp")]
649#[non_exhaustive]
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct AnalystEstimate {
652 /// Ticker symbol.
653 pub symbol: Option<String>,
654 /// Estimate date.
655 pub date: Option<String>,
656 /// Estimated revenue low.
657 pub estimated_revenue_low: Option<f64>,
658 /// Estimated revenue high.
659 pub estimated_revenue_high: Option<f64>,
660 /// Estimated revenue avg.
661 pub estimated_revenue_avg: Option<f64>,
662 /// Estimated EBITDA low.
663 pub estimated_ebitda_low: Option<f64>,
664 /// Estimated EBITDA high.
665 pub estimated_ebitda_high: Option<f64>,
666 /// Estimated EBITDA avg.
667 pub estimated_ebitda_avg: Option<f64>,
668 /// Estimated EPS avg.
669 pub estimated_eps_avg: Option<f64>,
670 /// Estimated EPS high.
671 pub estimated_eps_high: Option<f64>,
672 /// Estimated EPS low.
673 pub estimated_eps_low: Option<f64>,
674 /// Number of analysts covering revenue.
675 pub number_analyst_estimated_revenue: Option<i32>,
676 /// Number of analysts covering EPS.
677 pub number_analysts_estimated_eps: Option<i32>,
678}
679
680#[cfg(feature = "fmp")]
681impl From<crate::adapters::fmp::fundamentals::estimates::AnalystEstimateDTO> for AnalystEstimate {
682 fn from(d: crate::adapters::fmp::fundamentals::estimates::AnalystEstimateDTO) -> Self {
683 use crate::adapters::fmp::fundamentals::estimates::AnalystEstimateDTO;
684 let AnalystEstimateDTO {
685 symbol,
686 date,
687 estimated_revenue_low,
688 estimated_revenue_high,
689 estimated_revenue_avg,
690 estimated_ebitda_low,
691 estimated_ebitda_high,
692 estimated_ebitda_avg,
693 estimated_eps_avg,
694 estimated_eps_high,
695 estimated_eps_low,
696 number_analyst_estimated_revenue,
697 number_analysts_estimated_eps,
698 } = d;
699 Self {
700 symbol,
701 date,
702 estimated_revenue_low,
703 estimated_revenue_high,
704 estimated_revenue_avg,
705 estimated_ebitda_low,
706 estimated_ebitda_high,
707 estimated_ebitda_avg,
708 estimated_eps_avg,
709 estimated_eps_high,
710 estimated_eps_low,
711 number_analyst_estimated_revenue,
712 number_analysts_estimated_eps,
713 }
714 }
715}
716
717/// An analyst stock recommendation (buy/hold/sell counts).
718#[cfg(feature = "fmp")]
719#[non_exhaustive]
720#[derive(Debug, Clone, Serialize, Deserialize)]
721pub struct AnalystRecommendation {
722 /// Ticker symbol.
723 pub symbol: Option<String>,
724 /// Recommendation date.
725 pub date: Option<String>,
726 /// Number of buy ratings.
727 pub analyst_ratings_buy: Option<i32>,
728 /// Number of hold ratings.
729 pub analyst_ratings_hold: Option<i32>,
730 /// Number of sell ratings.
731 pub analyst_ratings_sell: Option<i32>,
732 /// Number of strong buy ratings.
733 pub analyst_ratings_strong_buy: Option<i32>,
734 /// Number of strong sell ratings.
735 pub analyst_ratings_strong_sell: Option<i32>,
736}
737
738#[cfg(feature = "fmp")]
739impl From<crate::adapters::fmp::fundamentals::estimates::AnalystRecommendationDTO>
740 for AnalystRecommendation
741{
742 fn from(d: crate::adapters::fmp::fundamentals::estimates::AnalystRecommendationDTO) -> Self {
743 use crate::adapters::fmp::fundamentals::estimates::AnalystRecommendationDTO;
744 let AnalystRecommendationDTO {
745 symbol,
746 date,
747 analyst_ratings_buy,
748 analyst_ratings_hold,
749 analyst_ratings_sell,
750 analyst_ratings_strong_buy,
751 analyst_ratings_strong_sell,
752 } = d;
753 Self {
754 symbol,
755 date,
756 analyst_ratings_buy,
757 analyst_ratings_hold,
758 analyst_ratings_sell,
759 analyst_ratings_strong_buy,
760 analyst_ratings_strong_sell,
761 }
762 }
763}
764
765/// Fetch insider trading transactions for a symbol.
766#[cfg(feature = "fmp")]
767pub async fn insider_trading(symbol: &str, limit: u32) -> Result<Vec<InsiderTransaction>> {
768 crate::adapters::fmp::corporate::insider_trading::insider_trading(symbol, limit)
769 .await
770 .map(|v| v.into_iter().map(Into::into).collect())
771}
772
773/// Fetch analyst estimates for a symbol.
774#[cfg(feature = "fmp")]
775pub async fn analyst_estimates(symbol: &str, period: Period) -> Result<Vec<AnalystEstimate>> {
776 crate::adapters::fmp::fundamentals::estimates::analyst_estimates(symbol, period.into(), 4)
777 .await
778 .map(|v| v.into_iter().map(Into::into).collect())
779}
780
781/// Fetch analyst stock recommendations for a symbol.
782#[cfg(feature = "fmp")]
783pub async fn analyst_recommendations(symbol: &str) -> Result<Vec<AnalystRecommendation>> {
784 crate::adapters::fmp::fundamentals::estimates::analyst_recommendations(symbol)
785 .await
786 .map(|v| v.into_iter().map(Into::into).collect())
787}
788
789// ── Polygon.io ──────────────────────────────────────────────────────
790
791/// Fetch sentiment analysis for a symbol based on recent Polygon.io news.
792#[cfg(feature = "polygon")]
793pub async fn symbol_sentiment(symbol: &str) -> Result<crate::models::sentiment::SymbolSentiment> {
794 use crate::adapters::polygon;
795 let paginated = polygon::stock_news(&[("ticker", symbol), ("limit", "10")]).await?;
796 let articles = paginated.results.unwrap_or_default();
797
798 let mut positive = 0u32;
799 let mut negative = 0u32;
800 let total = articles.len().max(1) as f64;
801 for article in &articles {
802 if let Some(ref insights) = article.insights {
803 for insight in insights {
804 if insight.ticker.as_deref() == Some(symbol) {
805 match insight.sentiment.as_deref() {
806 Some("positive") => positive += 1,
807 Some("negative") => negative += 1,
808 _ => {}
809 }
810 }
811 }
812 }
813 }
814
815 let (score, label): (Option<f64>, Option<String>) = if total > 0.0 {
816 let s = (positive as f64 - negative as f64) / total;
817 let l = if s > 0.2 {
818 "positive"
819 } else if s < -0.2 {
820 "negative"
821 } else {
822 "neutral"
823 };
824 (Some(s), Some(l.to_string()))
825 } else {
826 (None, None)
827 };
828
829 Ok(crate::models::sentiment::SymbolSentiment { score, label })
830}
831
832// ── Alpha Vantage ───────────────────────────────────────────────────
833
834/// An upcoming earnings calendar entry.
835#[cfg(feature = "alphavantage")]
836#[non_exhaustive]
837#[derive(Debug, Clone, Serialize, Deserialize)]
838pub struct EarningsCalendarEntry {
839 /// Ticker symbol.
840 pub symbol: String,
841 /// Company name.
842 pub name: Option<String>,
843 /// Report date.
844 pub report_date: Option<String>,
845 /// Fiscal date ending.
846 pub fiscal_date_ending: Option<String>,
847 /// Estimated EPS.
848 pub estimate: Option<f64>,
849 /// Currency.
850 pub currency: Option<String>,
851}
852
853#[cfg(feature = "alphavantage")]
854impl From<crate::adapters::alphavantage::models::EarningsCalendarEntryDTO>
855 for EarningsCalendarEntry
856{
857 fn from(d: crate::adapters::alphavantage::models::EarningsCalendarEntryDTO) -> Self {
858 Self {
859 symbol: d.symbol,
860 name: d.name,
861 report_date: d.report_date,
862 fiscal_date_ending: d.fiscal_date_ending,
863 estimate: d.estimate,
864 currency: d.currency,
865 }
866 }
867}
868
869/// An upcoming IPO calendar entry.
870#[cfg(feature = "alphavantage")]
871#[non_exhaustive]
872#[derive(Debug, Clone, Serialize, Deserialize)]
873pub struct IpoCalendarEntry {
874 /// Ticker symbol.
875 pub symbol: Option<String>,
876 /// Company name.
877 pub name: Option<String>,
878 /// IPO date.
879 pub ipo_date: Option<String>,
880 /// Price range (e.g., `"$15-$17"`).
881 pub price_range: Option<String>,
882 /// Exchange.
883 pub exchange: Option<String>,
884}
885
886#[cfg(feature = "alphavantage")]
887impl From<crate::adapters::alphavantage::models::IpoCalendarEntryDTO> for IpoCalendarEntry {
888 fn from(d: crate::adapters::alphavantage::models::IpoCalendarEntryDTO) -> Self {
889 Self {
890 symbol: d.symbol,
891 name: d.name,
892 ipo_date: d.ipo_date,
893 price_range: d.price_range,
894 exchange: d.exchange,
895 }
896 }
897}
898
899/// Fetch the upcoming earnings calendar (market-wide, not symbol-filtered).
900#[cfg(feature = "alphavantage")]
901pub async fn earnings_calendar() -> Result<Vec<EarningsCalendarEntry>> {
902 crate::adapters::alphavantage::fundamentals::earnings_calendar()
903 .await
904 .map(|v| v.into_iter().map(Into::into).collect())
905}
906
907/// Fetch the upcoming IPO calendar (market-wide, not symbol-filtered).
908#[cfg(feature = "alphavantage")]
909pub async fn ipo_calendar() -> Result<Vec<IpoCalendarEntry>> {
910 crate::adapters::alphavantage::fundamentals::ipo_calendar()
911 .await
912 .map(|v| v.into_iter().map(Into::into).collect())
913}
914
915#[cfg(test)]
916mod tests {
917 use super::*;
918
919 #[tokio::test]
920 #[ignore = "requires network access"]
921 async fn finance_calls_share_one_session() {
922 let config = ClientConfig::default();
923 let first = crate::adapters::yahoo::session::get_or_auth(&config)
924 .await
925 .unwrap();
926
927 let _ = search("apple", &SearchOptions::default()).await;
928 let _ = hours(None).await;
929
930 let after = crate::adapters::yahoo::session::get_or_auth(&config)
931 .await
932 .unwrap();
933 assert!(std::sync::Arc::ptr_eq(&first, &after));
934 }
935}