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