Skip to main content

finance_query/models/quote/
response.rs

1//! Quote Summary Response
2//!
3//! Handles parsing of Yahoo Finance quoteSummary API responses
4
5use crate::error::{FinanceError, Result};
6use crate::models::quote::*;
7use serde_json::Value;
8
9/// Response from the quoteSummary endpoint
10///
11/// Deserializes all requested modules once on construction to avoid repeated
12/// JSON parsing on every accessor call. Uses `Option<T>` for each module since
13/// Yahoo Finance may not return all modules for all symbols.
14///
15/// The return type of [`QuoteProvider::fetch_quote`](crate::QuoteProvider),
16/// so an implementor populates the modules it can serve and leaves the rest
17/// `None`. Each field mirrors one Yahoo `quoteSummary` module.
18#[derive(Debug, Clone, Default)]
19#[non_exhaustive]
20pub struct QuoteSummaryResponse {
21    /// The symbol this response is for
22    pub symbol: String,
23
24    /// Last price, change, and market state.
25    pub price: Option<Price>,
26    /// Day range, volume, yield, and valuation summary.
27    pub summary_detail: Option<SummaryDetail>,
28    /// Margins, returns, cash flow, and analyst targets.
29    pub financial_data: Option<FinancialData>,
30    /// Shares outstanding, float, beta, and per-share statistics.
31    pub default_key_statistics: Option<DefaultKeyStatistics>,
32    /// Company address, sector, industry, and officers.
33    pub asset_profile: Option<AssetProfile>,
34    /// Upcoming earnings and dividend dates.
35    pub calendar_events: Option<CalendarEvents>,
36    /// Quarterly and annual earnings history with estimates.
37    pub earnings: Option<Earnings>,
38    /// Analyst estimate trend by period.
39    pub earnings_trend: Option<EarningsTrend>,
40    /// Reported versus estimated EPS per quarter.
41    pub earnings_history: Option<EarningsHistory>,
42    /// Analyst buy, hold, and sell counts over time.
43    pub recommendation_trend: Option<RecommendationTrend>,
44    /// Insiders and their reported positions.
45    pub insider_holders: Option<InsiderHolders>,
46    /// Recent insider buys and sells.
47    pub insider_transactions: Option<InsiderTransactions>,
48    /// Institutional holders and position sizes.
49    pub institution_ownership: Option<InstitutionOwnership>,
50    /// Fund holders and position sizes.
51    pub fund_ownership: Option<FundOwnership>,
52    /// Insider and institutional ownership percentages.
53    pub major_holders_breakdown: Option<MajorHoldersBreakdown>,
54    /// Net insider share purchase totals.
55    pub net_share_purchase_activity: Option<NetSharePurchaseActivity>,
56    /// Instrument type, exchange, and naming metadata.
57    pub quote_type: Option<QuoteTypeData>,
58    /// Business description and contact details.
59    pub summary_profile: Option<SummaryProfile>,
60    /// Recent SEC filings with links.
61    pub sec_filings: Option<SecFilings>,
62    /// Analyst rating changes over time.
63    pub upgrade_downgrade_history: Option<UpgradeDowngradeHistory>,
64    /// Trailing and annual returns for a fund.
65    pub fund_performance: Option<FundPerformance>,
66    /// Fund category, family, and fee structure.
67    pub fund_profile: Option<FundProfile>,
68    /// A fund's largest positions and sector weights.
69    pub top_holdings: Option<TopHoldings>,
70    /// Index-level growth and valuation estimates.
71    pub index_trend: Option<IndexTrend>,
72    /// Industry-level growth and valuation estimates.
73    pub industry_trend: Option<IndustryTrend>,
74    /// Sector-level growth and valuation estimates.
75    pub sector_trend: Option<SectorTrend>,
76    /// Performance of the instrument against its peers.
77    pub equity_performance: Option<EquityPerformance>,
78}
79
80impl QuoteSummaryResponse {
81    /// Creates a QuoteSummaryResponse from raw JSON
82    ///
83    /// # Arguments
84    ///
85    /// * `json` - The raw JSON response from Yahoo Finance
86    /// * `symbol` - The stock symbol this response is for
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if:
91    /// - The response structure is invalid
92    /// - The symbol is not found in the response
93    /// - Required fields are missing
94    pub(crate) fn from_json(json: Value, symbol: &str) -> Result<Self> {
95        // Yahoo Finance response structure:
96        // {
97        //   "quoteSummary": {
98        //     "result": [
99        //       {
100        //         "price": { ... },
101        //         "summaryDetail": { ... },
102        //         ...
103        //       }
104        //     ],
105        //     "error": null
106        //   }
107        // }
108
109        let quote_summary =
110            json.get("quoteSummary")
111                .ok_or_else(|| FinanceError::ResponseStructureError {
112                    field: "quoteSummary".to_string(),
113                    context: "Missing quoteSummary field".to_string(),
114                })?;
115
116        // Check for errors
117        if let Some(error) = quote_summary.get("error")
118            && !error.is_null()
119        {
120            return Err(FinanceError::ApiError(format!("API error: {}", error)));
121        }
122
123        let result = quote_summary
124            .get("result")
125            .and_then(|r| r.as_array())
126            .ok_or_else(|| FinanceError::ResponseStructureError {
127                field: "result".to_string(),
128                context: "Missing or invalid result field".to_string(),
129            })?;
130
131        if result.is_empty() {
132            return Err(FinanceError::ApiError(format!(
133                "No data found for symbol: {}",
134                symbol
135            )));
136        }
137
138        let data = &result[0];
139
140        // Helper macro to deserialize a module, returning None on missing/error
141        macro_rules! deserialize_module {
142            ($name:expr) => {
143                data.get($name)
144                    .and_then(|v| serde_json::from_value(v.clone()).ok())
145            };
146        }
147
148        Ok(Self {
149            symbol: symbol.to_string(),
150            price: deserialize_module!("price"),
151            summary_detail: deserialize_module!("summaryDetail"),
152            financial_data: deserialize_module!("financialData"),
153            default_key_statistics: deserialize_module!("defaultKeyStatistics"),
154            asset_profile: deserialize_module!("assetProfile"),
155            calendar_events: deserialize_module!("calendarEvents"),
156            earnings: deserialize_module!("earnings"),
157            earnings_trend: deserialize_module!("earningsTrend"),
158            earnings_history: deserialize_module!("earningsHistory"),
159            recommendation_trend: deserialize_module!("recommendationTrend"),
160            insider_holders: deserialize_module!("insiderHolders"),
161            insider_transactions: deserialize_module!("insiderTransactions"),
162            institution_ownership: deserialize_module!("institutionOwnership"),
163            fund_ownership: deserialize_module!("fundOwnership"),
164            major_holders_breakdown: deserialize_module!("majorHoldersBreakdown"),
165            net_share_purchase_activity: deserialize_module!("netSharePurchaseActivity"),
166            quote_type: deserialize_module!("quoteType"),
167            summary_profile: deserialize_module!("summaryProfile"),
168            sec_filings: deserialize_module!("secFilings"),
169            upgrade_downgrade_history: deserialize_module!("upgradeDowngradeHistory"),
170            fund_performance: deserialize_module!("fundPerformance"),
171            fund_profile: deserialize_module!("fundProfile"),
172            top_holdings: deserialize_module!("topHoldings"),
173            index_trend: deserialize_module!("indexTrend"),
174            industry_trend: deserialize_module!("industryTrend"),
175            sector_trend: deserialize_module!("sectorTrend"),
176            equity_performance: deserialize_module!("equityPerformance"),
177        })
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use serde_json::json;
185
186    #[test]
187    fn test_from_json_valid() {
188        let json = json!({
189            "quoteSummary": {
190                "result": [
191                    {
192                        "price": {
193                            "regularMarketPrice": {
194                                "raw": 150.0,
195                                "fmt": "150.00"
196                            }
197                        },
198                        "summaryDetail": {
199                            "previousClose": {
200                                "raw": 149.0,
201                                "fmt": "149.00"
202                            }
203                        }
204                    }
205                ],
206                "error": null
207            }
208        });
209
210        let response = QuoteSummaryResponse::from_json(json, "AAPL").unwrap();
211        assert!(response.price.is_some());
212        assert!(response.summary_detail.is_some());
213    }
214
215    #[test]
216    fn test_from_json_error() {
217        let json = json!({
218            "quoteSummary": {
219                "result": [],
220                "error": null
221            }
222        });
223
224        let response = QuoteSummaryResponse::from_json(json, "INVALID");
225        assert!(response.is_err());
226    }
227}