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    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 = YahooClient::new(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 = YahooClient::new(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 = YahooClient::new(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 = YahooClient::new(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 = YahooClient::new(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 = YahooClient::new(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 = YahooClient::new(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 = YahooClient::new(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 = YahooClient::new(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 = YahooClient::new(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 = YahooClient::new(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// ── Financial Modeling Prep (FMP) ───────────────────────────────────
535
536/// Time period for analyst estimates.
537#[cfg(feature = "fmp")]
538#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
539pub enum Period {
540    /// Annual (yearly) estimates.
541    Annual,
542    /// Quarterly estimates.
543    Quarter,
544}
545
546#[cfg(feature = "fmp")]
547impl From<Period> for crate::adapters::fmp::models::Period {
548    fn from(p: Period) -> Self {
549        match p {
550            Period::Annual => Self::Annual,
551            Period::Quarter => Self::Quarter,
552        }
553    }
554}
555
556/// An insider trading transaction record.
557#[cfg(feature = "fmp")]
558#[non_exhaustive]
559#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct InsiderTransaction {
561    /// Ticker symbol.
562    pub symbol: Option<String>,
563    /// Filing date (YYYY-MM-DD).
564    pub filing_date: Option<String>,
565    /// Transaction date (YYYY-MM-DD).
566    pub transaction_date: Option<String>,
567    /// Reporting person name.
568    pub reporting_name: Option<String>,
569    /// Transaction type (e.g., "P-Purchase", "S-Sale").
570    pub transaction_type: Option<String>,
571    /// Number of securities transacted.
572    pub securities_transacted: Option<f64>,
573    /// Price per share.
574    pub price: Option<f64>,
575    /// Securities owned after transaction.
576    pub securities_owned: Option<f64>,
577    /// Form type / owner type description.
578    pub type_of_owner: Option<String>,
579    /// Link to SEC filing.
580    pub link: Option<String>,
581}
582
583#[cfg(feature = "fmp")]
584impl From<crate::adapters::fmp::corporate::insider_trading::InsiderTradeDTO>
585    for InsiderTransaction
586{
587    fn from(d: crate::adapters::fmp::corporate::insider_trading::InsiderTradeDTO) -> Self {
588        use crate::adapters::fmp::corporate::insider_trading::InsiderTradeDTO;
589        let InsiderTradeDTO {
590            symbol,
591            filing_date,
592            transaction_date,
593            reporting_name,
594            transaction_type,
595            securities_transacted,
596            price,
597            securities_owned,
598            type_of_owner,
599            link,
600            ..
601        } = d;
602        Self {
603            symbol,
604            filing_date,
605            transaction_date,
606            reporting_name,
607            transaction_type,
608            securities_transacted,
609            price,
610            securities_owned,
611            type_of_owner,
612            link,
613        }
614    }
615}
616
617/// An analyst estimate entry (revenue, EBITDA, EPS forecasts).
618#[cfg(feature = "fmp")]
619#[non_exhaustive]
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct AnalystEstimate {
622    /// Ticker symbol.
623    pub symbol: Option<String>,
624    /// Estimate date.
625    pub date: Option<String>,
626    /// Estimated revenue low.
627    pub estimated_revenue_low: Option<f64>,
628    /// Estimated revenue high.
629    pub estimated_revenue_high: Option<f64>,
630    /// Estimated revenue avg.
631    pub estimated_revenue_avg: Option<f64>,
632    /// Estimated EBITDA low.
633    pub estimated_ebitda_low: Option<f64>,
634    /// Estimated EBITDA high.
635    pub estimated_ebitda_high: Option<f64>,
636    /// Estimated EBITDA avg.
637    pub estimated_ebitda_avg: Option<f64>,
638    /// Estimated EPS avg.
639    pub estimated_eps_avg: Option<f64>,
640    /// Estimated EPS high.
641    pub estimated_eps_high: Option<f64>,
642    /// Estimated EPS low.
643    pub estimated_eps_low: Option<f64>,
644    /// Number of analysts covering revenue.
645    pub number_analyst_estimated_revenue: Option<i32>,
646    /// Number of analysts covering EPS.
647    pub number_analysts_estimated_eps: Option<i32>,
648}
649
650#[cfg(feature = "fmp")]
651impl From<crate::adapters::fmp::fundamentals::estimates::AnalystEstimateDTO> for AnalystEstimate {
652    fn from(d: crate::adapters::fmp::fundamentals::estimates::AnalystEstimateDTO) -> Self {
653        use crate::adapters::fmp::fundamentals::estimates::AnalystEstimateDTO;
654        let AnalystEstimateDTO {
655            symbol,
656            date,
657            estimated_revenue_low,
658            estimated_revenue_high,
659            estimated_revenue_avg,
660            estimated_ebitda_low,
661            estimated_ebitda_high,
662            estimated_ebitda_avg,
663            estimated_eps_avg,
664            estimated_eps_high,
665            estimated_eps_low,
666            number_analyst_estimated_revenue,
667            number_analysts_estimated_eps,
668        } = d;
669        Self {
670            symbol,
671            date,
672            estimated_revenue_low,
673            estimated_revenue_high,
674            estimated_revenue_avg,
675            estimated_ebitda_low,
676            estimated_ebitda_high,
677            estimated_ebitda_avg,
678            estimated_eps_avg,
679            estimated_eps_high,
680            estimated_eps_low,
681            number_analyst_estimated_revenue,
682            number_analysts_estimated_eps,
683        }
684    }
685}
686
687/// An analyst stock recommendation (buy/hold/sell counts).
688#[cfg(feature = "fmp")]
689#[non_exhaustive]
690#[derive(Debug, Clone, Serialize, Deserialize)]
691pub struct AnalystRecommendation {
692    /// Ticker symbol.
693    pub symbol: Option<String>,
694    /// Recommendation date.
695    pub date: Option<String>,
696    /// Number of buy ratings.
697    pub analyst_ratings_buy: Option<i32>,
698    /// Number of hold ratings.
699    pub analyst_ratings_hold: Option<i32>,
700    /// Number of sell ratings.
701    pub analyst_ratings_sell: Option<i32>,
702    /// Number of strong buy ratings.
703    pub analyst_ratings_strong_buy: Option<i32>,
704    /// Number of strong sell ratings.
705    pub analyst_ratings_strong_sell: Option<i32>,
706}
707
708#[cfg(feature = "fmp")]
709impl From<crate::adapters::fmp::fundamentals::estimates::AnalystRecommendationDTO>
710    for AnalystRecommendation
711{
712    fn from(d: crate::adapters::fmp::fundamentals::estimates::AnalystRecommendationDTO) -> Self {
713        use crate::adapters::fmp::fundamentals::estimates::AnalystRecommendationDTO;
714        let AnalystRecommendationDTO {
715            symbol,
716            date,
717            analyst_ratings_buy,
718            analyst_ratings_hold,
719            analyst_ratings_sell,
720            analyst_ratings_strong_buy,
721            analyst_ratings_strong_sell,
722        } = d;
723        Self {
724            symbol,
725            date,
726            analyst_ratings_buy,
727            analyst_ratings_hold,
728            analyst_ratings_sell,
729            analyst_ratings_strong_buy,
730            analyst_ratings_strong_sell,
731        }
732    }
733}
734
735/// Fetch insider trading transactions for a symbol.
736#[cfg(feature = "fmp")]
737pub async fn insider_trading(symbol: &str, limit: u32) -> Result<Vec<InsiderTransaction>> {
738    crate::adapters::fmp::corporate::insider_trading::insider_trading(symbol, limit)
739        .await
740        .map(|v| v.into_iter().map(Into::into).collect())
741}
742
743/// Fetch analyst estimates for a symbol.
744#[cfg(feature = "fmp")]
745pub async fn analyst_estimates(symbol: &str, period: Period) -> Result<Vec<AnalystEstimate>> {
746    crate::adapters::fmp::fundamentals::estimates::analyst_estimates(symbol, period.into(), 4)
747        .await
748        .map(|v| v.into_iter().map(Into::into).collect())
749}
750
751/// Fetch analyst stock recommendations for a symbol.
752#[cfg(feature = "fmp")]
753pub async fn analyst_recommendations(symbol: &str) -> Result<Vec<AnalystRecommendation>> {
754    crate::adapters::fmp::fundamentals::estimates::analyst_recommendations(symbol)
755        .await
756        .map(|v| v.into_iter().map(Into::into).collect())
757}
758
759// ── Polygon.io ──────────────────────────────────────────────────────
760
761/// Fetch sentiment analysis for a symbol based on recent Polygon.io news.
762#[cfg(feature = "polygon")]
763pub async fn symbol_sentiment(symbol: &str) -> Result<crate::models::sentiment::SymbolSentiment> {
764    use crate::adapters::polygon;
765    let paginated = polygon::stock_news(&[("ticker", symbol), ("limit", "10")]).await?;
766    let articles = paginated.results.unwrap_or_default();
767
768    let mut positive = 0u32;
769    let mut negative = 0u32;
770    let total = articles.len().max(1) as f64;
771    for article in &articles {
772        if let Some(ref insights) = article.insights {
773            for insight in insights {
774                if insight.ticker.as_deref() == Some(symbol) {
775                    match insight.sentiment.as_deref() {
776                        Some("positive") => positive += 1,
777                        Some("negative") => negative += 1,
778                        _ => {}
779                    }
780                }
781            }
782        }
783    }
784
785    let (score, label): (Option<f64>, Option<String>) = if total > 0.0 {
786        let s = (positive as f64 - negative as f64) / total;
787        let l = if s > 0.2 {
788            "positive"
789        } else if s < -0.2 {
790            "negative"
791        } else {
792            "neutral"
793        };
794        (Some(s), Some(l.to_string()))
795    } else {
796        (None, None)
797    };
798
799    Ok(crate::models::sentiment::SymbolSentiment { score, label })
800}
801
802// ── Alpha Vantage ───────────────────────────────────────────────────
803
804/// An upcoming earnings calendar entry.
805#[cfg(feature = "alphavantage")]
806#[non_exhaustive]
807#[derive(Debug, Clone, Serialize, Deserialize)]
808pub struct EarningsCalendarEntry {
809    /// Ticker symbol.
810    pub symbol: String,
811    /// Company name.
812    pub name: Option<String>,
813    /// Report date.
814    pub report_date: Option<String>,
815    /// Fiscal date ending.
816    pub fiscal_date_ending: Option<String>,
817    /// Estimated EPS.
818    pub estimate: Option<f64>,
819    /// Currency.
820    pub currency: Option<String>,
821}
822
823#[cfg(feature = "alphavantage")]
824impl From<crate::adapters::alphavantage::models::EarningsCalendarEntryDTO>
825    for EarningsCalendarEntry
826{
827    fn from(d: crate::adapters::alphavantage::models::EarningsCalendarEntryDTO) -> Self {
828        Self {
829            symbol: d.symbol,
830            name: d.name,
831            report_date: d.report_date,
832            fiscal_date_ending: d.fiscal_date_ending,
833            estimate: d.estimate,
834            currency: d.currency,
835        }
836    }
837}
838
839/// An upcoming IPO calendar entry.
840#[cfg(feature = "alphavantage")]
841#[non_exhaustive]
842#[derive(Debug, Clone, Serialize, Deserialize)]
843pub struct IpoCalendarEntry {
844    /// Ticker symbol.
845    pub symbol: Option<String>,
846    /// Company name.
847    pub name: Option<String>,
848    /// IPO date.
849    pub ipo_date: Option<String>,
850    /// Price range (e.g., `"$15-$17"`).
851    pub price_range: Option<String>,
852    /// Exchange.
853    pub exchange: Option<String>,
854}
855
856#[cfg(feature = "alphavantage")]
857impl From<crate::adapters::alphavantage::models::IpoCalendarEntryDTO> for IpoCalendarEntry {
858    fn from(d: crate::adapters::alphavantage::models::IpoCalendarEntryDTO) -> Self {
859        Self {
860            symbol: d.symbol,
861            name: d.name,
862            ipo_date: d.ipo_date,
863            price_range: d.price_range,
864            exchange: d.exchange,
865        }
866    }
867}
868
869/// Fetch the upcoming earnings calendar (market-wide, not symbol-filtered).
870#[cfg(feature = "alphavantage")]
871pub async fn earnings_calendar() -> Result<Vec<EarningsCalendarEntry>> {
872    crate::adapters::alphavantage::fundamentals::earnings_calendar()
873        .await
874        .map(|v| v.into_iter().map(Into::into).collect())
875}
876
877/// Fetch the upcoming IPO calendar (market-wide, not symbol-filtered).
878#[cfg(feature = "alphavantage")]
879pub async fn ipo_calendar() -> Result<Vec<IpoCalendarEntry>> {
880    crate::adapters::alphavantage::fundamentals::ipo_calendar()
881        .await
882        .map(|v| v.into_iter().map(Into::into).collect())
883}