Skip to main content

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}