Skip to main content

longbridge/fundamental/
types.rs

1use std::collections::HashMap;
2
3use num_enum::{FromPrimitive, IntoPrimitive};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use strum_macros::{Display, EnumString};
7use time::OffsetDateTime;
8
9// ── financial_report ─────────────────────────────────────────────
10
11/// Response for [`crate::FundamentalContext::financial_report`]
12///
13/// The `list` field contains deeply-nested indicator/account/value data keyed
14/// by report kind (`"IS"`, `"BS"`, `"CF"`).  The exact structure varies and is
15/// preserved as raw JSON.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct FinancialReports {
18    /// Raw nested financial data. Top-level keys are report kinds such as
19    /// `"IS"` (income statement), `"BS"` (balance sheet), `"CF"` (cash flow).
20    pub list: serde_json::Value,
21}
22
23// ── dividend ─────────────────────────────────────────────────────
24
25/// Response for [`crate::FundamentalContext::dividend`] and
26/// [`crate::FundamentalContext::dividend_detail`]
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct DividendList {
29    /// List of dividend events
30    pub list: Vec<DividendItem>,
31}
32
33/// A single dividend / distribution event
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct DividendItem {
36    /// Security symbol, e.g. `"700.HK"`
37    pub symbol: String,
38    /// Internal record ID (may be absent in dividend_detail response)
39    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
40    pub id: String,
41    /// Human-readable description, e.g. `"每股派息 5.3 HKD"`
42    pub desc: String,
43    /// Record / book-close date, e.g. `"2026.05.18"`
44    pub record_date: String,
45    /// Ex-dividend date, e.g. `"2026.05.15"`
46    pub ex_date: String,
47    /// Payment date, e.g. `"2026.06.01"`
48    pub payment_date: String,
49}
50
51// ── institution_rating ────────────────────────────────────────────
52
53/// Combined analyst-rating response for
54/// [`crate::FundamentalContext::institution_rating`]
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct InstitutionRating {
57    /// Latest snapshot from `/v1/quote/institution-rating-latest`
58    pub latest: InstitutionRatingLatest,
59    /// Consensus summary from `/v1/quote/institution-ratings`
60    pub summary: InstitutionRatingSummary,
61}
62
63/// Latest analyst-rating snapshot
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct InstitutionRatingLatest {
66    /// Rating distribution counts and date range
67    pub evaluate: RatingEvaluate,
68    /// Target price range
69    pub target: RatingTarget,
70    /// Industry classification ID
71    pub industry_id: i64,
72    /// Industry name
73    pub industry_name: String,
74    /// Rank of this security within the industry (1 = highest)
75    pub industry_rank: i32,
76    /// Total number of securities in the industry
77    pub industry_total: i32,
78    /// Mean analyst count in the industry
79    pub industry_mean: i32,
80    /// Median analyst count in the industry
81    pub industry_median: i32,
82}
83
84/// Analyst rating distribution counts
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct RatingEvaluate {
87    /// Number of "Buy" ratings
88    pub buy: i32,
89    /// Number of "Strong Buy" / "Outperform" ratings
90    pub over: i32,
91    /// Number of "Hold" / "Neutral" ratings
92    pub hold: i32,
93    /// Number of "Underperform" ratings
94    pub under: i32,
95    /// Number of "Sell" ratings
96    pub sell: i32,
97    /// Number of "No Opinion" ratings
98    pub no_opinion: i32,
99    /// Total analyst count
100    pub total: i32,
101    /// Window start (unix timestamp string; `"0"` means unset)
102    pub start_date: String,
103    /// Window end (unix timestamp string; `"0"` means unset)
104    pub end_date: String,
105}
106
107/// Analyst target price range
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct RatingTarget {
110    /// Highest price target
111    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
112    pub highest_price: Option<Decimal>,
113    /// Lowest price target
114    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
115    pub lowest_price: Option<Decimal>,
116    /// Previous close price
117    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
118    pub prev_close: Option<Decimal>,
119    /// Window start (unix timestamp string)
120    pub start_date: String,
121    /// Window end (unix timestamp string)
122    pub end_date: String,
123}
124
125/// Consensus summary from `/v1/quote/institution-ratings`
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct InstitutionRatingSummary {
128    /// Currency symbol, e.g. `"HK$"`
129    pub ccy_symbol: String,
130    /// Change vs previous period
131    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
132    pub change: Option<Decimal>,
133    /// Simplified rating distribution
134    pub evaluate: RatingSummaryEvaluate,
135    /// Overall recommendation
136    pub recommend: InstitutionRecommend,
137    /// Consensus target price
138    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
139    pub target: Option<Decimal>,
140    /// Last updated display string, e.g. `"2026 年 5 月 5 日"`
141    pub updated_at: String,
142}
143
144/// Simplified rating distribution for the consensus summary
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct RatingSummaryEvaluate {
147    /// Number of "Buy" ratings
148    pub buy: i32,
149    /// Date of the latest update
150    pub date: String,
151    /// Number of "Hold" ratings
152    pub hold: i32,
153    /// Number of "Sell" ratings
154    pub sell: i32,
155    /// Number of "Strong Buy" ratings
156    pub strong_buy: i32,
157    /// Number of "Underperform" ratings
158    pub under: i32,
159}
160
161// ── institution_rating_detail ─────────────────────────────────────
162
163/// Response for [`crate::FundamentalContext::institution_rating_detail`]
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct InstitutionRatingDetail {
166    /// Currency symbol, e.g. `"HK$"`
167    pub ccy_symbol: String,
168    /// Historical rating distribution time-series
169    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
170    pub evaluate: InstitutionRatingDetailEvaluate,
171    /// Historical target price time-series
172    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
173    pub target: InstitutionRatingDetailTarget,
174}
175
176/// Historical rating distribution time-series
177#[derive(Debug, Clone, Serialize, Deserialize, Default)]
178pub struct InstitutionRatingDetailEvaluate {
179    /// Weekly snapshots ordered from oldest to newest
180    pub list: Vec<InstitutionRatingDetailEvaluateItem>,
181}
182
183/// One weekly rating distribution snapshot
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct InstitutionRatingDetailEvaluateItem {
186    /// Number of "Buy" ratings
187    pub buy: i32,
188    /// Date in `"2021/05/14"` format
189    pub date: String,
190    /// Number of "Hold" ratings
191    pub hold: i32,
192    /// Number of "Sell" ratings
193    pub sell: i32,
194    /// Number of "Strong Buy" / "Outperform" ratings
195    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
196    pub strong_buy: i32,
197    /// Number of "No Opinion" ratings
198    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
199    pub no_opinion: i32,
200    /// Number of "Underperform" ratings
201    pub under: i32,
202}
203
204/// Historical target price time-series
205#[derive(Debug, Clone, Serialize, Deserialize, Default)]
206pub struct InstitutionRatingDetailTarget {
207    /// Prediction accuracy ratio, e.g. `"0.9934"` (may be `null`)
208    pub data_percent: Option<Decimal>,
209    /// Overall prediction accuracy percentage string
210    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
211    pub prediction_accuracy: Option<Decimal>,
212    /// Last updated display string
213    pub updated_at: String,
214    /// Weekly target price snapshots
215    pub list: Vec<InstitutionRatingDetailTargetItem>,
216}
217
218/// One weekly target price snapshot
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct InstitutionRatingDetailTargetItem {
221    /// Average target price
222    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
223    pub avg_target: Option<Decimal>,
224    /// Date in `"2021/05/16"` format
225    pub date: String,
226    /// Highest target price
227    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
228    pub max_target: Option<Decimal>,
229    /// Lowest target price
230    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
231    pub min_target: Option<Decimal>,
232    /// Whether the stock price reached the target
233    pub meet: bool,
234    /// Actual stock price at this date
235    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
236    pub price: Option<Decimal>,
237    /// Unix timestamp string
238    pub timestamp: String,
239}
240
241// ── forecast_eps ──────────────────────────────────────────────────
242
243/// Response for [`crate::FundamentalContext::forecast_eps`]
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct ForecastEps {
246    /// EPS forecast snapshots ordered by `forecast_start_date` ascending
247    pub items: Vec<ForecastEpsItem>,
248}
249
250/// One EPS forecast snapshot
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ForecastEpsItem {
253    /// Median EPS estimate
254    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
255    pub forecast_eps_median: Option<Decimal>,
256    /// Mean EPS estimate
257    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
258    pub forecast_eps_mean: Option<Decimal>,
259    /// Lowest EPS estimate
260    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
261    pub forecast_eps_lowest: Option<Decimal>,
262    /// Highest EPS estimate
263    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
264    pub forecast_eps_highest: Option<Decimal>,
265    /// Total number of forecasting institutions
266    pub institution_total: i32,
267    /// Number of institutions that raised their estimate
268    pub institution_up: i32,
269    /// Number of institutions that lowered their estimate
270    pub institution_down: i32,
271    /// Forecast window start
272    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
273    pub forecast_start_date: OffsetDateTime,
274    /// Forecast window end
275    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
276    pub forecast_end_date: OffsetDateTime,
277}
278
279// ── consensus ─────────────────────────────────────────────────────
280
281/// Response for [`crate::FundamentalContext::consensus`]
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct FinancialConsensus {
284    /// Per-period consensus reports
285    pub list: Vec<ConsensusReport>,
286    /// Index into `list` of the most recently released period
287    pub current_index: i32,
288    /// Reporting currency, e.g. `"HKD"`
289    pub currency: String,
290    /// Available period types, e.g. `["qf", "saf", "af"]`
291    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
292    pub opt_periods: Vec<String>,
293    /// Currently returned period type
294    pub current_period: String,
295}
296
297/// Consensus report for one fiscal period
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct ConsensusReport {
300    /// Fiscal year, e.g. `2025`
301    pub fiscal_year: i32,
302    /// Fiscal period code, e.g. `"Q4"`
303    pub fiscal_period: String,
304    /// Human-readable period label, e.g. `"Q4 FY2025"`
305    pub period_text: String,
306    /// Per-metric consensus details
307    pub details: Vec<ConsensusDetail>,
308}
309
310/// Consensus estimate for one financial metric
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct ConsensusDetail {
313    /// Metric key, e.g. `"revenue"`, `"eps"`
314    pub key: String,
315    /// Display name
316    pub name: String,
317    /// Metric description
318    pub description: String,
319    /// Actual reported value (empty string if not yet released)
320    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
321    pub actual: Option<Decimal>,
322    /// Consensus estimate value
323    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
324    pub estimate: Option<Decimal>,
325    /// Actual minus estimate
326    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
327    pub comp_value: Option<Decimal>,
328    /// Beat/miss description, e.g. `"超出预期"`
329    pub comp_desc: String,
330    /// Comparison result code for colour coding
331    pub comp: String,
332    /// Whether the actual results have been published
333    pub is_released: bool,
334}
335
336// ── valuation ─────────────────────────────────────────────────────
337
338/// Response for [`crate::FundamentalContext::valuation`]
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct ValuationData {
341    /// Valuation metrics (PE / PB / PS / dividend yield)
342    pub metrics: ValuationMetricsData,
343}
344
345/// Container for all valuation metrics
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ValuationMetricsData {
348    /// Price-to-Earnings ratio history
349    pub pe: Option<ValuationMetricData>,
350    /// Price-to-Book ratio history
351    pub pb: Option<ValuationMetricData>,
352    /// Price-to-Sales ratio history
353    pub ps: Option<ValuationMetricData>,
354    /// Dividend yield history
355    pub dvd_yld: Option<ValuationMetricData>,
356}
357
358/// Historical time-series for one valuation metric
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct ValuationMetricData {
361    /// Human-readable description with current value and percentile
362    pub desc: String,
363    /// Historical high value
364    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
365    pub high: Option<Decimal>,
366    /// Historical low value
367    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
368    pub low: Option<Decimal>,
369    /// Historical median value
370    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
371    pub median: Option<Decimal>,
372    /// Historical data points
373    pub list: Vec<ValuationPoint>,
374}
375
376/// One valuation data point
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct ValuationPoint {
379    /// Date of the data point
380    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
381    pub timestamp: OffsetDateTime,
382    /// Metric value
383    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
384    pub value: Option<Decimal>,
385}
386
387// ── valuation_history ─────────────────────────────────────────────
388
389/// Response for [`crate::FundamentalContext::valuation_history`]
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct ValuationHistoryResponse {
392    /// Historical valuation data
393    pub history: ValuationHistoryData,
394}
395
396/// Container for historical valuation metrics
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct ValuationHistoryData {
399    /// Historical metrics (PE / PB / PS)
400    pub metrics: ValuationHistoryMetrics,
401}
402
403/// Historical valuation metrics container
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct ValuationHistoryMetrics {
406    /// Price-to-Earnings history
407    pub pe: Option<ValuationHistoryMetric>,
408    /// Price-to-Book history
409    pub pb: Option<ValuationHistoryMetric>,
410    /// Price-to-Sales history
411    pub ps: Option<ValuationHistoryMetric>,
412}
413
414/// Historical data for one valuation metric including statistical bounds
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct ValuationHistoryMetric {
417    /// Human-readable description
418    pub desc: String,
419    /// Historical high over the period
420    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
421    pub high: Option<Decimal>,
422    /// Historical low over the period
423    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
424    pub low: Option<Decimal>,
425    /// Historical median over the period
426    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
427    pub median: Option<Decimal>,
428    /// Historical data points
429    pub list: Vec<ValuationPoint>,
430}
431
432// ── industry_valuation ────────────────────────────────────────────
433
434/// Response for [`crate::FundamentalContext::industry_valuation`]
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct IndustryValuationList {
437    /// List of peer securities with their valuation data
438    pub list: Vec<IndustryValuationItem>,
439}
440
441/// Valuation data for one peer security
442#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct IndustryValuationItem {
444    /// Security symbol, e.g. `"700.HK"`
445    pub symbol: String,
446    /// Company name
447    pub name: String,
448    /// Reporting currency
449    pub currency: String,
450    /// Total assets
451    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
452    pub assets: Option<Decimal>,
453    /// Book value per share
454    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
455    pub bps: Option<Decimal>,
456    /// Earnings per share
457    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
458    pub eps: Option<Decimal>,
459    /// Dividends per share
460    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
461    pub dps: Option<Decimal>,
462    /// Dividend yield
463    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
464    pub div_yld: Option<Decimal>,
465    /// Dividend payout ratio
466    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
467    pub div_payout_ratio: Option<Decimal>,
468    /// 5-year average dividends per share
469    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
470    pub five_y_avg_dps: Option<Decimal>,
471    /// Current PE ratio
472    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
473    pub pe: Option<Decimal>,
474    /// Historical PE/PB/PS snapshots
475    pub history: Vec<IndustryValuationHistory>,
476}
477
478/// Historical valuation snapshot for an industry peer
479#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct IndustryValuationHistory {
481    /// Unix timestamp string
482    pub date: String,
483    /// Price-to-Earnings ratio
484    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
485    pub pe: Option<Decimal>,
486    /// Price-to-Book ratio
487    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
488    pub pb: Option<Decimal>,
489    /// Price-to-Sales ratio
490    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
491    pub ps: Option<Decimal>,
492}
493
494// ── industry_valuation_dist ───────────────────────────────────────
495
496/// Response for [`crate::FundamentalContext::industry_valuation_dist`]
497#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct IndustryValuationDist {
499    /// PE ratio distribution within the industry
500    pub pe: Option<ValuationDist>,
501    /// PB ratio distribution within the industry
502    pub pb: Option<ValuationDist>,
503    /// PS ratio distribution within the industry
504    pub ps: Option<ValuationDist>,
505}
506
507/// Distribution statistics for one valuation metric within an industry
508#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct ValuationDist {
510    /// Minimum value in the industry
511    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
512    pub low: Option<Decimal>,
513    /// Maximum value in the industry
514    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
515    pub high: Option<Decimal>,
516    /// Median value in the industry
517    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
518    pub median: Option<Decimal>,
519    /// Current value of the queried security
520    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
521    pub value: Option<Decimal>,
522    /// Percentile ranking (0–1 range as string)
523    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
524    pub ranking: Option<Decimal>,
525    /// Ordinal rank index (1-based)
526    pub rank_index: String,
527    /// Total number of securities in the industry
528    pub rank_total: String,
529}
530
531// ── company ───────────────────────────────────────────────────────
532
533/// Response for [`crate::FundamentalContext::company`]
534#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct CompanyOverview {
536    /// Short name, e.g. `"腾讯控股"`
537    pub name: String,
538    /// Full legal name
539    pub company_name: String,
540    /// Founding date
541    pub founded: String,
542    /// Listing date
543    pub listing_date: String,
544    /// Primary listing market display name
545    pub market: String,
546    /// Market region code, e.g. `"HK"`
547    pub region: String,
548    /// Registered address
549    pub address: String,
550    /// Principal office address
551    pub office_address: String,
552    /// Company website
553    pub website: String,
554    /// IPO issue price
555    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
556    pub issue_price: Option<Decimal>,
557    /// Number of shares offered at IPO
558    pub shares_offered: String,
559    /// Chairman name
560    pub chairman: String,
561    /// Company secretary name
562    pub secretary: String,
563    /// Auditing institution
564    pub audit_inst: String,
565    /// Company classification category
566    pub category: String,
567    /// Fiscal year end, e.g. `"12 月 31 日"`
568    pub year_end: String,
569    /// Number of employees (returned as a string by the API, e.g. `"10000"`)
570    pub employees: String,
571    /// Phone number (API field name is `"Phone"`)
572    #[serde(rename = "Phone")]
573    pub phone: String,
574    /// Fax number
575    pub fax: String,
576    /// Investor relations email
577    pub email: String,
578    /// Legal representative
579    pub legal_repr: String,
580    /// CEO / Managing Director
581    pub manager: String,
582    /// Business licence number
583    pub bus_license: String,
584    /// Accounting firm
585    pub accounting_firm: String,
586    /// Securities representative
587    pub securities_rep: String,
588    /// Legal counsel
589    pub legal_counsel: String,
590    /// Postal code
591    pub zip_code: String,
592    /// Exchange ticker code, e.g. `"00700"`
593    pub ticker: String,
594    /// URL to the company's logo icon
595    pub icon: String,
596    /// Business profile / description
597    pub profile: String,
598    /// ADS ratio (may be empty)
599    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
600    pub ads_ratio: String,
601    /// Industry sector code
602    pub sector: i32,
603}
604
605// ── executive ─────────────────────────────────────────────────────
606
607/// Response for [`crate::FundamentalContext::executive`]
608#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct ExecutiveList {
610    /// Groups of executives per security (usually one group)
611    pub professional_list: Vec<ExecutiveGroup>,
612}
613
614/// Executives for one security
615#[derive(Debug, Clone, Serialize, Deserialize)]
616pub struct ExecutiveGroup {
617    /// Security symbol (`None` when the server omits it or returns empty)
618    #[serde(default, with = "crate::serde_utils::symbol_opt")]
619    pub symbol: Option<String>,
620    /// Link to the company wiki page
621    pub forward_url: String,
622    /// Total number of executives
623    pub total: i32,
624    /// Individual executive entries
625    pub professionals: Vec<Professional>,
626}
627
628/// One executive / board member
629#[derive(Debug, Clone, Serialize, Deserialize)]
630pub struct Professional {
631    /// Internal wiki person ID (string form)
632    pub id: String,
633    /// Full name
634    pub name: String,
635    /// Full name in Simplified Chinese
636    pub name_zhcn: String,
637    /// Full name in English
638    pub name_en: String,
639    /// Job title, e.g. `"Co-Founder, Chairman & CEO"`
640    pub title: String,
641    /// Biography text
642    pub biography: String,
643    /// URL to the person's photo
644    pub photo: String,
645    /// URL to the wiki profile page
646    pub wiki_url: String,
647}
648
649// ── shareholder ───────────────────────────────────────────────────
650
651/// Response for [`crate::FundamentalContext::shareholder`]
652#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct ShareholderList {
654    /// List of major shareholders
655    pub shareholder_list: Vec<Shareholder>,
656    /// Link to the full shareholder page
657    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
658    pub forward_url: String,
659    /// Total number of shareholders returned
660    pub total: i32,
661}
662
663/// One major shareholder
664#[derive(Debug, Clone, Serialize, Deserialize)]
665pub struct Shareholder {
666    /// Internal shareholder ID (string form)
667    pub shareholder_id: String,
668    /// Shareholder name
669    pub shareholder_name: String,
670    /// Institution type (may be empty)
671    pub institution_type: String,
672    /// Percentage of shares held
673    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
674    pub percent_of_shares: Option<Decimal>,
675    /// Change in shares held (positive = bought, negative = sold)
676    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
677    pub shares_changed: Option<Decimal>,
678    /// Date of the most recent filing, e.g. `"2026-05-04"`
679    pub report_date: String,
680    /// Other securities held by this shareholder (cross-holdings)
681    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
682    pub stocks: Vec<ShareholderStock>,
683}
684
685/// A security in an institutional shareholder's cross-holdings
686#[derive(Debug, Clone, Serialize, Deserialize)]
687pub struct ShareholderStock {
688    /// Security symbol of the cross-held stock
689    pub symbol: String,
690    /// Ticker code, e.g. `"BLK"`
691    pub code: String,
692    /// Market, e.g. `"US"`
693    pub market: String,
694    /// Day change percentage, e.g. `"-0.32%"`
695    pub chg: String,
696}
697
698// ── fund_holder ───────────────────────────────────────────────────
699
700/// Response for [`crate::FundamentalContext::fund_holder`]
701#[derive(Debug, Clone, Serialize, Deserialize)]
702pub struct FundHolders {
703    /// Funds and ETFs that hold the queried security
704    pub lists: Vec<FundHolder>,
705}
706
707/// A fund or ETF that holds the queried security
708#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct FundHolder {
710    /// Fund/ETF ticker code, e.g. `"513050"`
711    pub code: String,
712    /// Fund/ETF symbol, e.g. `"513050.SH"`
713    pub symbol: String,
714    /// Reporting currency, e.g. `"CNY"`
715    pub currency: String,
716    /// Fund/ETF full name
717    pub name: String,
718    /// Position ratio as a percentage decimal
719    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
720    pub position_ratio: Decimal,
721    /// Report date, e.g. `"2025.12.31"`
722    pub report_date: String,
723}
724
725// ── corp_action ───────────────────────────────────────────────────
726
727/// Response for [`crate::FundamentalContext::corp_action`]
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct CorpActions {
730    /// Corporate action events
731    pub items: Vec<CorpActionItem>,
732}
733
734/// One corporate action event
735#[derive(Debug, Clone, Serialize, Deserialize)]
736pub struct CorpActionItem {
737    /// Internal event ID
738    pub id: String,
739    /// Date in `YYYYMMDD` format, e.g. `"20260601"`
740    pub date: String,
741    /// Short display date, e.g. `"06.01"`
742    pub date_str: String,
743    /// Date type label, e.g. `"派息日"`, `"除权日"`
744    pub date_type: String,
745    /// Time zone description, e.g. `"北京时间"`
746    pub date_zone: String,
747    /// Event category, e.g. `"分配方案"`
748    pub act_type: String,
749    /// Human-readable event description
750    pub act_desc: String,
751    /// Machine-readable action code, e.g. `"DividendExDate"`
752    pub action: String,
753    /// Whether this is a recent event
754    pub recent: bool,
755    /// Whether publication was delayed
756    pub is_delay: bool,
757    /// Delay announcement content (if `is_delay` is `true`)
758    pub delay_content: String,
759    /// Associated live stream (if any)
760    pub live: Option<CorpActionLive>,
761    /// Associated security info (rarely populated; preserved as raw JSON)
762    pub security: Option<serde_json::Value>,
763}
764
765/// Live stream associated with a corporate action
766#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct CorpActionLive {
768    /// Live stream ID
769    pub id: String,
770    /// Status code: 1=preview, 2=live, 3=ended, 4=replay, 5=processing.
771    ///
772    /// The API may return this as either an integer or a string; it is
773    /// normalized to a string.
774    #[serde(deserialize_with = "crate::serde_utils::deserialize_string_or_int_as_string")]
775    pub status: String,
776    /// Start time
777    pub started_at: String,
778    /// Stream title
779    pub name: String,
780    /// Icon URL
781    pub icon: String,
782}
783
784// ── invest_relation ───────────────────────────────────────────────
785
786/// Response for [`crate::FundamentalContext::invest_relation`]
787#[derive(Debug, Clone, Serialize, Deserialize)]
788pub struct InvestRelations {
789    /// Link to the full investor-relations page
790    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
791    pub forward_url: String,
792    /// Securities in which the queried company holds a stake
793    pub invest_securities: Vec<InvestSecurity>,
794}
795
796/// A security in which the queried company has an investment stake
797#[derive(Debug, Clone, Serialize, Deserialize)]
798pub struct InvestSecurity {
799    /// Internal company ID (string form; may be `"0"`)
800    pub company_id: String,
801    /// Company name (locale-aware)
802    pub company_name: String,
803    /// Company name in English
804    pub company_name_en: String,
805    /// Company name in Simplified Chinese
806    pub company_name_zhcn: String,
807    /// Security symbol of the invested company
808    pub symbol: String,
809    /// Reporting currency
810    pub currency: String,
811    /// Percentage of shares held
812    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
813    pub percent_of_shares: Option<Decimal>,
814    /// Shareholder rank, e.g. `"1"` = largest shareholder
815    pub shares_rank: String,
816    /// Market value of the holding
817    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
818    pub shares_value: Option<Decimal>,
819}
820
821// ── operating ─────────────────────────────────────────────────────
822
823/// Response for [`crate::FundamentalContext::operating`]
824#[derive(Debug, Clone, Serialize, Deserialize)]
825pub struct OperatingList {
826    /// List of operating summary reports
827    pub list: Vec<OperatingItem>,
828}
829
830/// One operating summary report (annual / quarterly)
831#[derive(Debug, Clone, Serialize, Deserialize)]
832pub struct OperatingItem {
833    /// Internal report ID
834    pub id: String,
835    /// Report period code, e.g. `"af"` (annual), `"qf"` (quarterly)
836    pub report: String,
837    /// Report title, e.g. `"2025 财年年报"`
838    pub title: String,
839    /// Management discussion text
840    pub txt: String,
841    /// Whether this is the most recent report
842    pub latest: bool,
843    /// Keyword tags (structure undocumented; usually empty)
844    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
845    pub keywords: Vec<serde_json::Value>,
846    /// URL to the full community report page
847    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
848    pub web_url: String,
849    /// Key financial metrics extracted from the report
850    pub financial: OperatingFinancial,
851}
852
853/// Key financial metrics extracted from an operating report
854#[derive(Debug, Clone, Serialize, Deserialize)]
855pub struct OperatingFinancial {
856    /// Ticker code (may be empty)
857    pub code: String,
858    /// Symbol in `CODE.MARKET` format (may be empty)
859    pub symbol: String,
860    /// Reporting currency
861    pub currency: String,
862    /// Company name
863    pub name: String,
864    /// Market region
865    pub region: String,
866    /// Report period code
867    pub report: String,
868    /// Report period display text
869    pub report_txt: String,
870    /// Financial indicators
871    pub indicators: Vec<OperatingIndicator>,
872}
873
874/// One financial indicator in an operating report
875#[derive(Debug, Clone, Serialize, Deserialize)]
876pub struct OperatingIndicator {
877    /// Field name key, e.g. `"operating_revenue"`
878    pub field_name: String,
879    /// Display name, e.g. `"营业收入"`
880    pub indicator_name: String,
881    /// Formatted value, e.g. `"8217 亿"`
882    pub indicator_value: String,
883    /// Year-over-year change
884    #[serde(default, with = "crate::serde_utils::decimal_opt_str_is_none")]
885    pub yoy: Option<Decimal>,
886}
887
888// ── buyback ───────────────────────────────────────────────────────
889
890/// Response for [`crate::FundamentalContext::buyback`]
891#[derive(Debug, Clone, Serialize, Deserialize)]
892pub struct BuybackData {
893    /// Most recent buyback summary (TTM)
894    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
895    pub recent_buybacks: Option<RecentBuybacks>,
896    /// Historical annual buyback data
897    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
898    pub buyback_history: Vec<BuybackHistoryItem>,
899    /// Buyback payout and cash-flow ratios
900    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
901    pub buyback_ratios: Vec<BuybackRatios>,
902}
903
904/// TTM (trailing twelve months) buyback summary
905#[derive(Debug, Clone, Serialize, Deserialize)]
906pub struct RecentBuybacks {
907    /// Reporting currency
908    pub currency: String,
909    /// Net buyback amount TTM
910    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
911    pub net_buyback_ttm: Option<Decimal>,
912    /// Net buyback yield TTM
913    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
914    pub net_buyback_yield_ttm: Option<Decimal>,
915}
916
917/// Historical annual buyback data point
918#[derive(Debug, Clone, Serialize, Deserialize)]
919pub struct BuybackHistoryItem {
920    /// Fiscal year label, e.g. `"FY2024"`
921    pub fiscal_year: String,
922    /// Fiscal year date range string
923    pub fiscal_year_range: String,
924    /// Net buyback amount
925    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
926    pub net_buyback: Option<Decimal>,
927    /// Net buyback yield
928    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
929    pub net_buyback_yield: Option<Decimal>,
930    /// Year-over-year net buyback growth rate
931    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
932    pub net_buyback_growth_rate: Option<Decimal>,
933    /// Reporting currency
934    pub currency: String,
935}
936
937/// Buyback payout and cash-flow ratios
938#[derive(Debug, Clone, Serialize, Deserialize)]
939pub struct BuybackRatios {
940    /// Net buyback payout ratio
941    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
942    pub net_buyback_payout_ratio: Option<Decimal>,
943    /// Net buyback to free cash-flow ratio
944    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
945    pub net_buyback_to_cashflow_ratio: Option<Decimal>,
946}
947
948// ── ratings ───────────────────────────────────────────────────────
949
950/// Response for [`crate::FundamentalContext::ratings`]
951#[derive(Debug, Clone, Serialize, Deserialize)]
952pub struct StockRatings {
953    /// Style display name
954    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
955    pub style_txt_name: String,
956    /// Scale display name
957    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
958    pub scale_txt_name: String,
959    /// Report period display text
960    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
961    pub report_period_txt: String,
962    /// Composite score
963    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
964    pub multi_score: Option<f64>,
965    /// Composite score letter grade
966    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
967    pub multi_letter: String,
968    /// Score change vs previous period
969    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
970    pub multi_score_change: i32,
971    /// Industry name
972    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
973    pub industry_name: String,
974    /// Industry rank
975    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
976    pub industry_rank: Option<i64>,
977    /// Total securities in the industry
978    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
979    pub industry_total: Option<i64>,
980    /// Industry mean score
981    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
982    pub industry_mean_score: Option<f64>,
983    /// Industry median score
984    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
985    pub industry_median_score: Option<f64>,
986    /// Detailed rating categories
987    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
988    pub ratings: Vec<RatingCategory>,
989}
990
991/// One rating category (e.g. growth, profitability)
992#[derive(Debug, Clone, Serialize, Deserialize)]
993pub struct RatingCategory {
994    /// Category type code
995    #[serde(rename = "type")]
996    pub kind: i32,
997    /// Sub-indicator groups within this category
998    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
999    pub sub_indicators: Vec<RatingSubIndicatorGroup>,
1000}
1001
1002/// A group of sub-indicators under one category indicator
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004pub struct RatingSubIndicatorGroup {
1005    /// Parent indicator for this group
1006    pub indicator: RatingIndicator,
1007    /// Leaf sub-indicators
1008    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1009    pub sub_indicators: Vec<RatingLeafIndicator>,
1010}
1011
1012/// A rating indicator node (may be a parent or a leaf)
1013#[derive(Debug, Clone, Serialize, Deserialize)]
1014pub struct RatingIndicator {
1015    /// Indicator display name
1016    pub name: String,
1017    /// Score
1018    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1019    pub score: Option<f64>,
1020    /// Letter grade
1021    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1022    pub letter: String,
1023}
1024
1025/// A leaf rating indicator with a raw value
1026#[derive(Debug, Clone, Serialize, Deserialize)]
1027pub struct RatingLeafIndicator {
1028    /// Indicator display name
1029    pub name: String,
1030    /// Formatted value string
1031    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1032    pub value: String,
1033    /// Value type hint, e.g. `"percent"`
1034    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1035    pub value_type: String,
1036    /// Score
1037    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1038    pub score: Option<f64>,
1039    /// Letter grade
1040    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1041    pub letter: String,
1042}
1043
1044// ── enums ─────────────────────────────────────────────────────────
1045
1046/// Institutional analyst recommendation
1047#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
1048pub enum InstitutionRecommend {
1049    /// Unknown
1050    Unknown,
1051    /// Strong buy
1052    #[strum(serialize = "strong_buy")]
1053    StrongBuy,
1054    /// Buy
1055    #[strum(serialize = "buy")]
1056    Buy,
1057    /// Hold
1058    #[strum(serialize = "hold")]
1059    Hold,
1060    /// Sell
1061    #[strum(serialize = "sell")]
1062    Sell,
1063    /// Strong sell
1064    #[strum(serialize = "strong_sell")]
1065    StrongSell,
1066    /// Underperform
1067    #[strum(serialize = "underperform")]
1068    Underperform,
1069    /// No opinion
1070    #[strum(serialize = "no_opinion")]
1071    NoOpinion,
1072}
1073
1074impl_default_for_enum_string!(InstitutionRecommend);
1075impl_serde_for_enum_string!(InstitutionRecommend);
1076
1077/// Financial report kind
1078#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
1079pub enum FinancialReportKind {
1080    /// Income statement
1081    #[serde(rename = "IS")]
1082    IncomeStatement,
1083    /// Balance sheet
1084    #[serde(rename = "BS")]
1085    BalanceSheet,
1086    /// Cash flow statement
1087    #[serde(rename = "CF")]
1088    CashFlow,
1089    /// All statements
1090    #[default]
1091    #[serde(rename = "ALL")]
1092    All,
1093}
1094
1095/// Financial statement kind
1096///
1097/// Used by [`crate::FundamentalContext::us_financial_statement`], which needs
1098/// exactly one statement per request.  Unlike [`FinancialReportKind`] there is
1099/// deliberately no `All` variant and no `Default`: the endpoint answers with an
1100/// empty list for `ALL` (and for a missing `kind`), so a default would silently
1101/// return nothing.
1102#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
1103pub enum FinancialStatementKind {
1104    /// Income statement
1105    #[serde(rename = "IS")]
1106    IncomeStatement,
1107    /// Balance sheet
1108    #[serde(rename = "BS")]
1109    BalanceSheet,
1110    /// Cash flow statement
1111    #[serde(rename = "CF")]
1112    CashFlow,
1113}
1114
1115impl FinancialStatementKind {
1116    /// The value sent as the `kind` query parameter.
1117    #[inline]
1118    pub fn as_str(&self) -> &'static str {
1119        match self {
1120            FinancialStatementKind::IncomeStatement => "IS",
1121            FinancialStatementKind::BalanceSheet => "BS",
1122            FinancialStatementKind::CashFlow => "CF",
1123        }
1124    }
1125}
1126
1127/// Ranking indicator for [`crate::FundamentalContext::industry_rank`]
1128#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
1129pub enum IndustryRankIndicator {
1130    /// Leading gainer
1131    #[serde(rename = "leading-gainer")]
1132    LeadingGainer,
1133    /// Today's trend
1134    #[serde(rename = "today-trend")]
1135    TodayTrend,
1136    /// Popularity
1137    #[serde(rename = "popularity")]
1138    Popularity,
1139    /// Market capitalisation
1140    #[serde(rename = "market-cap")]
1141    MarketCap,
1142    /// Revenue
1143    #[serde(rename = "revenue")]
1144    Revenue,
1145    /// Revenue growth
1146    #[serde(rename = "revenue-growth")]
1147    RevenueGrowth,
1148    /// Net profit
1149    #[serde(rename = "net-profit")]
1150    NetProfit,
1151    /// Net profit growth
1152    #[serde(rename = "net-profit-growth")]
1153    NetProfitGrowth,
1154}
1155
1156impl IndustryRankIndicator {
1157    /// The value sent as the `indicator` query parameter.
1158    #[inline]
1159    pub fn as_str(&self) -> &'static str {
1160        match self {
1161            IndustryRankIndicator::LeadingGainer => "leading-gainer",
1162            IndustryRankIndicator::TodayTrend => "today-trend",
1163            IndustryRankIndicator::Popularity => "popularity",
1164            IndustryRankIndicator::MarketCap => "market-cap",
1165            IndustryRankIndicator::Revenue => "revenue",
1166            IndustryRankIndicator::RevenueGrowth => "revenue-growth",
1167            IndustryRankIndicator::NetProfit => "net-profit",
1168            IndustryRankIndicator::NetProfitGrowth => "net-profit-growth",
1169        }
1170    }
1171}
1172
1173/// Sort mode for [`crate::FundamentalContext::industry_rank`]
1174#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
1175pub enum IndustryRankSortType {
1176    /// Rank by the single selected indicator
1177    #[serde(rename = "single")]
1178    Single,
1179    /// Rank by a composite of several indicators
1180    #[serde(rename = "multi")]
1181    Multi,
1182}
1183
1184impl IndustryRankSortType {
1185    /// The value sent as the `sort_type` query parameter.
1186    #[inline]
1187    pub fn as_str(&self) -> &'static str {
1188        match self {
1189            IndustryRankSortType::Single => "single",
1190            IndustryRankSortType::Multi => "multi",
1191        }
1192    }
1193}
1194
1195// ── business_segments ─────────────────────────────────────────────
1196
1197/// Response for [`crate::FundamentalContext::business_segments`]
1198#[derive(Debug, Clone, Serialize, Deserialize)]
1199pub struct BusinessSegments {
1200    /// Report date
1201    pub date: String,
1202    /// Total revenue
1203    pub total: String,
1204    /// Reporting currency
1205    pub currency: String,
1206    /// Business segment breakdown
1207    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1208    pub business: Vec<BusinessSegmentItem>,
1209}
1210
1211/// One business segment item (latest snapshot)
1212#[derive(Debug, Clone, Serialize, Deserialize)]
1213pub struct BusinessSegmentItem {
1214    /// Segment name
1215    pub name: String,
1216    /// Percentage of total revenue
1217    pub percent: String,
1218}
1219
1220/// Response for [`crate::FundamentalContext::business_segments_history`]
1221#[derive(Debug, Clone, Serialize, Deserialize)]
1222pub struct BusinessSegmentsHistory {
1223    /// Historical snapshots
1224    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1225    pub historical: Vec<BusinessSegmentsHistoricalItem>,
1226}
1227
1228/// One historical business segments snapshot
1229#[derive(Debug, Clone, Serialize, Deserialize)]
1230pub struct BusinessSegmentsHistoricalItem {
1231    /// Report date
1232    pub date: String,
1233    /// Total revenue
1234    pub total: String,
1235    /// Reporting currency
1236    pub currency: String,
1237    /// Business segment breakdown
1238    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1239    pub business: Vec<BusinessSegmentHistoryItem>,
1240    /// Regional breakdown
1241    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1242    pub regionals: Vec<BusinessSegmentHistoryItem>,
1243}
1244
1245/// One business/regional segment item in a historical snapshot
1246#[derive(Debug, Clone, Serialize, Deserialize)]
1247pub struct BusinessSegmentHistoryItem {
1248    /// Segment name
1249    pub name: String,
1250    /// Percentage of total
1251    pub percent: String,
1252    /// Absolute value
1253    pub value: String,
1254}
1255
1256// ── institution_rating_views ──────────────────────────────────────
1257
1258/// Response for [`crate::FundamentalContext::institution_rating_views`]
1259#[derive(Debug, Clone, Serialize, Deserialize)]
1260pub struct InstitutionRatingViews {
1261    /// Historical rating distribution snapshots
1262    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1263    pub elist: Vec<InstitutionRatingViewItem>,
1264}
1265
1266/// One historical rating distribution snapshot
1267#[derive(Debug, Clone, Serialize, Deserialize)]
1268pub struct InstitutionRatingViewItem {
1269    /// Date as unix timestamp string (API returns as quoted or bare integer)
1270    pub date: String,
1271    /// Number of "Buy" ratings (API returns as string)
1272    pub buy: String,
1273    /// Number of "Outperform" ratings (API returns as string)
1274    pub over: String,
1275    /// Number of "Hold" ratings (API returns as string)
1276    pub hold: String,
1277    /// Number of "Underperform" ratings (API returns as string)
1278    pub under: String,
1279    /// Number of "Sell" ratings (API returns as string)
1280    pub sell: String,
1281    /// Total analyst count (API returns as string)
1282    pub total: String,
1283}
1284
1285// ── industry_rank ─────────────────────────────────────────────────
1286
1287/// Response for [`crate::FundamentalContext::industry_rank`]
1288#[derive(Debug, Clone, Serialize, Deserialize)]
1289pub struct IndustryRankResponse {
1290    /// Grouped rank items
1291    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1292    pub items: Vec<IndustryRankGroup>,
1293}
1294
1295/// A group of ranked industry items
1296#[derive(Debug, Clone, Serialize, Deserialize)]
1297pub struct IndustryRankGroup {
1298    /// Items in this group
1299    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1300    pub lists: Vec<IndustryRankItem>,
1301}
1302
1303/// One ranked industry item
1304#[derive(Debug, Clone, Serialize, Deserialize)]
1305pub struct IndustryRankItem {
1306    /// Industry / sector name
1307    pub name: String,
1308    /// Industry symbol
1309    pub symbol: String,
1310    /// Change percentage
1311    pub chg: String,
1312    /// Name of the leading stock
1313    pub leading_name: String,
1314    /// Ticker of the leading stock
1315    pub leading_ticker: String,
1316    /// Change percentage of the leading stock
1317    pub leading_chg: String,
1318    /// Value label name
1319    pub value_name: String,
1320    /// Value data
1321    pub value_data: String,
1322}
1323
1324// ── industry_peers ────────────────────────────────────────────────
1325
1326/// Response for [`crate::FundamentalContext::industry_peers`]
1327#[derive(Debug, Clone, Serialize, Deserialize)]
1328pub struct IndustryPeersResponse {
1329    /// Top-level industry node info (may be absent if no data)
1330    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1331    pub top: Option<IndustryPeersTop>,
1332    /// Root peer chain node (may be absent if no data)
1333    pub chain: Option<IndustryPeerNode>,
1334}
1335
1336/// Top-level industry info in the peers response
1337#[derive(Debug, Clone, Serialize, Deserialize)]
1338pub struct IndustryPeersTop {
1339    /// Industry name
1340    pub name: String,
1341    /// Market code
1342    pub market: String,
1343}
1344
1345/// A node in the recursive industry peer chain
1346#[derive(Debug, Clone, Serialize, Deserialize)]
1347pub struct IndustryPeerNode {
1348    /// Node name
1349    pub name: String,
1350    /// Node symbol
1351    pub symbol: String,
1352    /// Number of stocks in this node (API returns as integer)
1353    pub stock_num: i32,
1354    /// Change percentage
1355    pub chg: String,
1356    /// Year-to-date change
1357    pub ytd_chg: String,
1358    /// Child nodes (recursive)
1359    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1360    pub next: Vec<IndustryPeerNode>,
1361}
1362
1363// ── financial_report_snapshot ─────────────────────────────────────
1364
1365/// Response for [`crate::FundamentalContext::financial_report_snapshot`]
1366#[derive(Debug, Clone, Serialize, Deserialize)]
1367pub struct FinancialReportSnapshot {
1368    /// Company name
1369    pub name: String,
1370    /// Ticker code
1371    pub ticker: String,
1372    /// Fiscal period start date
1373    pub fp_start: String,
1374    /// Fiscal period end date
1375    pub fp_end: String,
1376    /// Reporting currency
1377    pub currency: String,
1378    /// Report description
1379    pub report_desc: String,
1380    /// Forecast revenue
1381    pub fo_revenue: Option<SnapshotForecastMetric>,
1382    /// Forecast EBIT
1383    pub fo_ebit: Option<SnapshotForecastMetric>,
1384    /// Forecast EPS
1385    pub fo_eps: Option<SnapshotForecastMetric>,
1386    /// Reported revenue
1387    pub fr_revenue: Option<SnapshotReportedMetric>,
1388    /// Reported net profit
1389    pub fr_profit: Option<SnapshotReportedMetric>,
1390    /// Reported operating cash flow
1391    pub fr_operate_cash: Option<SnapshotReportedMetric>,
1392    /// Reported investing cash flow
1393    pub fr_invest_cash: Option<SnapshotReportedMetric>,
1394    /// Reported financing cash flow
1395    pub fr_finance_cash: Option<SnapshotReportedMetric>,
1396    /// Reported total assets
1397    pub fr_total_assets: Option<SnapshotReportedMetric>,
1398    /// Reported total liabilities
1399    pub fr_total_liability: Option<SnapshotReportedMetric>,
1400    /// ROE TTM
1401    pub fr_roe_ttm: String,
1402    /// Profit margin
1403    pub fr_profit_margin: String,
1404    /// Profit margin TTM
1405    pub fr_profit_margin_ttm: String,
1406    /// Asset turnover TTM
1407    pub fr_asset_turn_ttm: String,
1408    /// Leverage TTM
1409    pub fr_leverage_ttm: String,
1410    /// Debt-to-assets ratio
1411    pub fr_debt_assets_ratio: String,
1412}
1413
1414/// A forecast metric in the financial report snapshot
1415#[derive(Debug, Clone, Serialize, Deserialize)]
1416pub struct SnapshotForecastMetric {
1417    /// Actual value
1418    pub value: String,
1419    /// Year-over-year change
1420    pub yoy: String,
1421    /// Beat/miss description
1422    pub cmp_desc: String,
1423    /// Consensus estimate value
1424    pub est_value: String,
1425}
1426
1427/// A reported metric in the financial report snapshot
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1429pub struct SnapshotReportedMetric {
1430    /// Actual value
1431    pub value: String,
1432    /// Year-over-year change
1433    pub yoy: String,
1434}
1435
1436// ── shareholder_top ───────────────────────────────────────────────
1437
1438/// Response for [`crate::FundamentalContext::shareholder_top`]
1439#[derive(Debug, Clone, Serialize, Deserialize)]
1440pub struct ShareholderTopResponse {
1441    /// Raw top-shareholder data
1442    pub data: serde_json::Value,
1443}
1444
1445// ── shareholder_detail ────────────────────────────────────────────
1446
1447/// Response for [`crate::FundamentalContext::shareholder_detail`]
1448#[derive(Debug, Clone, Serialize, Deserialize)]
1449pub struct ShareholderDetailResponse {
1450    /// Raw shareholder detail data
1451    pub data: serde_json::Value,
1452}
1453
1454// ── valuation_comparison ──────────────────────────────────────────
1455
1456/// One historical valuation data point.
1457#[derive(Debug, Clone, Serialize, Deserialize)]
1458pub struct ValuationHistoryPoint {
1459    /// Date (RFC 3339, converted from Unix timestamp)
1460    pub date: String,
1461    /// P/E ratio
1462    pub pe: String,
1463    /// P/B ratio
1464    pub pb: String,
1465    /// P/S ratio
1466    pub ps: String,
1467}
1468
1469/// One security's valuation comparison item.
1470#[derive(Debug, Clone, Serialize, Deserialize)]
1471pub struct ValuationComparisonItem {
1472    /// Symbol
1473    pub symbol: String,
1474    /// Security name
1475    pub name: String,
1476    /// Currency
1477    pub currency: String,
1478    /// Market capitalisation
1479    pub market_value: String,
1480    /// Latest closing price
1481    pub price_close: String,
1482    /// P/E ratio
1483    pub pe: String,
1484    /// P/B ratio
1485    pub pb: String,
1486    /// P/S ratio
1487    pub ps: String,
1488    /// Return on equity
1489    pub roe: String,
1490    /// Earnings per share
1491    pub eps: String,
1492    /// Book value per share
1493    pub bps: String,
1494    /// Dividends per share
1495    pub dps: String,
1496    /// Dividend yield
1497    pub div_yld: String,
1498    /// Total assets
1499    pub assets: String,
1500    /// Historical valuation points
1501    pub history: Vec<ValuationHistoryPoint>,
1502}
1503
1504/// Response for [`crate::FundamentalContext::valuation_comparison`]
1505#[derive(Debug, Clone, Serialize, Deserialize)]
1506pub struct ValuationComparisonResponse {
1507    /// Valuation comparison items
1508    pub list: Vec<ValuationComparisonItem>,
1509}
1510
1511/// Financial report period type
1512#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
1513pub enum FinancialReportPeriod {
1514    /// Annual report
1515    #[serde(rename = "af")]
1516    Annual,
1517    /// Semi-annual report
1518    #[serde(rename = "saf")]
1519    SemiAnnual,
1520    /// Q1 report
1521    #[serde(rename = "q1")]
1522    Q1,
1523    /// Q2 report
1524    #[serde(rename = "q2")]
1525    Q2,
1526    /// Q3 report
1527    #[serde(rename = "q3")]
1528    Q3,
1529    /// Full quarterly report
1530    #[serde(rename = "qf")]
1531    QuarterlyFull,
1532    /// Three-quarter report (first three quarters)
1533    #[serde(rename = "3q")]
1534    ThreeQ,
1535}
1536
1537// ── etf_asset_allocation ──────────────────────────────────────────
1538
1539/// ETF asset allocation element type
1540#[derive(Debug, FromPrimitive, IntoPrimitive, Copy, Clone, Hash, Eq, PartialEq)]
1541#[repr(i32)]
1542pub enum ElementType {
1543    /// Unknown
1544    #[num_enum(default)]
1545    Unknown = 0,
1546    /// Holdings
1547    Holdings = 1,
1548    /// Regional
1549    Regional = 2,
1550    /// Asset class
1551    AssetClass = 3,
1552    /// Industry
1553    Industry = 4,
1554}
1555
1556impl Serialize for ElementType {
1557    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1558    where
1559        S: serde::Serializer,
1560    {
1561        serializer.serialize_i32((*self).into())
1562    }
1563}
1564
1565impl<'de> Deserialize<'de> for ElementType {
1566    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1567    where
1568        D: serde::Deserializer<'de>,
1569    {
1570        Ok(ElementType::from(i32::deserialize(deserializer)?))
1571    }
1572}
1573
1574/// Holding detail of an ETF asset allocation element (holdings only)
1575#[derive(Debug, Clone, Serialize, Deserialize)]
1576pub struct HoldingDetail {
1577    /// Industry ID
1578    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1579    pub industry_id: String,
1580    /// Industry name
1581    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1582    pub industry_name: String,
1583    /// Index counter ID (e.g. `BK/US/CP99000`)
1584    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1585    pub index: String,
1586    /// Index name
1587    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1588    pub index_name: String,
1589    /// Holding type (e.g. `E` for stock)
1590    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1591    pub holding_type: String,
1592    /// Holding type name
1593    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1594    pub holding_type_name: String,
1595}
1596
1597/// One element of an ETF asset allocation group
1598#[derive(Debug, Clone, Serialize, Deserialize)]
1599pub struct AssetAllocationItem {
1600    /// Element name
1601    pub name: String,
1602    /// Security code (holdings only, e.g. `NVDA`)
1603    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1604    pub code: String,
1605    /// Position ratio (e.g. `0.0861114`)
1606    pub position_ratio: String,
1607    /// Security symbol (holdings only, e.g. `NVDA.US`)
1608    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1609    pub symbol: String,
1610    /// Localized names (locale → name, e.g. `zh-CN` → `英伟达`)
1611    #[serde(rename = "name_locales_map", default)]
1612    pub name_locales: HashMap<String, String>,
1613    /// Holding detail (holdings only)
1614    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1615    pub holding_detail: Option<HoldingDetail>,
1616}
1617
1618/// One ETF asset allocation group (grouped by element type)
1619#[derive(Debug, Clone, Serialize, Deserialize)]
1620pub struct AssetAllocationGroup {
1621    /// Report date (e.g. `20260601`)
1622    pub report_date: String,
1623    /// Element type of this group
1624    pub asset_type: ElementType,
1625    /// Elements
1626    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1627    pub lists: Vec<AssetAllocationItem>,
1628}
1629
1630/// Response for [`crate::FundamentalContext::etf_asset_allocation`]
1631#[derive(Debug, Clone, Serialize, Deserialize)]
1632pub struct AssetAllocationResponse {
1633    /// Asset allocation groups
1634    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1635    pub info: Vec<AssetAllocationGroup>,
1636}
1637
1638// ── macroeconomic ─────────────────────────────────────────────────────
1639
1640/// Country for filtering macroeconomic indicators
1641#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
1642pub enum MacroeconomicCountry {
1643    /// Hong Kong SAR China
1644    #[serde(rename = "Hong Kong SAR China")]
1645    HongKong,
1646    /// China (Mainland)
1647    #[serde(rename = "China (Mainland)")]
1648    China,
1649    /// United States
1650    #[serde(rename = "United States")]
1651    UnitedStates,
1652    /// Euro Zone
1653    #[serde(rename = "Euro Zone")]
1654    EuroZone,
1655    /// Japan
1656    #[serde(rename = "Japan")]
1657    Japan,
1658    /// Singapore
1659    #[serde(rename = "Singapore")]
1660    Singapore,
1661}
1662
1663/// Importance level of a macroeconomic indicator
1664#[derive(Debug, Copy, Clone, Eq, PartialEq)]
1665pub enum MacroeconomicImportance {
1666    /// Low importance
1667    Low = 1,
1668    /// Medium importance
1669    Medium = 2,
1670    /// High importance
1671    High = 3,
1672}
1673
1674impl MacroeconomicImportance {
1675    /// Convert from raw API integer value
1676    pub fn from_i32(v: i32) -> Option<Self> {
1677        match v {
1678            1 => Some(Self::Low),
1679            2 => Some(Self::Medium),
1680            3 => Some(Self::High),
1681            _ => None,
1682        }
1683    }
1684}
1685
1686/// Localized text in simplified Chinese, traditional Chinese, and English
1687#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1688pub struct MultiLanguageText {
1689    /// English
1690    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1691    pub english: String,
1692    /// Simplified Chinese
1693    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1694    pub simplified_chinese: String,
1695    /// Traditional Chinese
1696    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1697    pub traditional_chinese: String,
1698}
1699
1700/// Metadata for one macroeconomic indicator
1701#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1702pub struct MacroeconomicIndicator {
1703    /// External vendor code (used as input to `macroeconomic`)
1704    pub indicator_code: String,
1705    /// Publishing organisation
1706    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1707    pub source_org: String,
1708    /// Country
1709    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1710    pub country: String,
1711    /// Indicator name
1712    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1713    pub name: String,
1714    /// Adjustment factor
1715    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1716    pub adjustment_factor: String,
1717    /// Release periodicity (e.g. `monthly` / `quarterly`)
1718    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1719    pub periodicity: String,
1720    /// Indicator category
1721    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1722    pub category: String,
1723    /// Description
1724    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1725    pub describe: String,
1726    /// Importance — higher is more important
1727    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1728    pub importance: i32,
1729    /// Start date of data coverage
1730    #[serde(
1731        default,
1732        with = "crate::serde_utils::rfc3339_opt",
1733        rename = "start_date"
1734    )]
1735    pub start_date: Option<OffsetDateTime>,
1736}
1737
1738/// Response for [`crate::FundamentalContext::macroeconomic_indicators`]
1739#[derive(Debug, Clone, Serialize, Deserialize)]
1740pub struct MacroeconomicIndicatorListResponse {
1741    /// Indicator list
1742    #[serde(
1743        default,
1744        deserialize_with = "crate::serde_utils::null_as_default",
1745        rename = "list"
1746    )]
1747    pub data: Vec<MacroeconomicIndicator>,
1748    /// Total number of indicators matching the query
1749    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1750    pub count: i32,
1751}
1752
1753/// One historical data point for a macroeconomic indicator
1754#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1755pub struct Macroeconomic {
1756    /// Statistical period (e.g. `2024-Q1`, `2024-03`)
1757    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1758    pub period: String,
1759    /// Release datetime
1760    #[serde(default, with = "crate::serde_utils::rfc3339_opt")]
1761    pub release_at: Option<OffsetDateTime>,
1762    /// Actual value
1763    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1764    pub actual_value: String,
1765    /// Previous value
1766    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1767    pub previous_value: String,
1768    /// Forecast value (market consensus)
1769    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1770    pub forecast_value: String,
1771    /// Revised value
1772    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1773    pub revised_value: String,
1774    /// Next release datetime
1775    #[serde(default, with = "crate::serde_utils::rfc3339_opt")]
1776    pub next_release_at: Option<OffsetDateTime>,
1777    /// Unit
1778    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1779    pub unit: String,
1780    /// Unit prefix / data scale (e.g. millions / billions)
1781    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1782    pub unit_prefix: String,
1783}
1784
1785/// Response for [`crate::FundamentalContext::macroeconomic`]
1786#[derive(Debug, Clone, Serialize, Deserialize)]
1787pub struct MacroeconomicResponse {
1788    /// Indicator metadata
1789    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1790    pub info: MacroeconomicIndicator,
1791    /// Historical data points
1792    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1793    pub data: Vec<Macroeconomic>,
1794    /// Total number of historical data points
1795    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1796    pub count: i32,
1797}
1798
1799// ── US-market types
1800// ───────────────────────────────────────────────────────────
1801
1802/// Industry rank tag returned by
1803/// [`crate::FundamentalContext::us_company_overview`].
1804#[derive(Debug, Clone, Serialize, Deserialize)]
1805pub struct USRankTag {
1806    /// Tag key
1807    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1808    pub key: String,
1809    /// Display position / ordering
1810    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1811    pub location: i32,
1812    /// Tag title
1813    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1814    pub title: String,
1815    /// Tag text
1816    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1817    pub text: String,
1818    /// Rank type code
1819    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1820    pub rank_type: i32,
1821    /// Highlighted portion of the text
1822    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1823    pub highlight_text: String,
1824}
1825
1826/// One entry in [`USCompanyOverview`]'s sharelist field.
1827#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1828pub struct USSharelistItem {
1829    /// Price change ratio
1830    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1831    pub chg: String,
1832    /// Sharelist ID
1833    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1834    pub id: String,
1835    /// Sharelist name
1836    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1837    pub name: String,
1838}
1839
1840/// Response for [`crate::FundamentalContext::us_company_overview`].
1841#[derive(Debug, Clone, Serialize, Deserialize)]
1842pub struct USCompanyOverview {
1843    /// Company introduction / description
1844    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1845    pub intro: String,
1846    /// Market capitalisation
1847    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1848    pub market_cap: String,
1849    /// Currency symbol (e.g. `"$"`)
1850    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1851    pub ccy_symbol: String,
1852    /// Top industry-rank tags
1853    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1854    pub top_rank_tags: Vec<USRankTag>,
1855    /// Detail page URL
1856    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1857    pub detail_url: String,
1858    /// Related sharelists
1859    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1860    pub share_list: Vec<USSharelistItem>,
1861}
1862
1863/// One valuation metric entry within [`USValuationOverview`].
1864#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1865pub struct USValuationMetric {
1866    /// Position on the percentile circle / gauge
1867    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1868    pub circle: String,
1869    /// Segment / part label
1870    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1871    pub part: String,
1872    /// Metric value
1873    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1874    pub metric: String,
1875    /// Metric description
1876    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1877    pub desc: String,
1878    /// Industry median for this metric
1879    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1880    pub industry_median: String,
1881}
1882
1883/// Response for [`crate::FundamentalContext::us_valuation_overview`].
1884#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1885pub struct USValuationOverview {
1886    /// Map of metric key (e.g. "pe") to its valuation data
1887    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1888    pub metrics: std::collections::HashMap<String, USValuationMetric>,
1889    /// Active indicator key (e.g. "pe")
1890    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1891    pub indicator: String,
1892    /// Historical percentile range in years
1893    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1894    pub range: i32,
1895    /// Data date string
1896    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1897    pub date: String,
1898    /// Currency symbol (e.g. `"$"`)
1899    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1900    pub ccy_symbol: String,
1901    /// Embedded AI-chat context
1902    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1903    pub aichat_data: USAIChatData,
1904    /// AI-generated summary text
1905    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1906    pub ai_summary: String,
1907}
1908
1909/// One financial field within a [`USFinancialStatementPeriod`].
1910#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1911pub struct USFinancialStatementField {
1912    /// Display order within the statement
1913    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1914    pub display_order: i32,
1915    /// Field code
1916    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1917    pub field: String,
1918    /// Field ID
1919    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1920    pub id: String,
1921    /// Nesting level (for indentation)
1922    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1923    pub level: i64,
1924    /// Field display name
1925    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1926    pub name: String,
1927    /// Field value
1928    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1929    pub value: String,
1930    /// Value type hint
1931    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1932    pub value_type: String,
1933    /// Year-over-year change
1934    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1935    pub yoy: String,
1936}
1937
1938/// One reporting period in [`USFinancialStatement`].
1939#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1940pub struct USFinancialStatementPeriod {
1941    /// Fiscal period (e.g. `"Q1"`, `"FY"`)
1942    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1943    pub ff_period: String,
1944    /// Fiscal year
1945    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1946    pub ff_year: i32,
1947    /// Statement fields for this period
1948    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1949    pub fields: Vec<USFinancialStatementField>,
1950    /// Fiscal-period end date
1951    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1952    pub fp_end: String,
1953    /// Report label text
1954    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1955    pub report_txt: String,
1956    /// Report publication date
1957    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1958    pub rpt_date: String,
1959}
1960
1961/// Response for [`crate::FundamentalContext::us_financial_statement`].
1962#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1963pub struct USFinancialStatement {
1964    /// Reporting currency
1965    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1966    pub currency: String,
1967    /// Report type
1968    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1969    pub report: String,
1970    /// Per-period statement data
1971    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1972    pub list: Vec<USFinancialStatementPeriod>,
1973    /// Field codes that have no data
1974    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1975    pub empty_fields: Vec<String>,
1976}
1977
1978/// Per-fiscal-year dividend records for a US ETF.
1979#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1980pub struct USFiscalYearDividend {
1981    /// Dividend amount for the fiscal year
1982    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1983    pub dividend: String,
1984    /// Dividend yield for the fiscal year
1985    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1986    pub dividend_yield: String,
1987    /// Fiscal year
1988    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1989    pub fiscal_year: String,
1990    /// Currency
1991    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1992    pub currency: String,
1993    /// Fiscal-year date range
1994    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1995    pub fiscal_year_range: String,
1996}
1997
1998/// Response for [`crate::FundamentalContext::us_etf_dividend_info`].
1999#[derive(Debug, Clone, Serialize, Deserialize)]
2000pub struct USETFDividendInfo {
2001    /// Total dividend over trailing 12 months
2002    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2003    pub dividend_ttm: String,
2004    /// Dividend yield over trailing 12 months
2005    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2006    pub dividend_yield_ttm: String,
2007    /// Dividend frequency
2008    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2009    pub dividend_frequency: String,
2010    /// Currency
2011    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2012    pub currency: String,
2013    /// Per-fiscal-year dividend records
2014    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2015    pub fiscal_year_info: Vec<USFiscalYearDividend>,
2016}
2017
2018/// A single historical dividend payment.
2019#[derive(Debug, Clone, Serialize, Deserialize)]
2020pub struct USDividendItem {
2021    /// Per-share dividend amount
2022    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2023    pub dividend: String,
2024    /// Dividend type (e.g. cash dividend)
2025    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2026    pub dividend_type: String,
2027    /// Ex-dividend date
2028    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2029    pub ex_date: String,
2030    /// Payment date
2031    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2032    pub payment_date: String,
2033    /// Record date
2034    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2035    pub record_date: String,
2036}
2037
2038/// Trailing-12-month dividend summary within [`USCompanyDividends`].
2039#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2040pub struct USRecentDividend {
2041    /// Total dividend over the trailing 12 months
2042    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2043    pub dividend_ttm: String,
2044    /// Dividend yield over the trailing 12 months
2045    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2046    pub dividend_yield_ttm: String,
2047    /// Number of payouts
2048    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2049    pub payouts: String,
2050    /// Currency
2051    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2052    pub currency: String,
2053}
2054
2055/// One fiscal-year row in the dividend history or payout-ratio table.
2056#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2057pub struct USDividendHistoryItem {
2058    /// Fiscal year
2059    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2060    pub fiscal_year: String,
2061    /// Fiscal-year date range
2062    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2063    pub fiscal_year_range: String,
2064    /// Total shareholder yield (dividend + buyback)
2065    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2066    pub total_shareholder_yield: String,
2067    /// Dividend amount
2068    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2069    pub dividend: String,
2070    /// Dividend yield
2071    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2072    pub dividend_yield: String,
2073    /// Dividend growth rate
2074    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2075    pub dividend_growth_rate: String,
2076    /// Dividend payout ratio
2077    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2078    pub dividend_payout_ratio: String,
2079    /// Dividend-to-cash-flow ratio
2080    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2081    pub dividend_to_cashflow_ratio: String,
2082    /// Net buyback amount
2083    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2084    pub net_buyback: String,
2085    /// Net buyback yield
2086    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2087    pub net_buyback_yield: String,
2088    /// Net buyback growth rate
2089    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2090    pub net_buyback_growth_rate: String,
2091    /// Net buyback payout ratio
2092    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2093    pub net_buyback_payout_ratio: String,
2094    /// Net-buyback-to-cash-flow ratio
2095    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2096    pub net_buyback_to_cashflow_ratio: String,
2097    /// Currency
2098    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2099    pub currency: String,
2100}
2101
2102/// One actual dividend payment event in [`USCompanyDividends`].
2103#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2104pub struct USDividendPayoutRecord {
2105    /// Per-share dividend amount
2106    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2107    pub dividend: String,
2108    /// Dividend type (e.g. cash dividend)
2109    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2110    pub dividend_type: String,
2111    /// Currency
2112    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2113    pub currency: String,
2114    /// Ex-dividend date
2115    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2116    pub ex_date: String,
2117    /// Payment date
2118    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2119    pub payment_date: String,
2120    /// Record date
2121    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2122    pub record_date: String,
2123    /// Display title
2124    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2125    pub title: String,
2126    /// Start time as a Unix-seconds string
2127    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2128    pub start_time_unix: String,
2129}
2130
2131/// Response for [`crate::FundamentalContext::us_company_dividends`].
2132#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2133pub struct USCompanyDividends {
2134    /// Trailing-12-month dividend summary
2135    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2136    pub recent_dividends: USRecentDividend,
2137    /// Per-fiscal-year dividend history
2138    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2139    pub dividend_history: Vec<USDividendHistoryItem>,
2140    /// Per-fiscal-year payout ratios
2141    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2142    pub payout_ratios: Vec<USDividendHistoryItem>,
2143    /// Individual dividend payout events
2144    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2145    pub dividend_payout_history: Vec<USDividendPayoutRecord>,
2146}
2147
2148/// A single file in an ETF document list.
2149#[derive(Debug, Clone, Serialize, Deserialize)]
2150pub struct USETFFile {
2151    /// File name
2152    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2153    pub file_name: String,
2154    /// File path / URL
2155    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2156    pub file_path: String,
2157    /// Last update date
2158    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2159    pub update_date: String,
2160    /// Document type code
2161    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2162    pub code: String,
2163    /// File format (e.g. `"pdf"`)
2164    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2165    pub format: String,
2166}
2167
2168/// Response for [`crate::FundamentalContext::us_etf_files`].
2169#[derive(Debug, Clone, Serialize, Deserialize)]
2170pub struct USETFFilesResponse {
2171    /// List of ETF documents
2172    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2173    pub files: Vec<USETFFile>,
2174}
2175
2176// ── v2 wire types (internal, used for mapping to existing public types) ──────
2177
2178/// v2 wire: one indicator from GET /v2/quote/macrodata
2179#[derive(Debug, Clone, Serialize, Deserialize)]
2180pub(crate) struct V2MacroIndicator {
2181    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2182    pub indicator_id: i32,
2183    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2184    pub indicator_name: String,
2185    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2186    pub market: String,
2187    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2188    pub importance: i32,
2189    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2190    pub description: String,
2191    /// Update frequency: day/week/month/quarter/half_year/year
2192    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2193    pub frequence: String,
2194}
2195
2196/// v2 wire: response from GET /v2/quote/macrodata
2197#[derive(Debug, Clone, Serialize, Deserialize)]
2198pub(crate) struct V2MacroIndicatorListResponse {
2199    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2200    pub indicator_list: Vec<V2MacroIndicator>,
2201    /// Total count for pagination
2202    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2203    pub total: i32,
2204}
2205
2206/// v2 wire: one data point from GET /v2/quote/macrodata/:id
2207#[derive(Debug, Clone, Serialize, Deserialize)]
2208pub(crate) struct V2IndicatorDataDetail {
2209    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2210    pub actual_data: String,
2211    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2212    pub previous_data: String,
2213    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2214    pub estimated_data: String,
2215    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2216    pub published_time: String,
2217    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2218    pub observation_date: String,
2219}
2220
2221/// v2 wire: one indicator with data from GET /v2/quote/macrodata/:id
2222#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2223pub(crate) struct V2MacroIndicatorDetail {
2224    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2225    pub indicator_id: i32,
2226    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2227    pub indicator_name: String,
2228    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2229    pub unit: String,
2230    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2231    pub description: String,
2232    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2233    pub market: String,
2234    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2235    pub frequence: String,
2236    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2237    pub importance: i32,
2238    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2239    pub indicator_data: Vec<V2IndicatorDataDetail>,
2240}
2241
2242/// v2 wire: response from GET /v2/quote/macrodata/:id
2243/// (GetMacroIndicatorHistoryResp)
2244#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2245pub(crate) struct V2MacroIndicatorDataResponse {
2246    /// Single indicator with paginated data points
2247    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2248    pub indicator: V2MacroIndicatorDetail,
2249    /// Total data points matching the query (for pagination)
2250    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2251    pub total: i32,
2252}
2253
2254// ── US financial overview ─────────────────────────────────────────────────
2255
2256/// One reporting-period window shared by IS/BS/CF entries.
2257#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2258pub struct USReportPeriod {
2259    /// Period start date
2260    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2261    pub start_date: String,
2262    /// Period end date
2263    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2264    pub end_date: String,
2265    /// Report label text
2266    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2267    pub report_txt: String,
2268}
2269
2270/// One income-statement entry in [`USFinancialOverview`].
2271#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2272pub struct USFinancialISItem {
2273    /// Revenue
2274    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2275    pub revenue: String,
2276    /// Net income
2277    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2278    pub net_income: String,
2279    /// Net margin
2280    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2281    pub net_margin: String,
2282    /// Reporting period
2283    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2284    pub report: USReportPeriod,
2285}
2286
2287/// One balance-sheet entry in [`USFinancialOverview`].
2288#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2289pub struct USFinancialBSItem {
2290    /// Debt-to-assets ratio
2291    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2292    pub debt_assets_ratio: String,
2293    /// Total assets
2294    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2295    pub total_assets: String,
2296    /// Total liabilities
2297    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2298    pub total_liabilities: String,
2299    /// Reporting period
2300    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2301    pub report: USReportPeriod,
2302}
2303
2304/// One cash-flow entry in [`USFinancialOverview`].
2305#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2306pub struct USFinancialCFItem {
2307    /// Operating cash flow
2308    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2309    pub operating: String,
2310    /// Investing cash flow
2311    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2312    pub investing: String,
2313    /// Financing cash flow
2314    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2315    pub financing: String,
2316    /// Reporting period
2317    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2318    pub report: USReportPeriod,
2319}
2320
2321/// Response for [`crate::FundamentalContext::us_financial_overview`].
2322#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2323pub struct USFinancialOverview {
2324    /// Currency symbol (e.g. `"$"`)
2325    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2326    pub ccy_symbol: String,
2327    /// Report type
2328    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2329    pub report_type: String,
2330    /// Income-statement entries
2331    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2332    pub is_list: Vec<USFinancialISItem>,
2333    /// Balance-sheet entries
2334    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2335    pub bs_list: Vec<USFinancialBSItem>,
2336    /// Cash-flow entries
2337    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2338    pub cf_list: Vec<USFinancialCFItem>,
2339}
2340
2341// ── US key financial metrics ──────────────────────────────────────────────
2342
2343/// One period entry in [`USKeyFinancialMetrics`].
2344#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2345pub struct USKeyMetricItem {
2346    /// Fiscal period (e.g. `"Q1"`, `"FY"`)
2347    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2348    pub ff_period: String,
2349    /// Fiscal year
2350    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2351    pub ff_year: i32,
2352    /// Fiscal-period end date
2353    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2354    pub fp_end: String,
2355    /// Report label text
2356    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2357    pub report_txt: String,
2358    /// Report publication date
2359    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2360    pub rpt_date: String,
2361    /// Metric values — shape varies per field configuration.
2362    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2363    pub fields: Vec<serde_json::Value>,
2364}
2365
2366/// Response for [`crate::FundamentalContext::us_key_financial_metrics`].
2367#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2368pub struct USKeyFinancialMetrics {
2369    /// Reporting currency
2370    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2371    pub currency: String,
2372    /// Report type
2373    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2374    pub report: String,
2375    /// Field codes that have no data
2376    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2377    pub empty_fields: Vec<String>,
2378    /// Per-period metric data
2379    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2380    pub list: Vec<USKeyMetricItem>,
2381}
2382
2383// ── US analyst consensus ──────────────────────────────────────────────────
2384
2385/// AI chat context embedded in [`USAnalystConsensus`].
2386#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2387pub struct USAIChatData {
2388    /// AI agent ID
2389    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2390    pub agent_id: String,
2391    /// Handoff agent ID
2392    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2393    pub handoff_agent_id: String,
2394    /// Security symbol
2395    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2396    pub symbol: String,
2397    /// Prompt / context text
2398    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2399    pub text: String,
2400    /// Chat type
2401    #[serde(
2402        default,
2403        deserialize_with = "crate::serde_utils::null_as_default",
2404        rename = "type"
2405    )]
2406    pub chat_type: String,
2407    /// Workflow type
2408    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2409    pub workflow_type: String,
2410}
2411
2412/// Actual vs estimated value for one consensus metric.
2413#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2414pub struct USConsensusEstimate {
2415    /// Actual reported value
2416    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2417    pub actual: String,
2418    /// Consensus estimate value
2419    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2420    pub estimate: String,
2421}
2422
2423/// One fiscal-year entry in [`USAnalystConsensus`]'s list.
2424#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2425pub struct USConsensusItem {
2426    /// EBIT actual vs estimate
2427    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2428    pub ebit: USConsensusEstimate,
2429    /// EPS actual vs estimate
2430    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2431    pub eps: USConsensusEstimate,
2432    /// Fiscal year
2433    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2434    pub fiscal_year: i64,
2435    /// Report label text
2436    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2437    pub report_txt: String,
2438    /// Revenue actual vs estimate
2439    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2440    pub revenue: USConsensusEstimate,
2441}
2442
2443/// Response for [`crate::FundamentalContext::us_analyst_consensus`].
2444#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2445pub struct USAnalystConsensus {
2446    /// AI-generated summary text
2447    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2448    pub ai_summary: String,
2449    /// Embedded AI-chat context
2450    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2451    pub aichat_data: USAIChatData,
2452    /// Reporting currency
2453    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2454    pub currency: String,
2455    /// Report type
2456    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2457    pub report: String,
2458    /// Per-fiscal-year consensus estimates
2459    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2460    pub list: Vec<USConsensusItem>,
2461    /// Option consensus type identifiers (e.g. "call", "put").
2462    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2463    pub opt_reports: Vec<String>,
2464    /// Raw H5 page data (structure varies)
2465    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2466    pub h5_data: serde_json::Value,
2467}
2468
2469#[cfg(test)]
2470mod tests {
2471    use super::*;
2472
2473    #[test]
2474    fn us_company_dividends_tolerates_null_recent_dividends() {
2475        // The server sends an explicit `"recent_dividends": null` when the
2476        // company has no trailing dividends; `#[serde(default)]` alone only
2477        // covers a missing key.
2478        let parsed: USCompanyDividends = serde_json::from_str(
2479            r#"{"dividend_history":[],"dividend_payout_history":[],"payout_ratios":[],"recent_dividends":null}"#,
2480        )
2481        .unwrap();
2482        assert!(parsed.recent_dividends.dividend_ttm.is_empty());
2483    }
2484
2485    #[test]
2486    fn institution_rating_detail_tolerates_null_series() {
2487        // Symbols without coverage return `"target": null` (and potentially
2488        // `"evaluate": null`); both must map to empty series, not error.
2489        let parsed: InstitutionRatingDetail =
2490            serde_json::from_str(r#"{"ccy_symbol":"HK$","evaluate":null,"target":null}"#).unwrap();
2491        assert!(parsed.target.list.is_empty());
2492        assert!(parsed.evaluate.list.is_empty());
2493    }
2494}