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}