Skip to main content

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