finance_query/models/quote/
data.rs

1//! Quote module
2//!
3//! Contains the fully typed Quote struct for serialization and API responses.
4
5use serde::{Deserialize, Serialize};
6
7use super::{
8    AssetProfile, BalanceSheetHistory, BalanceSheetHistoryQuarterly, CalendarEvents,
9    CashflowStatementHistory, CashflowStatementHistoryQuarterly, DefaultKeyStatistics, Earnings,
10    EarningsHistory, EarningsTrend, EquityPerformance, FinancialData, FundOwnership,
11    FundPerformance, FundProfile, IncomeStatementHistory, IncomeStatementHistoryQuarterly,
12    IndexTrend, IndustryTrend, InsiderHolders, InsiderTransactions, InstitutionOwnership,
13    MajorHoldersBreakdown, NetSharePurchaseActivity, Price, QuoteSummaryResponse, QuoteTypeData,
14    RecommendationTrend, SecFilings, SectorTrend, SummaryDetail, SummaryProfile, TopHoldings,
15    UpgradeDowngradeHistory,
16};
17
18/// Flattened quote data with deduplicated fields
19///
20/// This is the primary data structure for stock quotes. It flattens scalar fields
21/// from multiple Yahoo Finance modules while preserving complex nested objects.
22///
23/// # Creating Quote Instances
24///
25/// Quote instances can only be obtained through the Ticker API:
26/// ```no_run
27/// # use finance_query::Ticker;
28/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
29/// let ticker = Ticker::new("AAPL").await?;
30/// let quote = ticker.quote(true).await?;  // include_logo = true
31/// println!("Price: {:?}", quote.regular_market_price);
32/// # Ok(())
33/// # }
34/// ```
35///
36/// Note: This struct is marked `#[non_exhaustive]` and cannot be constructed manually.
37/// Use `Ticker::quote()` or `AsyncTicker::quote()` instead.
38///
39/// # Field Precedence
40///
41/// For duplicate fields across Yahoo Finance modules:
42/// - Price → SummaryDetail → DefaultKeyStatistics → FinancialData → AssetProfile
43///
44/// All fields are optional since Yahoo Finance may not return all data for every symbol.
45///
46/// # DataFrame Conversion
47///
48/// With the `dataframe` feature enabled, call `.to_dataframe()` to convert to a polars DataFrame:
49/// ```ignore
50/// let df = quote.to_dataframe()?;
51/// ```
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
54#[serde(rename_all = "camelCase")]
55#[non_exhaustive]
56pub struct Quote {
57    /// Stock symbol
58    pub symbol: String,
59
60    /// Company logo URL (50x50px)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub logo_url: Option<String>,
63
64    /// Alternative company logo URL (50x50px)
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub company_logo_url: Option<String>,
67
68    // ===== IDENTITY & METADATA =====
69    /// Short name of the security
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub short_name: Option<String>,
72
73    /// Long name of the security
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub long_name: Option<String>,
76
77    /// Exchange code (e.g., "NMS" for NASDAQ)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub exchange: Option<String>,
80
81    /// Exchange name (e.g., "NasdaqGS")
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub exchange_name: Option<String>,
84
85    /// Quote type (e.g., "EQUITY", "ETF", "MUTUALFUND")
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub quote_type: Option<String>,
88
89    /// Currency code (e.g., "USD")
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub currency: Option<String>,
92
93    /// Currency symbol (e.g., "$")
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub currency_symbol: Option<String>,
96
97    /// Underlying symbol (for derivatives/options)
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub underlying_symbol: Option<String>,
100
101    /// From currency (for forex pairs)
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub from_currency: Option<String>,
104
105    /// To currency (for forex pairs)
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub to_currency: Option<String>,
108
109    // ===== REAL-TIME PRICE DATA =====
110    /// Current regular market price
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub regular_market_price: Option<super::FormattedValue<f64>>,
113
114    /// Regular market change value
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub regular_market_change: Option<super::FormattedValue<f64>>,
117
118    /// Regular market change percentage
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub regular_market_change_percent: Option<super::FormattedValue<f64>>,
121
122    /// Regular market time as Unix timestamp
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub regular_market_time: Option<i64>,
125
126    /// Regular market day high
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub regular_market_day_high: Option<super::FormattedValue<f64>>,
129
130    /// Regular market day low
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub regular_market_day_low: Option<super::FormattedValue<f64>>,
133
134    /// Regular market open price
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub regular_market_open: Option<super::FormattedValue<f64>>,
137
138    /// Regular market previous close
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub regular_market_previous_close: Option<super::FormattedValue<f64>>,
141
142    /// Regular market volume
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub regular_market_volume: Option<super::FormattedValue<i64>>,
145
146    /// Current market state (e.g., "REGULAR", "POST", "PRE")
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub market_state: Option<String>,
149
150    // ===== ALTERNATIVE TRADING METRICS (from summaryDetail) =====
151    /// Day's high price
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub day_high: Option<super::FormattedValue<f64>>,
154
155    /// Day's low price
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub day_low: Option<super::FormattedValue<f64>>,
158
159    /// Opening price
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub open: Option<super::FormattedValue<f64>>,
162
163    /// Previous close price
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub previous_close: Option<super::FormattedValue<f64>>,
166
167    /// Trading volume
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub volume: Option<super::FormattedValue<i64>>,
170
171    // ===== PRICE HISTORY =====
172    /// All-time high price
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub all_time_high: Option<super::FormattedValue<f64>>,
175
176    /// All-time low price
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub all_time_low: Option<super::FormattedValue<f64>>,
179
180    // ===== PRE/POST MARKET DATA =====
181    /// Pre-market price
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub pre_market_price: Option<super::FormattedValue<f64>>,
184
185    /// Pre-market change value
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub pre_market_change: Option<super::FormattedValue<f64>>,
188
189    /// Pre-market change percentage
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub pre_market_change_percent: Option<super::FormattedValue<f64>>,
192
193    /// Pre-market time as Unix timestamp
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub pre_market_time: Option<i64>,
196
197    /// Post-market price
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub post_market_price: Option<super::FormattedValue<f64>>,
200
201    /// Post-market change value
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub post_market_change: Option<super::FormattedValue<f64>>,
204
205    /// Post-market change percentage
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub post_market_change_percent: Option<super::FormattedValue<f64>>,
208
209    /// Post-market time as Unix timestamp
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub post_market_time: Option<i64>,
212
213    // ===== VOLUME DATA =====
214    /// Average daily volume over 10 days
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub average_daily_volume10_day: Option<super::FormattedValue<i64>>,
217
218    /// Average daily volume over 3 months
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub average_daily_volume3_month: Option<super::FormattedValue<i64>>,
221
222    /// Average trading volume
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub average_volume: Option<super::FormattedValue<i64>>,
225
226    /// Average trading volume (10 days)
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub average_volume10days: Option<super::FormattedValue<i64>>,
229
230    // ===== VALUATION METRICS =====
231    /// Market capitalization
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub market_cap: Option<super::FormattedValue<i64>>,
234
235    /// Total enterprise value
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub enterprise_value: Option<super::FormattedValue<i64>>,
238
239    /// Enterprise value to revenue ratio
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub enterprise_to_revenue: Option<super::FormattedValue<f64>>,
242
243    /// Enterprise value to EBITDA ratio
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub enterprise_to_ebitda: Option<super::FormattedValue<f64>>,
246
247    /// Price to book value ratio
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub price_to_book: Option<super::FormattedValue<f64>>,
250
251    /// Price to sales ratio (trailing 12 months)
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub price_to_sales_trailing12_months: Option<super::FormattedValue<f64>>,
254
255    // ===== PE RATIOS =====
256    /// Forward price-to-earnings ratio
257    #[serde(rename = "forwardPE", skip_serializing_if = "Option::is_none")]
258    pub forward_pe: Option<super::FormattedValue<f64>>,
259
260    /// Trailing price-to-earnings ratio
261    #[serde(rename = "trailingPE", skip_serializing_if = "Option::is_none")]
262    pub trailing_pe: Option<super::FormattedValue<f64>>,
263
264    // ===== RISK METRICS =====
265    /// Beta coefficient (volatility vs market)
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub beta: Option<super::FormattedValue<f64>>,
268
269    // ===== 52-WEEK RANGE & MOVING AVERAGES =====
270    /// 52-week high price
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub fifty_two_week_high: Option<super::FormattedValue<f64>>,
273
274    /// 52-week low price
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub fifty_two_week_low: Option<super::FormattedValue<f64>>,
277
278    /// 50-day moving average
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub fifty_day_average: Option<super::FormattedValue<f64>>,
281
282    /// 200-day moving average
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub two_hundred_day_average: Option<super::FormattedValue<f64>>,
285
286    /// 52-week price change percentage
287    #[serde(rename = "52WeekChange", skip_serializing_if = "Option::is_none")]
288    pub week_52_change: Option<super::FormattedValue<f64>>,
289
290    /// S&P 500 52-week change percentage
291    #[serde(rename = "SandP52WeekChange", skip_serializing_if = "Option::is_none")]
292    pub sand_p_52_week_change: Option<super::FormattedValue<f64>>,
293
294    // ===== DIVIDENDS =====
295    /// Annual dividend rate
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub dividend_rate: Option<super::FormattedValue<f64>>,
298
299    /// Dividend yield percentage
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub dividend_yield: Option<super::FormattedValue<f64>>,
302
303    /// Trailing annual dividend rate
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub trailing_annual_dividend_rate: Option<super::FormattedValue<f64>>,
306
307    /// Trailing annual dividend yield
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub trailing_annual_dividend_yield: Option<super::FormattedValue<f64>>,
310
311    /// 5-year average dividend yield
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub five_year_avg_dividend_yield: Option<super::FormattedValue<f64>>,
314
315    /// Ex-dividend date
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub ex_dividend_date: Option<super::FormattedValue<i64>>,
318
319    /// Dividend payout ratio
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub payout_ratio: Option<super::FormattedValue<f64>>,
322
323    /// Last dividend value
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub last_dividend_value: Option<super::FormattedValue<f64>>,
326
327    /// Last dividend date
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub last_dividend_date: Option<super::FormattedValue<i64>>,
330
331    // ===== BID/ASK =====
332    /// Current bid price
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub bid: Option<super::FormattedValue<f64>>,
335
336    /// Bid size (shares)
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub bid_size: Option<super::FormattedValue<i64>>,
339
340    /// Current ask price
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub ask: Option<super::FormattedValue<f64>>,
343
344    /// Ask size (shares)
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub ask_size: Option<super::FormattedValue<i64>>,
347
348    // ===== SHARES & OWNERSHIP =====
349    /// Number of shares outstanding
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub shares_outstanding: Option<super::FormattedValue<i64>>,
352
353    /// Number of floating shares
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub float_shares: Option<super::FormattedValue<i64>>,
356
357    /// Implied shares outstanding
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub implied_shares_outstanding: Option<super::FormattedValue<i64>>,
360
361    /// Percentage of shares held by insiders
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub held_percent_insiders: Option<super::FormattedValue<f64>>,
364
365    /// Percentage of shares held by institutions
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub held_percent_institutions: Option<super::FormattedValue<f64>>,
368
369    /// Number of shares short
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub shares_short: Option<super::FormattedValue<i64>>,
372
373    /// Number of shares short (prior month)
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub shares_short_prior_month: Option<super::FormattedValue<i64>>,
376
377    /// Short ratio (days to cover)
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub short_ratio: Option<super::FormattedValue<f64>>,
380
381    /// Short interest as percentage of float
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub short_percent_of_float: Option<super::FormattedValue<f64>>,
384
385    /// Short interest as percentage of shares outstanding
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub shares_percent_shares_out: Option<super::FormattedValue<f64>>,
388
389    /// Date of short interest data
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub date_short_interest: Option<super::FormattedValue<i64>>,
392
393    // ===== FINANCIAL METRICS =====
394    /// Current stock price (from financial data)
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub current_price: Option<super::FormattedValue<f64>>,
397
398    /// Highest analyst price target
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub target_high_price: Option<super::FormattedValue<f64>>,
401
402    /// Lowest analyst price target
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub target_low_price: Option<super::FormattedValue<f64>>,
405
406    /// Mean analyst price target
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub target_mean_price: Option<super::FormattedValue<f64>>,
409
410    /// Median analyst price target
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub target_median_price: Option<super::FormattedValue<f64>>,
413
414    /// Mean analyst recommendation (1.0 = strong buy, 5.0 = sell)
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub recommendation_mean: Option<super::FormattedValue<f64>>,
417
418    /// Recommendation key (e.g., "buy", "hold", "sell")
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub recommendation_key: Option<String>,
421
422    /// Number of analyst opinions
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub number_of_analyst_opinions: Option<super::FormattedValue<i64>>,
425
426    /// Total cash and cash equivalents
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub total_cash: Option<super::FormattedValue<i64>>,
429
430    /// Total cash per share
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub total_cash_per_share: Option<super::FormattedValue<f64>>,
433
434    /// EBITDA (Earnings Before Interest, Taxes, Depreciation, and Amortization)
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub ebitda: Option<super::FormattedValue<i64>>,
437
438    /// Total debt
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub total_debt: Option<super::FormattedValue<i64>>,
441
442    /// Total revenue
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub total_revenue: Option<super::FormattedValue<i64>>,
445
446    /// Net income to common shareholders
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub net_income_to_common: Option<super::FormattedValue<i64>>,
449
450    /// Debt to equity ratio
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub debt_to_equity: Option<super::FormattedValue<f64>>,
453
454    /// Revenue per share
455    #[serde(skip_serializing_if = "Option::is_none")]
456    pub revenue_per_share: Option<super::FormattedValue<f64>>,
457
458    /// Return on assets (ROA)
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub return_on_assets: Option<super::FormattedValue<f64>>,
461
462    /// Return on equity (ROE)
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub return_on_equity: Option<super::FormattedValue<f64>>,
465
466    /// Free cash flow
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub free_cashflow: Option<super::FormattedValue<i64>>,
469
470    /// Operating cash flow
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub operating_cashflow: Option<super::FormattedValue<i64>>,
473
474    // ===== MARGINS =====
475    /// Profit margins
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub profit_margins: Option<super::FormattedValue<f64>>,
478
479    /// Gross profit margins
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub gross_margins: Option<super::FormattedValue<f64>>,
482
483    /// EBITDA margins
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub ebitda_margins: Option<super::FormattedValue<f64>>,
486
487    /// Operating margins
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub operating_margins: Option<super::FormattedValue<f64>>,
490
491    /// Total gross profits
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub gross_profits: Option<super::FormattedValue<i64>>,
494
495    // ===== GROWTH RATES =====
496    /// Earnings growth rate
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub earnings_growth: Option<super::FormattedValue<f64>>,
499
500    /// Revenue growth rate
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub revenue_growth: Option<super::FormattedValue<f64>>,
503
504    /// Quarterly earnings growth rate
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub earnings_quarterly_growth: Option<super::FormattedValue<f64>>,
507
508    // ===== RATIOS =====
509    /// Current ratio (current assets / current liabilities)
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub current_ratio: Option<super::FormattedValue<f64>>,
512
513    /// Quick ratio (quick assets / current liabilities)
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub quick_ratio: Option<super::FormattedValue<f64>>,
516
517    // ===== EPS & BOOK VALUE =====
518    /// Trailing earnings per share
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub trailing_eps: Option<super::FormattedValue<f64>>,
521
522    /// Forward earnings per share
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub forward_eps: Option<super::FormattedValue<f64>>,
525
526    /// Book value per share
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub book_value: Option<super::FormattedValue<f64>>,
529
530    // ===== COMPANY PROFILE =====
531    /// Sector
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub sector: Option<String>,
534
535    /// Sector key (machine-readable)
536    #[serde(skip_serializing_if = "Option::is_none")]
537    pub sector_key: Option<String>,
538
539    /// Sector display name
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub sector_disp: Option<String>,
542
543    /// Industry
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub industry: Option<String>,
546
547    /// Industry key (machine-readable)
548    #[serde(skip_serializing_if = "Option::is_none")]
549    pub industry_key: Option<String>,
550
551    /// Industry display name
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub industry_disp: Option<String>,
554
555    /// Long business summary
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub long_business_summary: Option<String>,
558
559    /// Company website
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub website: Option<String>,
562
563    /// Investor relations website
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub ir_website: Option<String>,
566
567    /// Street address line 1
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub address1: Option<String>,
570
571    /// City
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub city: Option<String>,
574
575    /// State or province
576    #[serde(skip_serializing_if = "Option::is_none")]
577    pub state: Option<String>,
578
579    /// Postal/ZIP code
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub zip: Option<String>,
582
583    /// Country
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub country: Option<String>,
586
587    /// Phone number
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub phone: Option<String>,
590
591    /// Number of full-time employees
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub full_time_employees: Option<i64>,
594
595    /// Fund category (for mutual funds/ETFs)
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub category: Option<String>,
598
599    /// Fund family name
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub fund_family: Option<String>,
602
603    // ===== RISK SCORES =====
604    /// Audit risk score
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub audit_risk: Option<i32>,
607
608    /// Board risk score
609    #[serde(skip_serializing_if = "Option::is_none")]
610    pub board_risk: Option<i32>,
611
612    /// Compensation risk score
613    #[serde(skip_serializing_if = "Option::is_none")]
614    pub compensation_risk: Option<i32>,
615
616    /// Shareholder rights risk score
617    #[serde(skip_serializing_if = "Option::is_none")]
618    pub shareholder_rights_risk: Option<i32>,
619
620    /// Overall risk score
621    #[serde(skip_serializing_if = "Option::is_none")]
622    pub overall_risk: Option<i32>,
623
624    // ===== TIMEZONE & EXCHANGE =====
625    /// Full timezone name
626    #[serde(skip_serializing_if = "Option::is_none")]
627    pub time_zone_full_name: Option<String>,
628
629    /// Short timezone name
630    #[serde(skip_serializing_if = "Option::is_none")]
631    pub time_zone_short_name: Option<String>,
632
633    /// GMT offset in milliseconds
634    #[serde(skip_serializing_if = "Option::is_none")]
635    pub gmt_off_set_milliseconds: Option<i64>,
636
637    /// First trade date (Unix epoch UTC)
638    #[serde(skip_serializing_if = "Option::is_none")]
639    pub first_trade_date_epoch_utc: Option<i64>,
640
641    /// Message board ID
642    #[serde(skip_serializing_if = "Option::is_none")]
643    pub message_board_id: Option<String>,
644
645    /// Exchange data delay in seconds
646    #[serde(skip_serializing_if = "Option::is_none")]
647    pub exchange_data_delayed_by: Option<i32>,
648
649    // ===== FUND-SPECIFIC =====
650    /// Net asset value price (for funds)
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub nav_price: Option<super::FormattedValue<f64>>,
653
654    /// Total assets (for funds)
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub total_assets: Option<super::FormattedValue<i64>>,
657
658    /// Yield (for bonds/funds)
659    #[serde(rename = "yield", skip_serializing_if = "Option::is_none")]
660    pub yield_value: Option<super::FormattedValue<f64>>,
661
662    // ===== STOCK SPLITS & DATES =====
663    /// Last stock split factor
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub last_split_factor: Option<String>,
666
667    /// Last stock split date
668    #[serde(skip_serializing_if = "Option::is_none")]
669    pub last_split_date: Option<super::FormattedValue<i64>>,
670
671    /// Last fiscal year end date
672    #[serde(skip_serializing_if = "Option::is_none")]
673    pub last_fiscal_year_end: Option<super::FormattedValue<i64>>,
674
675    /// Next fiscal year end date
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub next_fiscal_year_end: Option<super::FormattedValue<i64>>,
678
679    /// Most recent quarter date
680    #[serde(skip_serializing_if = "Option::is_none")]
681    pub most_recent_quarter: Option<super::FormattedValue<i64>>,
682
683    // ===== MISC =====
684    /// Price hint for decimal places
685    #[serde(skip_serializing_if = "Option::is_none")]
686    pub price_hint: Option<super::FormattedValue<i64>>,
687
688    /// Whether the security is tradeable
689    #[serde(skip_serializing_if = "Option::is_none")]
690    pub tradeable: Option<bool>,
691
692    /// Currency code for financial data
693    #[serde(skip_serializing_if = "Option::is_none")]
694    pub financial_currency: Option<String>,
695
696    // ===== PRESERVED NESTED OBJECTS =====
697    /// Company officers (executives and compensation)
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub company_officers: Option<Vec<super::CompanyOfficer>>,
700
701    /// Earnings data (quarterly earnings vs estimates, revenue/earnings history)
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub earnings: Option<Earnings>,
704
705    /// Calendar events (upcoming earnings dates, dividend dates)
706    #[serde(skip_serializing_if = "Option::is_none")]
707    pub calendar_events: Option<CalendarEvents>,
708
709    /// Analyst recommendation trends over time
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub recommendation_trend: Option<RecommendationTrend>,
712
713    /// Analyst upgrades/downgrades history
714    #[serde(skip_serializing_if = "Option::is_none")]
715    pub upgrade_downgrade_history: Option<UpgradeDowngradeHistory>,
716
717    /// Historical earnings data (actual vs estimate)
718    #[serde(skip_serializing_if = "Option::is_none")]
719    pub earnings_history: Option<EarningsHistory>,
720
721    /// Earnings trend data (estimates and revisions)
722    #[serde(skip_serializing_if = "Option::is_none")]
723    pub earnings_trend: Option<EarningsTrend>,
724
725    /// Insider stock holdings
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub insider_holders: Option<InsiderHolders>,
728
729    /// Insider transactions
730    #[serde(skip_serializing_if = "Option::is_none")]
731    pub insider_transactions: Option<InsiderTransactions>,
732
733    /// Top institutional owners
734    #[serde(skip_serializing_if = "Option::is_none")]
735    pub institution_ownership: Option<InstitutionOwnership>,
736
737    /// Top fund owners
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub fund_ownership: Option<FundOwnership>,
740
741    /// Major holders breakdown (insiders, institutions, etc.)
742    #[serde(skip_serializing_if = "Option::is_none")]
743    pub major_holders_breakdown: Option<MajorHoldersBreakdown>,
744
745    /// Net share purchase activity by insiders
746    #[serde(skip_serializing_if = "Option::is_none")]
747    pub net_share_purchase_activity: Option<NetSharePurchaseActivity>,
748
749    /// SEC filings
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub sec_filings: Option<SecFilings>,
752
753    /// Balance sheet history (annual)
754    #[serde(skip_serializing_if = "Option::is_none")]
755    pub balance_sheet_history: Option<BalanceSheetHistory>,
756
757    /// Balance sheet history (quarterly)
758    #[serde(skip_serializing_if = "Option::is_none")]
759    pub balance_sheet_history_quarterly: Option<BalanceSheetHistoryQuarterly>,
760
761    /// Cash flow statement history (annual)
762    #[serde(skip_serializing_if = "Option::is_none")]
763    pub cashflow_statement_history: Option<CashflowStatementHistory>,
764
765    /// Cash flow statement history (quarterly)
766    #[serde(skip_serializing_if = "Option::is_none")]
767    pub cashflow_statement_history_quarterly: Option<CashflowStatementHistoryQuarterly>,
768
769    /// Income statement history (annual)
770    #[serde(skip_serializing_if = "Option::is_none")]
771    pub income_statement_history: Option<IncomeStatementHistory>,
772
773    /// Income statement history (quarterly)
774    #[serde(skip_serializing_if = "Option::is_none")]
775    pub income_statement_history_quarterly: Option<IncomeStatementHistoryQuarterly>,
776
777    /// Equity performance (returns vs benchmark across multiple time periods)
778    #[serde(skip_serializing_if = "Option::is_none")]
779    pub equity_performance: Option<EquityPerformance>,
780
781    /// Index trend (PE and PEG ratios)
782    #[serde(skip_serializing_if = "Option::is_none")]
783    pub index_trend: Option<IndexTrend>,
784
785    /// Industry trend
786    #[serde(skip_serializing_if = "Option::is_none")]
787    pub industry_trend: Option<IndustryTrend>,
788
789    /// Sector trend
790    #[serde(skip_serializing_if = "Option::is_none")]
791    pub sector_trend: Option<SectorTrend>,
792
793    /// Fund profile (for ETFs and mutual funds)
794    #[serde(skip_serializing_if = "Option::is_none")]
795    pub fund_profile: Option<FundProfile>,
796
797    /// Fund performance data (for ETFs and mutual funds)
798    #[serde(skip_serializing_if = "Option::is_none")]
799    pub fund_performance: Option<FundPerformance>,
800
801    /// Top holdings and sector weightings (for ETFs and mutual funds)
802    #[serde(skip_serializing_if = "Option::is_none")]
803    pub top_holdings: Option<TopHoldings>,
804}
805
806impl Quote {
807    /// Creates a Quote from a QuoteSummaryResponse
808    ///
809    /// Extracts and flattens all typed modules from the raw response.
810    /// Field precedence for duplicates: Price → SummaryDetail → KeyStats → FinancialData → AssetProfile
811    ///
812    /// # Arguments
813    ///
814    /// * `response` - The quote summary response from Yahoo Finance
815    /// * `logo_url` - Optional company logo URL (fetched separately from /v7/finance/quote)
816    /// * `company_logo_url` - Optional alternative company logo URL (fetched separately from /v7/finance/quote)
817    pub(crate) fn from_response(
818        response: &QuoteSummaryResponse,
819        logo_url: Option<String>,
820        company_logo_url: Option<String>,
821    ) -> Self {
822        // Deserialize all modules first
823        let price: Option<Price> = response.get_typed("price").ok();
824        let quote_type: Option<QuoteTypeData> = response.get_typed("quoteType").ok();
825        let summary_detail: Option<SummaryDetail> = response.get_typed("summaryDetail").ok();
826        let financial_data: Option<FinancialData> = response.get_typed("financialData").ok();
827        let key_stats: Option<DefaultKeyStatistics> =
828            response.get_typed("defaultKeyStatistics").ok();
829        let asset_profile: Option<AssetProfile> = response.get_typed("assetProfile").ok();
830        let summary_profile: Option<SummaryProfile> = response.get_typed("summaryProfile").ok();
831
832        Self {
833            symbol: response.symbol.clone(),
834            logo_url,
835            company_logo_url,
836
837            // ===== IDENTITY & METADATA =====
838            // Price priority, fallback to QuoteTypeData
839            short_name: price
840                .as_ref()
841                .and_then(|p| p.short_name.clone())
842                .or_else(|| quote_type.as_ref().and_then(|q| q.short_name.clone())),
843
844            long_name: price
845                .as_ref()
846                .and_then(|p| p.long_name.clone())
847                .or_else(|| quote_type.as_ref().and_then(|q| q.long_name.clone())),
848
849            exchange: price
850                .as_ref()
851                .and_then(|p| p.exchange.clone())
852                .or_else(|| quote_type.as_ref().and_then(|q| q.exchange.clone())),
853
854            exchange_name: price.as_ref().and_then(|p| p.exchange_name.clone()),
855
856            quote_type: price
857                .as_ref()
858                .and_then(|p| p.quote_type.clone())
859                .or_else(|| quote_type.as_ref().and_then(|q| q.quote_type.clone())),
860
861            currency: price.as_ref().and_then(|p| p.currency.clone()).or_else(|| {
862                summary_detail
863                    .as_ref()
864                    .and_then(|s| s.currency.clone())
865                    .or_else(|| {
866                        financial_data
867                            .as_ref()
868                            .and_then(|f| f.financial_currency.clone())
869                    })
870            }),
871
872            currency_symbol: price.as_ref().and_then(|p| p.currency_symbol.clone()),
873
874            underlying_symbol: price
875                .as_ref()
876                .and_then(|p| p.underlying_symbol.clone())
877                .or_else(|| {
878                    quote_type
879                        .as_ref()
880                        .and_then(|q| q.underlying_symbol.clone())
881                }),
882            from_currency: price
883                .as_ref()
884                .and_then(|p| p.from_currency.clone())
885                .or_else(|| {
886                    summary_detail
887                        .as_ref()
888                        .and_then(|s| s.from_currency.clone())
889                }),
890            to_currency: price
891                .as_ref()
892                .and_then(|p| p.to_currency.clone())
893                .or_else(|| summary_detail.as_ref().and_then(|s| s.to_currency.clone())),
894
895            // ===== REAL-TIME PRICE DATA (from Price only) =====
896            regular_market_price: price.as_ref().and_then(|p| p.regular_market_price.clone()),
897            regular_market_change: price.as_ref().and_then(|p| p.regular_market_change.clone()),
898            regular_market_change_percent: price
899                .as_ref()
900                .and_then(|p| p.regular_market_change_percent.clone()),
901            regular_market_time: price.as_ref().and_then(|p| p.regular_market_time),
902            regular_market_day_high: price
903                .as_ref()
904                .and_then(|p| p.regular_market_day_high.clone()),
905            regular_market_day_low: price
906                .as_ref()
907                .and_then(|p| p.regular_market_day_low.clone()),
908            regular_market_open: price.as_ref().and_then(|p| p.regular_market_open.clone()),
909            regular_market_previous_close: price
910                .as_ref()
911                .and_then(|p| p.regular_market_previous_close.clone()),
912            regular_market_volume: price.as_ref().and_then(|p| p.regular_market_volume.clone()),
913            market_state: price.as_ref().and_then(|p| p.market_state.clone()),
914
915            // ===== ALTERNATIVE TRADING METRICS (from summaryDetail) =====
916            day_high: summary_detail.as_ref().and_then(|s| s.day_high.clone()),
917            day_low: summary_detail.as_ref().and_then(|s| s.day_low.clone()),
918            open: summary_detail.as_ref().and_then(|s| s.open.clone()),
919            previous_close: summary_detail
920                .as_ref()
921                .and_then(|s| s.previous_close.clone()),
922            volume: summary_detail.as_ref().and_then(|s| s.volume.clone()),
923
924            // ===== PRICE HISTORY =====
925            all_time_high: summary_detail
926                .as_ref()
927                .and_then(|s| s.all_time_high.clone()),
928            all_time_low: summary_detail.as_ref().and_then(|s| s.all_time_low.clone()),
929
930            // ===== PRE/POST MARKET DATA =====
931            pre_market_price: price.as_ref().and_then(|p| p.pre_market_price.clone()),
932            pre_market_change: price.as_ref().and_then(|p| p.pre_market_change.clone()),
933            pre_market_change_percent: price
934                .as_ref()
935                .and_then(|p| p.pre_market_change_percent.clone()),
936            pre_market_time: price.as_ref().and_then(|p| p.pre_market_time),
937            post_market_price: price.as_ref().and_then(|p| p.post_market_price.clone()),
938            post_market_change: price.as_ref().and_then(|p| p.post_market_change.clone()),
939            post_market_change_percent: price
940                .as_ref()
941                .and_then(|p| p.post_market_change_percent.clone()),
942            post_market_time: price.as_ref().and_then(|p| p.post_market_time),
943
944            // ===== VOLUME DATA =====
945            // Price priority, fallback to SummaryDetail
946            average_daily_volume10_day: price
947                .as_ref()
948                .and_then(|p| p.average_daily_volume10_day.clone())
949                .or_else(|| {
950                    summary_detail
951                        .as_ref()
952                        .and_then(|s| s.average_daily_volume10_day.clone())
953                }),
954            average_daily_volume3_month: price
955                .as_ref()
956                .and_then(|p| p.average_daily_volume3_month.clone()),
957            average_volume: summary_detail
958                .as_ref()
959                .and_then(|s| s.average_volume.clone()),
960            average_volume10days: summary_detail
961                .as_ref()
962                .and_then(|s| s.average_volume10days.clone()),
963
964            // ===== VALUATION METRICS =====
965            // Price priority for market_cap (real-time)
966            market_cap: price.as_ref().and_then(|p| p.market_cap.clone()),
967            enterprise_value: key_stats.as_ref().and_then(|k| k.enterprise_value.clone()),
968            enterprise_to_revenue: key_stats
969                .as_ref()
970                .and_then(|k| k.enterprise_to_revenue.clone()),
971            enterprise_to_ebitda: key_stats
972                .as_ref()
973                .and_then(|k| k.enterprise_to_ebitda.clone()),
974            price_to_book: key_stats.as_ref().and_then(|k| k.price_to_book.clone()),
975            price_to_sales_trailing12_months: summary_detail
976                .as_ref()
977                .and_then(|s| s.price_to_sales_trailing12_months.clone()),
978
979            // ===== PE RATIOS =====
980            // SummaryDetail priority, fallback to KeyStats
981            forward_pe: summary_detail
982                .as_ref()
983                .and_then(|s| s.forward_pe.clone())
984                .or_else(|| key_stats.as_ref().and_then(|k| k.forward_pe.clone())),
985            trailing_pe: summary_detail.as_ref().and_then(|s| s.trailing_pe.clone()),
986
987            // ===== RISK METRICS =====
988            // SummaryDetail priority, fallback to KeyStats
989            beta: summary_detail
990                .as_ref()
991                .and_then(|s| s.beta.clone())
992                .or_else(|| key_stats.as_ref().and_then(|k| k.beta.clone())),
993
994            // ===== 52-WEEK RANGE & MOVING AVERAGES =====
995            fifty_two_week_high: summary_detail
996                .as_ref()
997                .and_then(|s| s.fifty_two_week_high.clone()),
998            fifty_two_week_low: summary_detail
999                .as_ref()
1000                .and_then(|s| s.fifty_two_week_low.clone()),
1001            fifty_day_average: summary_detail
1002                .as_ref()
1003                .and_then(|s| s.fifty_day_average.clone()),
1004            two_hundred_day_average: summary_detail
1005                .as_ref()
1006                .and_then(|s| s.two_hundred_day_average.clone()),
1007            week_52_change: key_stats.as_ref().and_then(|k| k.week_52_change.clone()),
1008            sand_p_52_week_change: key_stats
1009                .as_ref()
1010                .and_then(|k| k.sand_p_52_week_change.clone()),
1011
1012            // ===== DIVIDENDS =====
1013            dividend_rate: summary_detail
1014                .as_ref()
1015                .and_then(|s| s.dividend_rate.clone()),
1016            dividend_yield: summary_detail
1017                .as_ref()
1018                .and_then(|s| s.dividend_yield.clone()),
1019            trailing_annual_dividend_rate: summary_detail
1020                .as_ref()
1021                .and_then(|s| s.trailing_annual_dividend_rate.clone()),
1022            trailing_annual_dividend_yield: summary_detail
1023                .as_ref()
1024                .and_then(|s| s.trailing_annual_dividend_yield.clone()),
1025            five_year_avg_dividend_yield: summary_detail
1026                .as_ref()
1027                .and_then(|s| s.five_year_avg_dividend_yield.clone()),
1028            ex_dividend_date: summary_detail
1029                .as_ref()
1030                .and_then(|s| s.ex_dividend_date.clone()),
1031            payout_ratio: summary_detail.as_ref().and_then(|s| s.payout_ratio.clone()),
1032            last_dividend_value: key_stats
1033                .as_ref()
1034                .and_then(|k| k.last_dividend_value.clone()),
1035            last_dividend_date: key_stats
1036                .as_ref()
1037                .and_then(|k| k.last_dividend_date.clone()),
1038
1039            // ===== BID/ASK =====
1040            bid: summary_detail.as_ref().and_then(|s| s.bid.clone()),
1041            bid_size: summary_detail.as_ref().and_then(|s| s.bid_size.clone()),
1042            ask: summary_detail.as_ref().and_then(|s| s.ask.clone()),
1043            ask_size: summary_detail.as_ref().and_then(|s| s.ask_size.clone()),
1044
1045            // ===== SHARES & OWNERSHIP =====
1046            shares_outstanding: key_stats
1047                .as_ref()
1048                .and_then(|k| k.shares_outstanding.clone()),
1049            float_shares: key_stats.as_ref().and_then(|k| k.float_shares.clone()),
1050            implied_shares_outstanding: key_stats
1051                .as_ref()
1052                .and_then(|k| k.implied_shares_outstanding.clone()),
1053            held_percent_insiders: key_stats
1054                .as_ref()
1055                .and_then(|k| k.held_percent_insiders.clone()),
1056            held_percent_institutions: key_stats
1057                .as_ref()
1058                .and_then(|k| k.held_percent_institutions.clone()),
1059            shares_short: key_stats.as_ref().and_then(|k| k.shares_short.clone()),
1060            shares_short_prior_month: key_stats
1061                .as_ref()
1062                .and_then(|k| k.shares_short_prior_month.clone()),
1063            short_ratio: key_stats.as_ref().and_then(|k| k.short_ratio.clone()),
1064            short_percent_of_float: key_stats
1065                .as_ref()
1066                .and_then(|k| k.short_percent_of_float.clone()),
1067            shares_percent_shares_out: key_stats
1068                .as_ref()
1069                .and_then(|k| k.shares_percent_shares_out.clone()),
1070            date_short_interest: key_stats
1071                .as_ref()
1072                .and_then(|k| k.date_short_interest.clone()),
1073
1074            // ===== FINANCIAL METRICS =====
1075            current_price: financial_data
1076                .as_ref()
1077                .and_then(|f| f.current_price.clone()),
1078            target_high_price: financial_data
1079                .as_ref()
1080                .and_then(|f| f.target_high_price.clone()),
1081            target_low_price: financial_data
1082                .as_ref()
1083                .and_then(|f| f.target_low_price.clone()),
1084            target_mean_price: financial_data
1085                .as_ref()
1086                .and_then(|f| f.target_mean_price.clone()),
1087            target_median_price: financial_data
1088                .as_ref()
1089                .and_then(|f| f.target_median_price.clone()),
1090            recommendation_mean: financial_data
1091                .as_ref()
1092                .and_then(|f| f.recommendation_mean.clone()),
1093            recommendation_key: financial_data
1094                .as_ref()
1095                .and_then(|f| f.recommendation_key.clone()),
1096            number_of_analyst_opinions: financial_data
1097                .as_ref()
1098                .and_then(|f| f.number_of_analyst_opinions.clone()),
1099            total_cash: financial_data.as_ref().and_then(|f| f.total_cash.clone()),
1100            total_cash_per_share: financial_data
1101                .as_ref()
1102                .and_then(|f| f.total_cash_per_share.clone()),
1103            ebitda: financial_data.as_ref().and_then(|f| f.ebitda.clone()),
1104            total_debt: financial_data.as_ref().and_then(|f| f.total_debt.clone()),
1105            total_revenue: financial_data
1106                .as_ref()
1107                .and_then(|f| f.total_revenue.clone()),
1108            net_income_to_common: key_stats
1109                .as_ref()
1110                .and_then(|k| k.net_income_to_common.clone()),
1111            debt_to_equity: financial_data
1112                .as_ref()
1113                .and_then(|f| f.debt_to_equity.clone()),
1114            revenue_per_share: financial_data
1115                .as_ref()
1116                .and_then(|f| f.revenue_per_share.clone()),
1117            return_on_assets: financial_data
1118                .as_ref()
1119                .and_then(|f| f.return_on_assets.clone()),
1120            return_on_equity: financial_data
1121                .as_ref()
1122                .and_then(|f| f.return_on_equity.clone()),
1123            free_cashflow: financial_data
1124                .as_ref()
1125                .and_then(|f| f.free_cashflow.clone()),
1126            operating_cashflow: financial_data
1127                .as_ref()
1128                .and_then(|f| f.operating_cashflow.clone()),
1129
1130            // ===== MARGINS =====
1131            // FinancialData priority
1132            profit_margins: financial_data
1133                .as_ref()
1134                .and_then(|f| f.profit_margins.clone()),
1135            gross_margins: financial_data
1136                .as_ref()
1137                .and_then(|f| f.gross_margins.clone()),
1138            ebitda_margins: financial_data
1139                .as_ref()
1140                .and_then(|f| f.ebitda_margins.clone()),
1141            operating_margins: financial_data
1142                .as_ref()
1143                .and_then(|f| f.operating_margins.clone()),
1144            gross_profits: financial_data
1145                .as_ref()
1146                .and_then(|f| f.gross_profits.clone()),
1147
1148            // ===== GROWTH RATES =====
1149            earnings_growth: financial_data
1150                .as_ref()
1151                .and_then(|f| f.earnings_growth.clone()),
1152            revenue_growth: financial_data
1153                .as_ref()
1154                .and_then(|f| f.revenue_growth.clone()),
1155            earnings_quarterly_growth: key_stats
1156                .as_ref()
1157                .and_then(|k| k.earnings_quarterly_growth.clone()),
1158
1159            // ===== RATIOS =====
1160            current_ratio: financial_data
1161                .as_ref()
1162                .and_then(|f| f.current_ratio.clone()),
1163            quick_ratio: financial_data.as_ref().and_then(|f| f.quick_ratio.clone()),
1164
1165            // ===== EPS & BOOK VALUE =====
1166            trailing_eps: key_stats.as_ref().and_then(|k| k.trailing_eps.clone()),
1167            forward_eps: key_stats.as_ref().and_then(|k| k.forward_eps.clone()),
1168            book_value: key_stats.as_ref().and_then(|k| k.book_value.clone()),
1169
1170            // ===== COMPANY PROFILE =====
1171            // AssetProfile priority, fallback to SummaryProfile
1172            sector: asset_profile
1173                .as_ref()
1174                .and_then(|a| a.sector.clone())
1175                .or_else(|| summary_profile.as_ref().and_then(|s| s.sector.clone())),
1176            sector_key: asset_profile.as_ref().and_then(|a| a.sector_key.clone()),
1177            sector_disp: asset_profile.as_ref().and_then(|a| a.sector_disp.clone()),
1178            industry: asset_profile
1179                .as_ref()
1180                .and_then(|a| a.industry.clone())
1181                .or_else(|| summary_profile.as_ref().and_then(|s| s.industry.clone())),
1182            industry_key: asset_profile.as_ref().and_then(|a| a.industry_key.clone()),
1183            industry_disp: asset_profile.as_ref().and_then(|a| a.industry_disp.clone()),
1184            long_business_summary: asset_profile
1185                .as_ref()
1186                .and_then(|a| a.long_business_summary.clone())
1187                .or_else(|| {
1188                    summary_profile
1189                        .as_ref()
1190                        .and_then(|s| s.long_business_summary.clone())
1191                }),
1192            address1: asset_profile
1193                .as_ref()
1194                .and_then(|a| a.address1.clone())
1195                .or_else(|| summary_profile.as_ref().and_then(|s| s.address1.clone())),
1196            city: asset_profile
1197                .as_ref()
1198                .and_then(|a| a.city.clone())
1199                .or_else(|| summary_profile.as_ref().and_then(|s| s.city.clone())),
1200            state: asset_profile
1201                .as_ref()
1202                .and_then(|a| a.state.clone())
1203                .or_else(|| summary_profile.as_ref().and_then(|s| s.state.clone())),
1204            zip: asset_profile
1205                .as_ref()
1206                .and_then(|a| a.zip.clone())
1207                .or_else(|| summary_profile.as_ref().and_then(|s| s.zip.clone())),
1208            country: asset_profile
1209                .as_ref()
1210                .and_then(|a| a.country.clone())
1211                .or_else(|| summary_profile.as_ref().and_then(|s| s.country.clone())),
1212            phone: asset_profile
1213                .as_ref()
1214                .and_then(|a| a.phone.clone())
1215                .or_else(|| summary_profile.as_ref().and_then(|s| s.phone.clone())),
1216            full_time_employees: asset_profile
1217                .as_ref()
1218                .and_then(|a| a.full_time_employees)
1219                .or_else(|| summary_profile.as_ref().and_then(|s| s.full_time_employees)),
1220
1221            website: asset_profile
1222                .as_ref()
1223                .and_then(|a| a.website.clone())
1224                .or_else(|| summary_profile.as_ref().and_then(|s| s.website.clone())),
1225            ir_website: summary_profile.as_ref().and_then(|s| s.ir_website.clone()),
1226
1227            category: key_stats.as_ref().and_then(|k| k.category.clone()),
1228            fund_family: key_stats.as_ref().and_then(|k| k.fund_family.clone()),
1229
1230            // ===== RISK SCORES =====
1231            audit_risk: asset_profile.as_ref().and_then(|a| a.audit_risk),
1232            board_risk: asset_profile.as_ref().and_then(|a| a.board_risk),
1233            compensation_risk: asset_profile.as_ref().and_then(|a| a.compensation_risk),
1234            shareholder_rights_risk: asset_profile
1235                .as_ref()
1236                .and_then(|a| a.shareholder_rights_risk),
1237            overall_risk: asset_profile.as_ref().and_then(|a| a.overall_risk),
1238
1239            // ===== TIMEZONE & EXCHANGE =====
1240            time_zone_full_name: quote_type
1241                .as_ref()
1242                .and_then(|q| q.time_zone_full_name.clone()),
1243            time_zone_short_name: quote_type
1244                .as_ref()
1245                .and_then(|q| q.time_zone_short_name.clone()),
1246            gmt_off_set_milliseconds: quote_type.as_ref().and_then(|q| q.gmt_off_set_milliseconds),
1247            first_trade_date_epoch_utc: quote_type
1248                .as_ref()
1249                .and_then(|q| q.first_trade_date_epoch_utc),
1250            message_board_id: quote_type.as_ref().and_then(|q| q.message_board_id.clone()),
1251            exchange_data_delayed_by: price.as_ref().and_then(|p| p.exchange_data_delayed_by),
1252
1253            // ===== FUND-SPECIFIC =====
1254            nav_price: summary_detail.as_ref().and_then(|s| s.nav_price.clone()),
1255            total_assets: summary_detail.as_ref().and_then(|s| s.total_assets.clone()),
1256            yield_value: summary_detail.as_ref().and_then(|s| s.yield_value.clone()),
1257
1258            // ===== STOCK SPLITS & DATES =====
1259            last_split_factor: key_stats.as_ref().and_then(|k| k.last_split_factor.clone()),
1260            last_split_date: key_stats.as_ref().and_then(|k| k.last_split_date.clone()),
1261            last_fiscal_year_end: key_stats
1262                .as_ref()
1263                .and_then(|k| k.last_fiscal_year_end.clone()),
1264            next_fiscal_year_end: key_stats
1265                .as_ref()
1266                .and_then(|k| k.next_fiscal_year_end.clone()),
1267            most_recent_quarter: key_stats
1268                .as_ref()
1269                .and_then(|k| k.most_recent_quarter.clone()),
1270
1271            // ===== MISC =====
1272            // Price priority for price_hint
1273            price_hint: price.as_ref().and_then(|p| p.price_hint.clone()),
1274            tradeable: summary_detail.as_ref().and_then(|s| s.tradeable),
1275            financial_currency: financial_data
1276                .as_ref()
1277                .and_then(|f| f.financial_currency.clone()),
1278
1279            // ===== PRESERVED NESTED OBJECTS =====
1280            company_officers: asset_profile.as_ref().map(|a| a.company_officers.clone()),
1281            earnings: response.get_typed("earnings").ok(),
1282            calendar_events: response.get_typed("calendarEvents").ok(),
1283            recommendation_trend: response.get_typed("recommendationTrend").ok(),
1284            upgrade_downgrade_history: response.get_typed("upgradeDowngradeHistory").ok(),
1285            earnings_history: response.get_typed("earningsHistory").ok(),
1286            earnings_trend: response.get_typed("earningsTrend").ok(),
1287            insider_holders: response.get_typed("insiderHolders").ok(),
1288            insider_transactions: response.get_typed("insiderTransactions").ok(),
1289            institution_ownership: response.get_typed("institutionOwnership").ok(),
1290            fund_ownership: response.get_typed("fundOwnership").ok(),
1291            major_holders_breakdown: response.get_typed("majorHoldersBreakdown").ok(),
1292            net_share_purchase_activity: response.get_typed("netSharePurchaseActivity").ok(),
1293            sec_filings: response.get_typed("secFilings").ok(),
1294            balance_sheet_history: response.get_typed("balanceSheetHistory").ok(),
1295            balance_sheet_history_quarterly: response
1296                .get_typed("balanceSheetHistoryQuarterly")
1297                .ok(),
1298            cashflow_statement_history: response.get_typed("cashflowStatementHistory").ok(),
1299            cashflow_statement_history_quarterly: response
1300                .get_typed("cashflowStatementHistoryQuarterly")
1301                .ok(),
1302            income_statement_history: response.get_typed("incomeStatementHistory").ok(),
1303            income_statement_history_quarterly: response
1304                .get_typed("incomeStatementHistoryQuarterly")
1305                .ok(),
1306            equity_performance: response.get_typed("equityPerformance").ok(),
1307            index_trend: response.get_typed("indexTrend").ok(),
1308            industry_trend: response.get_typed("industryTrend").ok(),
1309            sector_trend: response.get_typed("sectorTrend").ok(),
1310            fund_profile: response.get_typed("fundProfile").ok(),
1311            fund_performance: response.get_typed("fundPerformance").ok(),
1312            top_holdings: response.get_typed("topHoldings").ok(),
1313        }
1314    }
1315
1316    /// Returns the most relevant current price based on market state
1317    ///
1318    /// Returns post-market price if in post-market, pre-market price if in pre-market,
1319    /// otherwise regular market price.
1320    pub fn live_price(&self) -> Option<f64> {
1321        if self.market_state.as_deref() == Some("POST") {
1322            self.post_market_price
1323                .as_ref()
1324                .and_then(|p| p.raw)
1325                .or_else(|| self.regular_market_price.as_ref()?.raw)
1326        } else if self.market_state.as_deref() == Some("PRE") {
1327            self.pre_market_price
1328                .as_ref()
1329                .and_then(|p| p.raw)
1330                .or_else(|| self.regular_market_price.as_ref()?.raw)
1331        } else {
1332            self.regular_market_price.as_ref()?.raw
1333        }
1334    }
1335
1336    /// Returns the day's trading range as (low, high)
1337    pub fn day_range(&self) -> Option<(f64, f64)> {
1338        let low = self.regular_market_day_low.as_ref()?.raw?;
1339        let high = self.regular_market_day_high.as_ref()?.raw?;
1340        Some((low, high))
1341    }
1342
1343    /// Returns the 52-week range as (low, high)
1344    pub fn week_52_range(&self) -> Option<(f64, f64)> {
1345        let low = self.fifty_two_week_low.as_ref()?.raw?;
1346        let high = self.fifty_two_week_high.as_ref()?.raw?;
1347        Some((low, high))
1348    }
1349
1350    /// Returns whether the market is currently open
1351    pub fn is_market_open(&self) -> bool {
1352        self.market_state.as_deref() == Some("REGULAR")
1353    }
1354
1355    /// Returns whether this is in pre-market trading
1356    pub fn is_pre_market(&self) -> bool {
1357        self.market_state.as_deref() == Some("PRE")
1358    }
1359
1360    /// Returns whether this is in post-market trading
1361    pub fn is_post_market(&self) -> bool {
1362        self.market_state.as_deref() == Some("POST")
1363    }
1364
1365    /// Creates a Quote from a batch /v7/finance/quote response item
1366    ///
1367    /// This parses the simpler flat structure from the batch quotes endpoint,
1368    /// which has fewer fields than the full quoteSummary response.
1369    pub(crate) fn from_batch_response(json: &serde_json::Value) -> Result<Self, String> {
1370        let symbol = json
1371            .get("symbol")
1372            .and_then(|v| v.as_str())
1373            .ok_or("Missing symbol")?
1374            .to_string();
1375
1376        // Helper to extract FormattedValue<f64>
1377        // Handles both raw numbers and formatted objects: {"raw": 273.4, "fmt": "273.40"}
1378        let get_f64 = |key: &str| -> Option<super::FormattedValue<f64>> {
1379            json.get(key).and_then(|v| {
1380                if let Some(raw) = v.as_f64() {
1381                    // Raw number (formatted=false)
1382                    Some(super::FormattedValue {
1383                        raw: Some(raw),
1384                        fmt: None,
1385                        long_fmt: None,
1386                    })
1387                } else if v.is_object() {
1388                    // Formatted object (formatted=true)
1389                    Some(super::FormattedValue {
1390                        raw: v.get("raw").and_then(|r| r.as_f64()),
1391                        fmt: v.get("fmt").and_then(|f| f.as_str()).map(|s| s.to_string()),
1392                        long_fmt: v
1393                            .get("longFmt")
1394                            .and_then(|f| f.as_str())
1395                            .map(|s| s.to_string()),
1396                    })
1397                } else {
1398                    None
1399                }
1400            })
1401        };
1402
1403        // Helper to extract FormattedValue<i64>
1404        // Handles both raw numbers and formatted objects
1405        let get_i64 = |key: &str| -> Option<super::FormattedValue<i64>> {
1406            json.get(key).and_then(|v| {
1407                if let Some(raw) = v.as_i64() {
1408                    // Raw number (formatted=false)
1409                    Some(super::FormattedValue {
1410                        raw: Some(raw),
1411                        fmt: None,
1412                        long_fmt: None,
1413                    })
1414                } else if v.is_object() {
1415                    // Formatted object (formatted=true)
1416                    Some(super::FormattedValue {
1417                        raw: v.get("raw").and_then(|r| r.as_i64()),
1418                        fmt: v.get("fmt").and_then(|f| f.as_str()).map(|s| s.to_string()),
1419                        long_fmt: v
1420                            .get("longFmt")
1421                            .and_then(|f| f.as_str())
1422                            .map(|s| s.to_string()),
1423                    })
1424                } else {
1425                    None
1426                }
1427            })
1428        };
1429
1430        // Helper to extract String
1431        let get_str = |key: &str| -> Option<String> {
1432            json.get(key)
1433                .and_then(|v| v.as_str())
1434                .map(|s| s.to_string())
1435        };
1436
1437        Ok(Self {
1438            symbol,
1439            logo_url: get_str("logoUrl"),
1440            company_logo_url: get_str("companyLogoUrl"),
1441
1442            // Identity
1443            short_name: get_str("shortName"),
1444            long_name: get_str("longName"),
1445            exchange: get_str("exchange"),
1446            exchange_name: get_str("fullExchangeName"),
1447            quote_type: get_str("quoteType"),
1448            currency: get_str("currency"),
1449            currency_symbol: get_str("currencySymbol"),
1450            underlying_symbol: get_str("underlyingSymbol"),
1451            from_currency: get_str("fromCurrency"),
1452            to_currency: get_str("toCurrency"),
1453
1454            // Real-time price
1455            regular_market_price: get_f64("regularMarketPrice"),
1456            regular_market_change: get_f64("regularMarketChange"),
1457            regular_market_change_percent: get_f64("regularMarketChangePercent"),
1458            regular_market_time: json.get("regularMarketTime").and_then(|v| v.as_i64()),
1459            regular_market_day_high: get_f64("regularMarketDayHigh"),
1460            regular_market_day_low: get_f64("regularMarketDayLow"),
1461            regular_market_open: get_f64("regularMarketOpen"),
1462            regular_market_previous_close: get_f64("regularMarketPreviousClose"),
1463            regular_market_volume: get_i64("regularMarketVolume"),
1464            market_state: get_str("marketState"),
1465
1466            // Alternative trading metrics
1467            day_high: get_f64("dayHigh"),
1468            day_low: get_f64("dayLow"),
1469            open: get_f64("open"),
1470            previous_close: get_f64("previousClose"),
1471            volume: get_i64("volume"),
1472
1473            // Price history
1474            all_time_high: None,
1475            all_time_low: None,
1476
1477            // Pre/post market
1478            pre_market_price: get_f64("preMarketPrice"),
1479            pre_market_change: get_f64("preMarketChange"),
1480            pre_market_change_percent: get_f64("preMarketChangePercent"),
1481            pre_market_time: json.get("preMarketTime").and_then(|v| v.as_i64()),
1482            post_market_price: get_f64("postMarketPrice"),
1483            post_market_change: get_f64("postMarketChange"),
1484            post_market_change_percent: get_f64("postMarketChangePercent"),
1485            post_market_time: json.get("postMarketTime").and_then(|v| v.as_i64()),
1486
1487            // Volume
1488            average_daily_volume10_day: get_i64("averageDailyVolume10Day"),
1489            average_daily_volume3_month: get_i64("averageDailyVolume3Month"),
1490            average_volume: get_i64("averageVolume"),
1491            average_volume10days: get_i64("averageVolume10days"),
1492
1493            // Valuation
1494            market_cap: get_i64("marketCap"),
1495            enterprise_value: None,
1496            enterprise_to_revenue: None,
1497            enterprise_to_ebitda: None,
1498            price_to_book: get_f64("priceToBook"),
1499            price_to_sales_trailing12_months: None,
1500
1501            // PE
1502            forward_pe: get_f64("forwardPE"),
1503            trailing_pe: get_f64("trailingPE"),
1504
1505            // Risk
1506            beta: None,
1507
1508            // 52-week
1509            fifty_two_week_high: get_f64("fiftyTwoWeekHigh"),
1510            fifty_two_week_low: get_f64("fiftyTwoWeekLow"),
1511            fifty_day_average: get_f64("fiftyDayAverage"),
1512            two_hundred_day_average: get_f64("twoHundredDayAverage"),
1513            week_52_change: get_f64("52WeekChange"),
1514            sand_p_52_week_change: None,
1515
1516            // Dividends
1517            dividend_rate: get_f64("dividendRate"),
1518            dividend_yield: get_f64("dividendYield"),
1519            trailing_annual_dividend_rate: get_f64("trailingAnnualDividendRate"),
1520            trailing_annual_dividend_yield: get_f64("trailingAnnualDividendYield"),
1521            five_year_avg_dividend_yield: None,
1522            ex_dividend_date: None,
1523            payout_ratio: None,
1524            last_dividend_value: None,
1525            last_dividend_date: None,
1526
1527            // Bid/Ask
1528            bid: get_f64("bid"),
1529            bid_size: get_i64("bidSize"),
1530            ask: get_f64("ask"),
1531            ask_size: get_i64("askSize"),
1532
1533            // Shares
1534            shares_outstanding: get_i64("sharesOutstanding"),
1535            float_shares: None,
1536            implied_shares_outstanding: None,
1537            held_percent_insiders: None,
1538            held_percent_institutions: None,
1539            shares_short: None,
1540            shares_short_prior_month: None,
1541            short_ratio: None,
1542            short_percent_of_float: None,
1543            shares_percent_shares_out: None,
1544            date_short_interest: None,
1545
1546            // Financial metrics
1547            current_price: None,
1548            target_high_price: None,
1549            target_low_price: None,
1550            target_mean_price: None,
1551            target_median_price: None,
1552            recommendation_mean: None,
1553            recommendation_key: None,
1554            number_of_analyst_opinions: None,
1555            total_cash: None,
1556            total_cash_per_share: None,
1557            ebitda: None,
1558            total_debt: None,
1559            total_revenue: None,
1560            net_income_to_common: None,
1561            debt_to_equity: None,
1562            revenue_per_share: None,
1563            return_on_assets: None,
1564            return_on_equity: None,
1565            free_cashflow: None,
1566            operating_cashflow: None,
1567
1568            // Margins
1569            profit_margins: None,
1570            gross_margins: None,
1571            ebitda_margins: None,
1572            operating_margins: None,
1573            gross_profits: None,
1574
1575            // Growth
1576            earnings_growth: None,
1577            revenue_growth: None,
1578            earnings_quarterly_growth: None,
1579
1580            // Ratios
1581            current_ratio: None,
1582            quick_ratio: None,
1583
1584            // EPS
1585            trailing_eps: get_f64("trailingEps"),
1586            forward_eps: get_f64("forwardEps"),
1587            book_value: get_f64("bookValue"),
1588
1589            // Company profile
1590            sector: get_str("sector"),
1591            sector_key: get_str("sectorKey"),
1592            sector_disp: get_str("sectorDisp"),
1593            industry: get_str("industry"),
1594            industry_key: get_str("industryKey"),
1595            industry_disp: get_str("industryDisp"),
1596            long_business_summary: None,
1597            website: None,
1598            ir_website: None,
1599            address1: None,
1600            city: None,
1601            state: None,
1602            zip: None,
1603            country: None,
1604            phone: None,
1605            full_time_employees: None,
1606            category: None,
1607            fund_family: None,
1608
1609            // Risk scores
1610            audit_risk: None,
1611            board_risk: None,
1612            compensation_risk: None,
1613            shareholder_rights_risk: None,
1614            overall_risk: None,
1615
1616            // Timezone
1617            time_zone_full_name: get_str("exchangeTimezoneName"),
1618            time_zone_short_name: get_str("exchangeTimezoneShortName"),
1619            gmt_off_set_milliseconds: json.get("gmtOffSetMilliseconds").and_then(|v| v.as_i64()),
1620            first_trade_date_epoch_utc: json
1621                .get("firstTradeDateMilliseconds")
1622                .and_then(|v| v.as_i64()),
1623            message_board_id: get_str("messageBoardId"),
1624            exchange_data_delayed_by: json
1625                .get("exchangeDataDelayedBy")
1626                .and_then(|v| v.as_i64())
1627                .map(|v| v as i32),
1628
1629            // Fund-specific
1630            nav_price: get_f64("navPrice"),
1631            total_assets: get_i64("totalAssets"),
1632            yield_value: get_f64("yield"),
1633
1634            // Stock splits
1635            last_split_factor: None,
1636            last_split_date: None,
1637            last_fiscal_year_end: None,
1638            next_fiscal_year_end: None,
1639            most_recent_quarter: None,
1640
1641            // Misc
1642            price_hint: get_i64("priceHint"),
1643            tradeable: json.get("tradeable").and_then(|v| v.as_bool()),
1644            financial_currency: get_str("financialCurrency"),
1645
1646            // Nested objects not available in batch response
1647            company_officers: None,
1648            earnings: None,
1649            calendar_events: None,
1650            recommendation_trend: None,
1651            upgrade_downgrade_history: None,
1652            earnings_history: None,
1653            earnings_trend: None,
1654            insider_holders: None,
1655            insider_transactions: None,
1656            institution_ownership: None,
1657            fund_ownership: None,
1658            major_holders_breakdown: None,
1659            net_share_purchase_activity: None,
1660            sec_filings: None,
1661            balance_sheet_history: None,
1662            balance_sheet_history_quarterly: None,
1663            cashflow_statement_history: None,
1664            cashflow_statement_history_quarterly: None,
1665            income_statement_history: None,
1666            income_statement_history_quarterly: None,
1667            equity_performance: None,
1668            index_trend: None,
1669            industry_trend: None,
1670            sector_trend: None,
1671            fund_profile: None,
1672            fund_performance: None,
1673            top_holdings: None,
1674        })
1675    }
1676}