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