Skip to main content

longbridge/fundamental/
types.rs

1#![allow(missing_docs)]
2
3use std::collections::HashMap;
4
5use num_enum::{FromPrimitive, IntoPrimitive};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use strum_macros::{Display, EnumString};
9use time::OffsetDateTime;
10
11use crate::utils::counter::deserialize_counter_id_as_symbol;
12
13// ── financial_report ─────────────────────────────────────────────
14
15/// Response for [`crate::FundamentalContext::financial_report`]
16///
17/// The `list` field contains deeply-nested indicator/account/value data keyed
18/// by report kind (`"IS"`, `"BS"`, `"CF"`).  The exact structure varies and is
19/// preserved as raw JSON.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct FinancialReports {
22    /// Raw nested financial data. Top-level keys are report kinds such as
23    /// `"IS"` (income statement), `"BS"` (balance sheet), `"CF"` (cash flow).
24    pub list: serde_json::Value,
25}
26
27// ── dividend ─────────────────────────────────────────────────────
28
29/// Response for [`crate::FundamentalContext::dividend`] and
30/// [`crate::FundamentalContext::dividend_detail`]
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DividendList {
33    /// List of dividend events
34    pub list: Vec<DividendItem>,
35}
36
37/// A single dividend / distribution event
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct DividendItem {
40    /// Security symbol, e.g. `"700.HK"`
41    #[serde(
42        rename = "counter_id",
43        deserialize_with = "deserialize_counter_id_as_symbol"
44    )]
45    pub symbol: String,
46    /// Internal record ID (may be absent in dividend_detail response)
47    #[serde(default)]
48    pub id: String,
49    /// Human-readable description, e.g. `"每股派息 5.3 HKD"`
50    pub desc: String,
51    /// Record / book-close date, e.g. `"2026.05.18"`
52    pub record_date: String,
53    /// Ex-dividend date, e.g. `"2026.05.15"`
54    pub ex_date: String,
55    /// Payment date, e.g. `"2026.06.01"`
56    pub payment_date: String,
57}
58
59// ── institution_rating ────────────────────────────────────────────
60
61/// Combined analyst-rating response for
62/// [`crate::FundamentalContext::institution_rating`]
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct InstitutionRating {
65    /// Latest snapshot from `/v1/quote/institution-rating-latest`
66    pub latest: InstitutionRatingLatest,
67    /// Consensus summary from `/v1/quote/institution-ratings`
68    pub summary: InstitutionRatingSummary,
69}
70
71/// Latest analyst-rating snapshot
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct InstitutionRatingLatest {
74    /// Rating distribution counts and date range
75    pub evaluate: RatingEvaluate,
76    /// Target price range
77    pub target: RatingTarget,
78    /// Industry classification ID
79    pub industry_id: i64,
80    /// Industry name
81    pub industry_name: String,
82    /// Rank of this security within the industry (1 = highest)
83    pub industry_rank: i32,
84    /// Total number of securities in the industry
85    pub industry_total: i32,
86    /// Mean analyst count in the industry
87    pub industry_mean: i32,
88    /// Median analyst count in the industry
89    pub industry_median: i32,
90}
91
92/// Analyst rating distribution counts
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct RatingEvaluate {
95    /// Number of "Buy" ratings
96    pub buy: i32,
97    /// Number of "Strong Buy" / "Outperform" ratings
98    pub over: i32,
99    /// Number of "Hold" / "Neutral" ratings
100    pub hold: i32,
101    /// Number of "Underperform" ratings
102    pub under: i32,
103    /// Number of "Sell" ratings
104    pub sell: i32,
105    /// Number of "No Opinion" ratings
106    pub no_opinion: i32,
107    /// Total analyst count
108    pub total: i32,
109    /// Window start (unix timestamp string; `"0"` means unset)
110    pub start_date: String,
111    /// Window end (unix timestamp string; `"0"` means unset)
112    pub end_date: String,
113}
114
115/// Analyst target price range
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct RatingTarget {
118    /// Highest price target
119    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
120    pub highest_price: Option<Decimal>,
121    /// Lowest price target
122    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
123    pub lowest_price: Option<Decimal>,
124    /// Previous close price
125    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
126    pub prev_close: Option<Decimal>,
127    /// Window start (unix timestamp string)
128    pub start_date: String,
129    /// Window end (unix timestamp string)
130    pub end_date: String,
131}
132
133/// Consensus summary from `/v1/quote/institution-ratings`
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct InstitutionRatingSummary {
136    /// Currency symbol, e.g. `"HK$"`
137    pub ccy_symbol: String,
138    /// Change vs previous period
139    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
140    pub change: Option<Decimal>,
141    /// Simplified rating distribution
142    pub evaluate: RatingSummaryEvaluate,
143    /// Overall recommendation
144    pub recommend: InstitutionRecommend,
145    /// Consensus target price
146    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
147    pub target: Option<Decimal>,
148    /// Last updated display string, e.g. `"2026 年 5 月 5 日"`
149    pub updated_at: String,
150}
151
152/// Simplified rating distribution for the consensus summary
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct RatingSummaryEvaluate {
155    /// Number of "Buy" ratings
156    pub buy: i32,
157    /// Date of the latest update
158    pub date: String,
159    /// Number of "Hold" ratings
160    pub hold: i32,
161    /// Number of "Sell" ratings
162    pub sell: i32,
163    /// Number of "Strong Buy" ratings
164    pub strong_buy: i32,
165    /// Number of "Underperform" ratings
166    pub under: i32,
167}
168
169// ── institution_rating_detail ─────────────────────────────────────
170
171/// Response for [`crate::FundamentalContext::institution_rating_detail`]
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct InstitutionRatingDetail {
174    /// Currency symbol, e.g. `"HK$"`
175    pub ccy_symbol: String,
176    /// Historical rating distribution time-series
177    pub evaluate: InstitutionRatingDetailEvaluate,
178    /// Historical target price time-series
179    pub target: InstitutionRatingDetailTarget,
180}
181
182/// Historical rating distribution time-series
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct InstitutionRatingDetailEvaluate {
185    /// Weekly snapshots ordered from oldest to newest
186    pub list: Vec<InstitutionRatingDetailEvaluateItem>,
187}
188
189/// One weekly rating distribution snapshot
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct InstitutionRatingDetailEvaluateItem {
192    /// Number of "Buy" ratings
193    pub buy: i32,
194    /// Date in `"2021/05/14"` format
195    pub date: String,
196    /// Number of "Hold" ratings
197    pub hold: i32,
198    /// Number of "Sell" ratings
199    pub sell: i32,
200    /// Number of "Strong Buy" / "Outperform" ratings
201    #[serde(default)]
202    pub strong_buy: i32,
203    /// Number of "No Opinion" ratings
204    #[serde(default)]
205    pub no_opinion: i32,
206    /// Number of "Underperform" ratings
207    pub under: i32,
208}
209
210/// Historical target price time-series
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct InstitutionRatingDetailTarget {
213    /// Prediction accuracy ratio, e.g. `"0.9934"` (may be `null`)
214    pub data_percent: Option<Decimal>,
215    /// Overall prediction accuracy percentage string
216    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
217    pub prediction_accuracy: Option<Decimal>,
218    /// Last updated display string
219    pub updated_at: String,
220    /// Weekly target price snapshots
221    pub list: Vec<InstitutionRatingDetailTargetItem>,
222}
223
224/// One weekly target price snapshot
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct InstitutionRatingDetailTargetItem {
227    /// Average target price
228    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
229    pub avg_target: Option<Decimal>,
230    /// Date in `"2021/05/16"` format
231    pub date: String,
232    /// Highest target price
233    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
234    pub max_target: Option<Decimal>,
235    /// Lowest target price
236    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
237    pub min_target: Option<Decimal>,
238    /// Whether the stock price reached the target
239    pub meet: bool,
240    /// Actual stock price at this date
241    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
242    pub price: Option<Decimal>,
243    /// Unix timestamp string
244    pub timestamp: String,
245}
246
247// ── forecast_eps ──────────────────────────────────────────────────
248
249/// Response for [`crate::FundamentalContext::forecast_eps`]
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct ForecastEps {
252    /// EPS forecast snapshots ordered by `forecast_start_date` ascending
253    pub items: Vec<ForecastEpsItem>,
254}
255
256/// One EPS forecast snapshot
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct ForecastEpsItem {
259    /// Median EPS estimate
260    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
261    pub forecast_eps_median: Option<Decimal>,
262    /// Mean EPS estimate
263    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
264    pub forecast_eps_mean: Option<Decimal>,
265    /// Lowest EPS estimate
266    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
267    pub forecast_eps_lowest: Option<Decimal>,
268    /// Highest EPS estimate
269    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
270    pub forecast_eps_highest: Option<Decimal>,
271    /// Total number of forecasting institutions
272    pub institution_total: i32,
273    /// Number of institutions that raised their estimate
274    pub institution_up: i32,
275    /// Number of institutions that lowered their estimate
276    pub institution_down: i32,
277    /// Forecast window start
278    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
279    pub forecast_start_date: OffsetDateTime,
280    /// Forecast window end
281    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
282    pub forecast_end_date: OffsetDateTime,
283}
284
285// ── consensus ─────────────────────────────────────────────────────
286
287/// Response for [`crate::FundamentalContext::consensus`]
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct FinancialConsensus {
290    /// Per-period consensus reports
291    pub list: Vec<ConsensusReport>,
292    /// Index into `list` of the most recently released period
293    pub current_index: i32,
294    /// Reporting currency, e.g. `"HKD"`
295    pub currency: String,
296    /// Available period types, e.g. `["qf", "saf", "af"]`
297    #[serde(default)]
298    pub opt_periods: Vec<String>,
299    /// Currently returned period type
300    pub current_period: String,
301}
302
303/// Consensus report for one fiscal period
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct ConsensusReport {
306    /// Fiscal year, e.g. `2025`
307    pub fiscal_year: i32,
308    /// Fiscal period code, e.g. `"Q4"`
309    pub fiscal_period: String,
310    /// Human-readable period label, e.g. `"Q4 FY2025"`
311    pub period_text: String,
312    /// Per-metric consensus details
313    pub details: Vec<ConsensusDetail>,
314}
315
316/// Consensus estimate for one financial metric
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ConsensusDetail {
319    /// Metric key, e.g. `"revenue"`, `"eps"`
320    pub key: String,
321    /// Display name
322    pub name: String,
323    /// Metric description
324    pub description: String,
325    /// Actual reported value (empty string if not yet released)
326    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
327    pub actual: Option<Decimal>,
328    /// Consensus estimate value
329    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
330    pub estimate: Option<Decimal>,
331    /// Actual minus estimate
332    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
333    pub comp_value: Option<Decimal>,
334    /// Beat/miss description, e.g. `"超出预期"`
335    pub comp_desc: String,
336    /// Comparison result code for colour coding
337    pub comp: String,
338    /// Whether the actual results have been published
339    pub is_released: bool,
340}
341
342// ── valuation ─────────────────────────────────────────────────────
343
344/// Response for [`crate::FundamentalContext::valuation`]
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct ValuationData {
347    /// Valuation metrics (PE / PB / PS / dividend yield)
348    pub metrics: ValuationMetricsData,
349}
350
351/// Container for all valuation metrics
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ValuationMetricsData {
354    /// Price-to-Earnings ratio history
355    pub pe: Option<ValuationMetricData>,
356    /// Price-to-Book ratio history
357    pub pb: Option<ValuationMetricData>,
358    /// Price-to-Sales ratio history
359    pub ps: Option<ValuationMetricData>,
360    /// Dividend yield history
361    pub dvd_yld: Option<ValuationMetricData>,
362}
363
364/// Historical time-series for one valuation metric
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct ValuationMetricData {
367    /// Human-readable description with current value and percentile
368    pub desc: String,
369    /// Historical high value
370    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
371    pub high: Option<Decimal>,
372    /// Historical low value
373    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
374    pub low: Option<Decimal>,
375    /// Historical median value
376    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
377    pub median: Option<Decimal>,
378    /// Historical data points
379    pub list: Vec<ValuationPoint>,
380}
381
382/// One valuation data point
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct ValuationPoint {
385    /// Date of the data point
386    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
387    pub timestamp: OffsetDateTime,
388    /// Metric value
389    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
390    pub value: Option<Decimal>,
391}
392
393// ── valuation_history ─────────────────────────────────────────────
394
395/// Response for [`crate::FundamentalContext::valuation_history`]
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct ValuationHistoryResponse {
398    /// Historical valuation data
399    pub history: ValuationHistoryData,
400}
401
402/// Container for historical valuation metrics
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct ValuationHistoryData {
405    /// Historical metrics (PE / PB / PS)
406    pub metrics: ValuationHistoryMetrics,
407}
408
409/// Historical valuation metrics container
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct ValuationHistoryMetrics {
412    /// Price-to-Earnings history
413    pub pe: Option<ValuationHistoryMetric>,
414    /// Price-to-Book history
415    pub pb: Option<ValuationHistoryMetric>,
416    /// Price-to-Sales history
417    pub ps: Option<ValuationHistoryMetric>,
418}
419
420/// Historical data for one valuation metric including statistical bounds
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct ValuationHistoryMetric {
423    /// Human-readable description
424    pub desc: String,
425    /// Historical high over the period
426    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
427    pub high: Option<Decimal>,
428    /// Historical low over the period
429    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
430    pub low: Option<Decimal>,
431    /// Historical median over the period
432    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
433    pub median: Option<Decimal>,
434    /// Historical data points
435    pub list: Vec<ValuationPoint>,
436}
437
438// ── industry_valuation ────────────────────────────────────────────
439
440/// Response for [`crate::FundamentalContext::industry_valuation`]
441#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct IndustryValuationList {
443    /// List of peer securities with their valuation data
444    pub list: Vec<IndustryValuationItem>,
445}
446
447/// Valuation data for one peer security
448#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct IndustryValuationItem {
450    /// Security symbol, e.g. `"700.HK"`
451    #[serde(
452        rename = "counter_id",
453        deserialize_with = "deserialize_counter_id_as_symbol"
454    )]
455    pub symbol: String,
456    /// Company name
457    pub name: String,
458    /// Reporting currency
459    pub currency: String,
460    /// Total assets
461    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
462    pub assets: Option<Decimal>,
463    /// Book value per share
464    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
465    pub bps: Option<Decimal>,
466    /// Earnings per share
467    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
468    pub eps: Option<Decimal>,
469    /// Dividends per share
470    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
471    pub dps: Option<Decimal>,
472    /// Dividend yield
473    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
474    pub div_yld: Option<Decimal>,
475    /// Dividend payout ratio
476    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
477    pub div_payout_ratio: Option<Decimal>,
478    /// 5-year average dividends per share
479    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
480    pub five_y_avg_dps: Option<Decimal>,
481    /// Current PE ratio
482    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
483    pub pe: Option<Decimal>,
484    /// Historical PE/PB/PS snapshots
485    pub history: Vec<IndustryValuationHistory>,
486}
487
488/// Historical valuation snapshot for an industry peer
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct IndustryValuationHistory {
491    /// Unix timestamp string
492    pub date: String,
493    /// Price-to-Earnings ratio
494    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
495    pub pe: Option<Decimal>,
496    /// Price-to-Book ratio
497    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
498    pub pb: Option<Decimal>,
499    /// Price-to-Sales ratio
500    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
501    pub ps: Option<Decimal>,
502}
503
504// ── industry_valuation_dist ───────────────────────────────────────
505
506/// Response for [`crate::FundamentalContext::industry_valuation_dist`]
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct IndustryValuationDist {
509    /// PE ratio distribution within the industry
510    pub pe: Option<ValuationDist>,
511    /// PB ratio distribution within the industry
512    pub pb: Option<ValuationDist>,
513    /// PS ratio distribution within the industry
514    pub ps: Option<ValuationDist>,
515}
516
517/// Distribution statistics for one valuation metric within an industry
518#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct ValuationDist {
520    /// Minimum value in the industry
521    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
522    pub low: Option<Decimal>,
523    /// Maximum value in the industry
524    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
525    pub high: Option<Decimal>,
526    /// Median value in the industry
527    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
528    pub median: Option<Decimal>,
529    /// Current value of the queried security
530    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
531    pub value: Option<Decimal>,
532    /// Percentile ranking (0–1 range as string)
533    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
534    pub ranking: Option<Decimal>,
535    /// Ordinal rank index (1-based)
536    pub rank_index: String,
537    /// Total number of securities in the industry
538    pub rank_total: String,
539}
540
541// ── company ───────────────────────────────────────────────────────
542
543/// Response for [`crate::FundamentalContext::company`]
544#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct CompanyOverview {
546    /// Short name, e.g. `"腾讯控股"`
547    pub name: String,
548    /// Full legal name
549    pub company_name: String,
550    /// Founding date
551    pub founded: String,
552    /// Listing date
553    pub listing_date: String,
554    /// Primary listing market display name
555    pub market: String,
556    /// Market region code, e.g. `"HK"`
557    pub region: String,
558    /// Registered address
559    pub address: String,
560    /// Principal office address
561    pub office_address: String,
562    /// Company website
563    pub website: String,
564    /// IPO issue price
565    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
566    pub issue_price: Option<Decimal>,
567    /// Number of shares offered at IPO
568    pub shares_offered: String,
569    /// Chairman name
570    pub chairman: String,
571    /// Company secretary name
572    pub secretary: String,
573    /// Auditing institution
574    pub audit_inst: String,
575    /// Company classification category
576    pub category: String,
577    /// Fiscal year end, e.g. `"12 月 31 日"`
578    pub year_end: String,
579    /// Number of employees
580    pub employees: String,
581    /// Phone number (API field name is `"Phone"`)
582    #[serde(rename = "Phone")]
583    pub phone: String,
584    /// Fax number
585    pub fax: String,
586    /// Investor relations email
587    pub email: String,
588    /// Legal representative
589    pub legal_repr: String,
590    /// CEO / Managing Director
591    pub manager: String,
592    /// Business licence number
593    pub bus_license: String,
594    /// Accounting firm
595    pub accounting_firm: String,
596    /// Securities representative
597    pub securities_rep: String,
598    /// Legal counsel
599    pub legal_counsel: String,
600    /// Postal code
601    pub zip_code: String,
602    /// Exchange ticker code, e.g. `"00700"`
603    pub ticker: String,
604    /// URL to the company's logo icon
605    pub icon: String,
606    /// Business profile / description
607    pub profile: String,
608    /// ADS ratio (may be empty)
609    #[serde(default)]
610    pub ads_ratio: String,
611    /// Industry sector code
612    pub sector: i32,
613}
614
615// ── executive ─────────────────────────────────────────────────────
616
617/// Response for [`crate::FundamentalContext::executive`]
618#[derive(Debug, Clone, Serialize, Deserialize)]
619pub struct ExecutiveList {
620    /// Groups of executives per security (usually one group)
621    pub professional_list: Vec<ExecutiveGroup>,
622}
623
624/// Executives for one security
625#[derive(Debug, Clone, Serialize, Deserialize)]
626pub struct ExecutiveGroup {
627    /// Security symbol
628    #[serde(
629        rename = "counter_id",
630        deserialize_with = "deserialize_counter_id_as_symbol"
631    )]
632    pub symbol: String,
633    /// Link to the company wiki page
634    pub forward_url: String,
635    /// Total number of executives
636    pub total: i32,
637    /// Individual executive entries
638    pub professionals: Vec<Professional>,
639}
640
641/// One executive / board member
642#[derive(Debug, Clone, Serialize, Deserialize)]
643pub struct Professional {
644    /// Internal wiki person ID (string form)
645    pub id: String,
646    /// Full name
647    pub name: String,
648    /// Full name in Simplified Chinese
649    pub name_zhcn: String,
650    /// Full name in English
651    pub name_en: String,
652    /// Job title, e.g. `"Co-Founder, Chairman & CEO"`
653    pub title: String,
654    /// Biography text
655    pub biography: String,
656    /// URL to the person's photo
657    pub photo: String,
658    /// URL to the wiki profile page
659    pub wiki_url: String,
660}
661
662// ── shareholder ───────────────────────────────────────────────────
663
664/// Response for [`crate::FundamentalContext::shareholder`]
665#[derive(Debug, Clone, Serialize, Deserialize)]
666pub struct ShareholderList {
667    /// List of major shareholders
668    pub shareholder_list: Vec<Shareholder>,
669    /// Link to the full shareholder page
670    #[serde(default)]
671    pub forward_url: String,
672    /// Total number of shareholders returned
673    pub total: i32,
674}
675
676/// One major shareholder
677#[derive(Debug, Clone, Serialize, Deserialize)]
678pub struct Shareholder {
679    /// Internal shareholder ID (string form)
680    pub shareholder_id: String,
681    /// Shareholder name
682    pub shareholder_name: String,
683    /// Institution type (may be empty)
684    pub institution_type: String,
685    /// Percentage of shares held
686    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
687    pub percent_of_shares: Option<Decimal>,
688    /// Change in shares held (positive = bought, negative = sold)
689    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
690    pub shares_changed: Option<Decimal>,
691    /// Date of the most recent filing, e.g. `"2026-05-04"`
692    pub report_date: String,
693    /// Other securities held by this shareholder (cross-holdings)
694    #[serde(default)]
695    pub stocks: Vec<ShareholderStock>,
696}
697
698/// A security in an institutional shareholder's cross-holdings
699#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct ShareholderStock {
701    /// Security symbol of the cross-held stock
702    #[serde(
703        rename = "counter_id",
704        deserialize_with = "deserialize_counter_id_as_symbol"
705    )]
706    pub symbol: String,
707    /// Ticker code, e.g. `"BLK"`
708    pub code: String,
709    /// Market, e.g. `"US"`
710    pub market: String,
711    /// Day change percentage, e.g. `"-0.32%"`
712    pub chg: String,
713}
714
715// ── fund_holder ───────────────────────────────────────────────────
716
717/// Response for [`crate::FundamentalContext::fund_holder`]
718#[derive(Debug, Clone, Serialize, Deserialize)]
719pub struct FundHolders {
720    /// Funds and ETFs that hold the queried security
721    pub lists: Vec<FundHolder>,
722}
723
724/// A fund or ETF that holds the queried security
725#[derive(Debug, Clone, Serialize, Deserialize)]
726pub struct FundHolder {
727    /// Fund/ETF ticker code, e.g. `"513050"`
728    pub code: String,
729    /// Fund/ETF symbol, e.g. `"ETF/SH/513050"` → converted to `"513050.SH"`
730    #[serde(
731        rename = "counter_id",
732        deserialize_with = "deserialize_counter_id_as_symbol"
733    )]
734    pub symbol: String,
735    /// Reporting currency, e.g. `"CNY"`
736    pub currency: String,
737    /// Fund/ETF full name
738    pub name: String,
739    /// Position ratio as a percentage decimal
740    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
741    pub position_ratio: Decimal,
742    /// Report date, e.g. `"2025.12.31"`
743    pub report_date: String,
744}
745
746// ── corp_action ───────────────────────────────────────────────────
747
748/// Response for [`crate::FundamentalContext::corp_action`]
749#[derive(Debug, Clone, Serialize, Deserialize)]
750pub struct CorpActions {
751    /// Corporate action events
752    pub items: Vec<CorpActionItem>,
753}
754
755/// One corporate action event
756#[derive(Debug, Clone, Serialize, Deserialize)]
757pub struct CorpActionItem {
758    /// Internal event ID
759    pub id: String,
760    /// Date in `YYYYMMDD` format, e.g. `"20260601"`
761    pub date: String,
762    /// Short display date, e.g. `"06.01"`
763    pub date_str: String,
764    /// Date type label, e.g. `"派息日"`, `"除权日"`
765    pub date_type: String,
766    /// Time zone description, e.g. `"北京时间"`
767    pub date_zone: String,
768    /// Event category, e.g. `"分配方案"`
769    pub act_type: String,
770    /// Human-readable event description
771    pub act_desc: String,
772    /// Machine-readable action code, e.g. `"DividendExDate"`
773    pub action: String,
774    /// Whether this is a recent event
775    pub recent: bool,
776    /// Whether publication was delayed
777    pub is_delay: bool,
778    /// Delay announcement content (if `is_delay` is `true`)
779    pub delay_content: String,
780    /// Associated live stream (if any)
781    pub live: Option<CorpActionLive>,
782    /// Associated security info (rarely populated; preserved as raw JSON)
783    pub security: Option<serde_json::Value>,
784}
785
786/// Live stream associated with a corporate action
787#[derive(Debug, Clone, Serialize, Deserialize)]
788pub struct CorpActionLive {
789    /// Live stream ID
790    pub id: String,
791    /// Status code: 1=preview, 2=live, 3=ended, 4=replay, 5=processing
792    pub status: serde_json::Value, // API may return int or string
793    /// Start time
794    pub started_at: String,
795    /// Stream title
796    pub name: String,
797    /// Icon URL
798    pub icon: String,
799}
800
801// ── invest_relation ───────────────────────────────────────────────
802
803/// Response for [`crate::FundamentalContext::invest_relation`]
804#[derive(Debug, Clone, Serialize, Deserialize)]
805pub struct InvestRelations {
806    /// Link to the full investor-relations page
807    #[serde(default)]
808    pub forward_url: String,
809    /// Securities in which the queried company holds a stake
810    pub invest_securities: Vec<InvestSecurity>,
811}
812
813/// A security in which the queried company has an investment stake
814#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct InvestSecurity {
816    /// Internal company ID (string form; may be `"0"`)
817    pub company_id: String,
818    /// Company name (locale-aware)
819    pub company_name: String,
820    /// Company name in English
821    pub company_name_en: String,
822    /// Company name in Simplified Chinese
823    pub company_name_zhcn: String,
824    /// Security symbol of the invested company
825    #[serde(
826        rename = "counter_id",
827        deserialize_with = "deserialize_counter_id_as_symbol"
828    )]
829    pub symbol: String,
830    /// Reporting currency
831    pub currency: String,
832    /// Percentage of shares held
833    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
834    pub percent_of_shares: Option<Decimal>,
835    /// Shareholder rank, e.g. `"1"` = largest shareholder
836    pub shares_rank: String,
837    /// Market value of the holding
838    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
839    pub shares_value: Option<Decimal>,
840}
841
842// ── operating ─────────────────────────────────────────────────────
843
844/// Response for [`crate::FundamentalContext::operating`]
845#[derive(Debug, Clone, Serialize, Deserialize)]
846pub struct OperatingList {
847    /// List of operating summary reports
848    pub list: Vec<OperatingItem>,
849}
850
851/// One operating summary report (annual / quarterly)
852#[derive(Debug, Clone, Serialize, Deserialize)]
853pub struct OperatingItem {
854    /// Internal report ID
855    pub id: String,
856    /// Report period code, e.g. `"af"` (annual), `"qf"` (quarterly)
857    pub report: String,
858    /// Report title, e.g. `"2025 财年年报"`
859    pub title: String,
860    /// Management discussion text
861    pub txt: String,
862    /// Whether this is the most recent report
863    pub latest: bool,
864    /// Keyword tags (structure undocumented; usually empty)
865    #[serde(default)]
866    pub keywords: Vec<serde_json::Value>,
867    /// URL to the full community report page
868    #[serde(default)]
869    pub web_url: String,
870    /// Key financial metrics extracted from the report
871    pub financial: OperatingFinancial,
872}
873
874/// Key financial metrics extracted from an operating report
875#[derive(Debug, Clone, Serialize, Deserialize)]
876pub struct OperatingFinancial {
877    /// Ticker code (may be empty)
878    pub code: String,
879    /// Symbol in `CODE.MARKET` format (may be empty)
880    #[serde(
881        rename = "counter_id",
882        deserialize_with = "deserialize_counter_id_as_symbol"
883    )]
884    pub symbol: String,
885    /// Reporting currency
886    pub currency: String,
887    /// Company name
888    pub name: String,
889    /// Market region
890    pub region: String,
891    /// Report period code
892    pub report: String,
893    /// Report period display text
894    pub report_txt: String,
895    /// Financial indicators
896    pub indicators: Vec<OperatingIndicator>,
897}
898
899/// One financial indicator in an operating report
900#[derive(Debug, Clone, Serialize, Deserialize)]
901pub struct OperatingIndicator {
902    /// Field name key, e.g. `"operating_revenue"`
903    pub field_name: String,
904    /// Display name, e.g. `"营业收入"`
905    pub indicator_name: String,
906    /// Formatted value, e.g. `"8217 亿"`
907    pub indicator_value: String,
908    /// Year-over-year change
909    #[serde(default, with = "crate::serde_utils::decimal_opt_str_is_none")]
910    pub yoy: Option<Decimal>,
911}
912
913// ── buyback ───────────────────────────────────────────────────────
914
915/// Response for [`crate::FundamentalContext::buyback`]
916#[derive(Debug, Clone, Serialize, Deserialize)]
917pub struct BuybackData {
918    /// Most recent buyback summary (TTM)
919    #[serde(default)]
920    pub recent_buybacks: Option<RecentBuybacks>,
921    /// Historical annual buyback data
922    #[serde(default)]
923    pub buyback_history: Vec<BuybackHistoryItem>,
924    /// Buyback payout and cash-flow ratios
925    #[serde(default)]
926    pub buyback_ratios: Vec<BuybackRatios>,
927}
928
929/// TTM (trailing twelve months) buyback summary
930#[derive(Debug, Clone, Serialize, Deserialize)]
931pub struct RecentBuybacks {
932    /// Reporting currency
933    pub currency: String,
934    /// Net buyback amount TTM
935    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
936    pub net_buyback_ttm: Option<Decimal>,
937    /// Net buyback yield TTM
938    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
939    pub net_buyback_yield_ttm: Option<Decimal>,
940}
941
942/// Historical annual buyback data point
943#[derive(Debug, Clone, Serialize, Deserialize)]
944pub struct BuybackHistoryItem {
945    /// Fiscal year label, e.g. `"FY2024"`
946    pub fiscal_year: String,
947    /// Fiscal year date range string
948    pub fiscal_year_range: String,
949    /// Net buyback amount
950    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
951    pub net_buyback: Option<Decimal>,
952    /// Net buyback yield
953    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
954    pub net_buyback_yield: Option<Decimal>,
955    /// Year-over-year net buyback growth rate
956    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
957    pub net_buyback_growth_rate: Option<Decimal>,
958    /// Reporting currency
959    pub currency: String,
960}
961
962/// Buyback payout and cash-flow ratios
963#[derive(Debug, Clone, Serialize, Deserialize)]
964pub struct BuybackRatios {
965    /// Net buyback payout ratio
966    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
967    pub net_buyback_payout_ratio: Option<Decimal>,
968    /// Net buyback to free cash-flow ratio
969    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
970    pub net_buyback_to_cashflow_ratio: Option<Decimal>,
971}
972
973// ── ratings ───────────────────────────────────────────────────────
974
975/// Response for [`crate::FundamentalContext::ratings`]
976#[derive(Debug, Clone, Serialize, Deserialize)]
977pub struct StockRatings {
978    /// Style display name
979    #[serde(default)]
980    pub style_txt_name: String,
981    /// Scale display name
982    #[serde(default)]
983    pub scale_txt_name: String,
984    /// Report period display text
985    #[serde(default)]
986    pub report_period_txt: String,
987    /// Composite score (may be int, float, or null)
988    #[serde(default)]
989    pub multi_score: serde_json::Value,
990    /// Composite score letter grade
991    #[serde(default)]
992    pub multi_letter: String,
993    /// Score change vs previous period
994    #[serde(default)]
995    pub multi_score_change: i32,
996    /// Industry name
997    #[serde(default)]
998    pub industry_name: String,
999    /// Industry rank (may be int or null)
1000    #[serde(default)]
1001    pub industry_rank: serde_json::Value,
1002    /// Total securities in the industry
1003    #[serde(default)]
1004    pub industry_total: serde_json::Value,
1005    /// Industry mean score
1006    #[serde(default)]
1007    pub industry_mean_score: serde_json::Value,
1008    /// Industry median score
1009    #[serde(default)]
1010    pub industry_median_score: serde_json::Value,
1011    /// Detailed rating categories
1012    #[serde(default)]
1013    pub ratings: Vec<RatingCategory>,
1014}
1015
1016/// One rating category (e.g. growth, profitability)
1017#[derive(Debug, Clone, Serialize, Deserialize)]
1018pub struct RatingCategory {
1019    /// Category type code
1020    #[serde(rename = "type")]
1021    pub kind: i32,
1022    /// Sub-indicator groups within this category
1023    #[serde(default)]
1024    pub sub_indicators: Vec<RatingSubIndicatorGroup>,
1025}
1026
1027/// A group of sub-indicators under one category indicator
1028#[derive(Debug, Clone, Serialize, Deserialize)]
1029pub struct RatingSubIndicatorGroup {
1030    /// Parent indicator for this group
1031    pub indicator: RatingIndicator,
1032    /// Leaf sub-indicators
1033    #[serde(default)]
1034    pub sub_indicators: Vec<RatingLeafIndicator>,
1035}
1036
1037/// A rating indicator node (may be a parent or a leaf)
1038#[derive(Debug, Clone, Serialize, Deserialize)]
1039pub struct RatingIndicator {
1040    /// Indicator display name
1041    pub name: String,
1042    /// Score (may be int, float, or null)
1043    #[serde(default)]
1044    pub score: serde_json::Value,
1045    /// Letter grade
1046    #[serde(default)]
1047    pub letter: String,
1048}
1049
1050/// A leaf rating indicator with a raw value
1051#[derive(Debug, Clone, Serialize, Deserialize)]
1052pub struct RatingLeafIndicator {
1053    /// Indicator display name
1054    pub name: String,
1055    /// Formatted value string
1056    #[serde(default)]
1057    pub value: String,
1058    /// Value type hint, e.g. `"percent"`
1059    #[serde(default)]
1060    pub value_type: String,
1061    /// Score (may be int, float, or null)
1062    #[serde(default)]
1063    pub score: serde_json::Value,
1064    /// Letter grade
1065    #[serde(default)]
1066    pub letter: String,
1067}
1068
1069// ── enums ─────────────────────────────────────────────────────────
1070
1071/// Institutional analyst recommendation
1072#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
1073pub enum InstitutionRecommend {
1074    /// Unknown
1075    Unknown,
1076    /// Strong buy
1077    #[strum(serialize = "strong_buy")]
1078    StrongBuy,
1079    /// Buy
1080    #[strum(serialize = "buy")]
1081    Buy,
1082    /// Hold
1083    #[strum(serialize = "hold")]
1084    Hold,
1085    /// Sell
1086    #[strum(serialize = "sell")]
1087    Sell,
1088    /// Strong sell
1089    #[strum(serialize = "strong_sell")]
1090    StrongSell,
1091    /// Underperform
1092    #[strum(serialize = "underperform")]
1093    Underperform,
1094    /// No opinion
1095    #[strum(serialize = "no_opinion")]
1096    NoOpinion,
1097}
1098
1099impl_default_for_enum_string!(InstitutionRecommend);
1100impl_serde_for_enum_string!(InstitutionRecommend);
1101
1102/// Financial report kind
1103#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
1104pub enum FinancialReportKind {
1105    /// Income statement
1106    #[serde(rename = "IS")]
1107    IncomeStatement,
1108    /// Balance sheet
1109    #[serde(rename = "BS")]
1110    BalanceSheet,
1111    /// Cash flow statement
1112    #[serde(rename = "CF")]
1113    CashFlow,
1114    /// All statements
1115    #[default]
1116    #[serde(rename = "ALL")]
1117    All,
1118}
1119
1120// ── business_segments ─────────────────────────────────────────────
1121
1122/// Response for [`crate::FundamentalContext::business_segments`]
1123#[derive(Debug, Clone, Serialize, Deserialize)]
1124pub struct BusinessSegments {
1125    /// Report date
1126    pub date: String,
1127    /// Total revenue
1128    pub total: String,
1129    /// Reporting currency
1130    pub currency: String,
1131    /// Business segment breakdown
1132    #[serde(default)]
1133    pub business: Vec<BusinessSegmentItem>,
1134}
1135
1136/// One business segment item (latest snapshot)
1137#[derive(Debug, Clone, Serialize, Deserialize)]
1138pub struct BusinessSegmentItem {
1139    /// Segment name
1140    pub name: String,
1141    /// Percentage of total revenue
1142    pub percent: String,
1143}
1144
1145/// Response for [`crate::FundamentalContext::business_segments_history`]
1146#[derive(Debug, Clone, Serialize, Deserialize)]
1147pub struct BusinessSegmentsHistory {
1148    /// Historical snapshots
1149    #[serde(default)]
1150    pub historical: Vec<BusinessSegmentsHistoricalItem>,
1151}
1152
1153/// One historical business segments snapshot
1154#[derive(Debug, Clone, Serialize, Deserialize)]
1155pub struct BusinessSegmentsHistoricalItem {
1156    /// Report date
1157    pub date: String,
1158    /// Total revenue
1159    pub total: String,
1160    /// Reporting currency
1161    pub currency: String,
1162    /// Business segment breakdown
1163    #[serde(default)]
1164    pub business: Vec<BusinessSegmentHistoryItem>,
1165    /// Regional breakdown
1166    #[serde(default)]
1167    pub regionals: Vec<BusinessSegmentHistoryItem>,
1168}
1169
1170/// One business/regional segment item in a historical snapshot
1171#[derive(Debug, Clone, Serialize, Deserialize)]
1172pub struct BusinessSegmentHistoryItem {
1173    /// Segment name
1174    pub name: String,
1175    /// Percentage of total
1176    pub percent: String,
1177    /// Absolute value
1178    pub value: String,
1179}
1180
1181// ── institution_rating_views ──────────────────────────────────────
1182
1183/// Response for [`crate::FundamentalContext::institution_rating_views`]
1184#[derive(Debug, Clone, Serialize, Deserialize)]
1185pub struct InstitutionRatingViews {
1186    /// Historical rating distribution snapshots
1187    #[serde(default)]
1188    pub elist: Vec<InstitutionRatingViewItem>,
1189}
1190
1191/// One historical rating distribution snapshot
1192#[derive(Debug, Clone, Serialize, Deserialize)]
1193pub struct InstitutionRatingViewItem {
1194    /// Date as unix timestamp string (API returns as quoted or bare integer)
1195    pub date: String,
1196    /// Number of "Buy" ratings (API returns as string)
1197    pub buy: String,
1198    /// Number of "Outperform" ratings (API returns as string)
1199    pub over: String,
1200    /// Number of "Hold" ratings (API returns as string)
1201    pub hold: String,
1202    /// Number of "Underperform" ratings (API returns as string)
1203    pub under: String,
1204    /// Number of "Sell" ratings (API returns as string)
1205    pub sell: String,
1206    /// Total analyst count (API returns as string)
1207    pub total: String,
1208}
1209
1210// ── industry_rank ─────────────────────────────────────────────────
1211
1212/// Response for [`crate::FundamentalContext::industry_rank`]
1213#[derive(Debug, Clone, Serialize, Deserialize)]
1214pub struct IndustryRankResponse {
1215    /// Grouped rank items
1216    #[serde(default)]
1217    pub items: Vec<IndustryRankGroup>,
1218}
1219
1220/// A group of ranked industry items
1221#[derive(Debug, Clone, Serialize, Deserialize)]
1222pub struct IndustryRankGroup {
1223    /// Items in this group
1224    #[serde(default)]
1225    pub lists: Vec<IndustryRankItem>,
1226}
1227
1228/// One ranked industry item
1229#[derive(Debug, Clone, Serialize, Deserialize)]
1230pub struct IndustryRankItem {
1231    /// Industry / sector name
1232    pub name: String,
1233    /// Counter ID of the industry
1234    pub counter_id: String,
1235    /// Change percentage
1236    pub chg: String,
1237    /// Name of the leading stock
1238    pub leading_name: String,
1239    /// Ticker of the leading stock
1240    pub leading_ticker: String,
1241    /// Change percentage of the leading stock
1242    pub leading_chg: String,
1243    /// Value label name
1244    pub value_name: String,
1245    /// Value data
1246    pub value_data: String,
1247}
1248
1249// ── industry_peers ────────────────────────────────────────────────
1250
1251/// Response for [`crate::FundamentalContext::industry_peers`]
1252#[derive(Debug, Clone, Serialize, Deserialize)]
1253pub struct IndustryPeersResponse {
1254    /// Top-level industry node info
1255    pub top: IndustryPeersTop,
1256    /// Root peer chain node (may be absent if no data)
1257    pub chain: Option<IndustryPeerNode>,
1258}
1259
1260/// Top-level industry info in the peers response
1261#[derive(Debug, Clone, Serialize, Deserialize)]
1262pub struct IndustryPeersTop {
1263    /// Industry name
1264    pub name: String,
1265    /// Market code
1266    pub market: String,
1267}
1268
1269/// A node in the recursive industry peer chain
1270#[derive(Debug, Clone, Serialize, Deserialize)]
1271pub struct IndustryPeerNode {
1272    /// Node name
1273    pub name: String,
1274    /// Counter ID
1275    pub counter_id: String,
1276    /// Number of stocks in this node (API returns as integer)
1277    pub stock_num: i32,
1278    /// Change percentage
1279    pub chg: String,
1280    /// Year-to-date change
1281    pub ytd_chg: String,
1282    /// Child nodes (recursive)
1283    #[serde(default)]
1284    pub next: Vec<IndustryPeerNode>,
1285}
1286
1287// ── financial_report_snapshot ─────────────────────────────────────
1288
1289/// Response for [`crate::FundamentalContext::financial_report_snapshot`]
1290#[derive(Debug, Clone, Serialize, Deserialize)]
1291pub struct FinancialReportSnapshot {
1292    /// Company name
1293    pub name: String,
1294    /// Ticker code
1295    pub ticker: String,
1296    /// Fiscal period start date
1297    pub fp_start: String,
1298    /// Fiscal period end date
1299    pub fp_end: String,
1300    /// Reporting currency
1301    pub currency: String,
1302    /// Report description
1303    pub report_desc: String,
1304    /// Forecast revenue
1305    pub fo_revenue: Option<SnapshotForecastMetric>,
1306    /// Forecast EBIT
1307    pub fo_ebit: Option<SnapshotForecastMetric>,
1308    /// Forecast EPS
1309    pub fo_eps: Option<SnapshotForecastMetric>,
1310    /// Reported revenue
1311    pub fr_revenue: Option<SnapshotReportedMetric>,
1312    /// Reported net profit
1313    pub fr_profit: Option<SnapshotReportedMetric>,
1314    /// Reported operating cash flow
1315    pub fr_operate_cash: Option<SnapshotReportedMetric>,
1316    /// Reported investing cash flow
1317    pub fr_invest_cash: Option<SnapshotReportedMetric>,
1318    /// Reported financing cash flow
1319    pub fr_finance_cash: Option<SnapshotReportedMetric>,
1320    /// Reported total assets
1321    pub fr_total_assets: Option<SnapshotReportedMetric>,
1322    /// Reported total liabilities
1323    pub fr_total_liability: Option<SnapshotReportedMetric>,
1324    /// ROE TTM
1325    pub fr_roe_ttm: String,
1326    /// Profit margin
1327    pub fr_profit_margin: String,
1328    /// Profit margin TTM
1329    pub fr_profit_margin_ttm: String,
1330    /// Asset turnover TTM
1331    pub fr_asset_turn_ttm: String,
1332    /// Leverage TTM
1333    pub fr_leverage_ttm: String,
1334    /// Debt-to-assets ratio
1335    pub fr_debt_assets_ratio: String,
1336}
1337
1338/// A forecast metric in the financial report snapshot
1339#[derive(Debug, Clone, Serialize, Deserialize)]
1340pub struct SnapshotForecastMetric {
1341    /// Actual value
1342    pub value: String,
1343    /// Year-over-year change
1344    pub yoy: String,
1345    /// Beat/miss description
1346    pub cmp_desc: String,
1347    /// Consensus estimate value
1348    pub est_value: String,
1349}
1350
1351/// A reported metric in the financial report snapshot
1352#[derive(Debug, Clone, Serialize, Deserialize)]
1353pub struct SnapshotReportedMetric {
1354    /// Actual value
1355    pub value: String,
1356    /// Year-over-year change
1357    pub yoy: String,
1358}
1359
1360// ── shareholder_top ───────────────────────────────────────────────
1361
1362/// Response for [`crate::FundamentalContext::shareholder_top`]
1363#[derive(Debug, Clone, Serialize, Deserialize)]
1364pub struct ShareholderTopResponse {
1365    /// Raw top-shareholder data
1366    pub data: serde_json::Value,
1367}
1368
1369// ── shareholder_detail ────────────────────────────────────────────
1370
1371/// Response for [`crate::FundamentalContext::shareholder_detail`]
1372#[derive(Debug, Clone, Serialize, Deserialize)]
1373pub struct ShareholderDetailResponse {
1374    /// Raw shareholder detail data
1375    pub data: serde_json::Value,
1376}
1377
1378// ── valuation_comparison ──────────────────────────────────────────
1379
1380/// One historical valuation data point.
1381#[derive(Debug, Clone, Serialize, Deserialize)]
1382pub struct ValuationHistoryPoint {
1383    /// Date (RFC 3339, converted from Unix timestamp)
1384    pub date: String,
1385    /// P/E ratio
1386    pub pe: String,
1387    /// P/B ratio
1388    pub pb: String,
1389    /// P/S ratio
1390    pub ps: String,
1391}
1392
1393/// One security's valuation comparison item.
1394#[derive(Debug, Clone, Serialize, Deserialize)]
1395pub struct ValuationComparisonItem {
1396    /// Symbol (converted from counter_id)
1397    pub symbol: String,
1398    /// Security name
1399    pub name: String,
1400    /// Currency
1401    pub currency: String,
1402    /// Market capitalisation
1403    pub market_value: String,
1404    /// Latest closing price
1405    pub price_close: String,
1406    /// P/E ratio
1407    pub pe: String,
1408    /// P/B ratio
1409    pub pb: String,
1410    /// P/S ratio
1411    pub ps: String,
1412    /// Return on equity
1413    pub roe: String,
1414    /// Earnings per share
1415    pub eps: String,
1416    /// Book value per share
1417    pub bps: String,
1418    /// Dividends per share
1419    pub dps: String,
1420    /// Dividend yield
1421    pub div_yld: String,
1422    /// Total assets
1423    pub assets: String,
1424    /// Historical valuation points
1425    pub history: Vec<ValuationHistoryPoint>,
1426}
1427
1428/// Response for [`crate::FundamentalContext::valuation_comparison`]
1429#[derive(Debug, Clone, Serialize, Deserialize)]
1430pub struct ValuationComparisonResponse {
1431    /// Valuation comparison items
1432    pub list: Vec<ValuationComparisonItem>,
1433}
1434
1435/// Financial report period type
1436#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
1437pub enum FinancialReportPeriod {
1438    /// Annual report
1439    #[serde(rename = "af")]
1440    Annual,
1441    /// Semi-annual report
1442    #[serde(rename = "saf")]
1443    SemiAnnual,
1444    /// Q1 report
1445    #[serde(rename = "q1")]
1446    Q1,
1447    /// Q2 report
1448    #[serde(rename = "q2")]
1449    Q2,
1450    /// Q3 report
1451    #[serde(rename = "q3")]
1452    Q3,
1453    /// Full quarterly report
1454    #[serde(rename = "qf")]
1455    QuarterlyFull,
1456    /// Three-quarter report (first three quarters)
1457    #[serde(rename = "3q")]
1458    ThreeQ,
1459}
1460
1461// ── etf_asset_allocation ──────────────────────────────────────────
1462
1463/// ETF asset allocation element type
1464#[derive(Debug, FromPrimitive, IntoPrimitive, Copy, Clone, Hash, Eq, PartialEq)]
1465#[repr(i32)]
1466pub enum ElementType {
1467    /// Unknown
1468    #[num_enum(default)]
1469    Unknown = 0,
1470    /// Holdings
1471    Holdings = 1,
1472    /// Regional
1473    Regional = 2,
1474    /// Asset class
1475    AssetClass = 3,
1476    /// Industry
1477    Industry = 4,
1478}
1479
1480impl Serialize for ElementType {
1481    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1482    where
1483        S: serde::Serializer,
1484    {
1485        serializer.serialize_i32((*self).into())
1486    }
1487}
1488
1489impl<'de> Deserialize<'de> for ElementType {
1490    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1491    where
1492        D: serde::Deserializer<'de>,
1493    {
1494        Ok(ElementType::from(i32::deserialize(deserializer)?))
1495    }
1496}
1497
1498/// Holding detail of an ETF asset allocation element (holdings only)
1499#[derive(Debug, Clone, Serialize, Deserialize)]
1500pub struct HoldingDetail {
1501    /// Industry ID
1502    #[serde(default)]
1503    pub industry_id: String,
1504    /// Industry name
1505    #[serde(default)]
1506    pub industry_name: String,
1507    /// Index counter ID (e.g. `BK/US/CP99000`)
1508    #[serde(default)]
1509    pub index: String,
1510    /// Index name
1511    #[serde(default)]
1512    pub index_name: String,
1513    /// Holding type (e.g. `E` for stock)
1514    #[serde(default)]
1515    pub holding_type: String,
1516    /// Holding type name
1517    #[serde(default)]
1518    pub holding_type_name: String,
1519}
1520
1521/// One element of an ETF asset allocation group
1522#[derive(Debug, Clone, Serialize, Deserialize)]
1523pub struct AssetAllocationItem {
1524    /// Element name
1525    pub name: String,
1526    /// Security code (holdings only, e.g. `NVDA`)
1527    #[serde(default)]
1528    pub code: String,
1529    /// Position ratio (e.g. `0.0861114`)
1530    pub position_ratio: String,
1531    /// Security symbol (holdings only, e.g. `NVDA.US`)
1532    #[serde(
1533        rename = "counter_id",
1534        deserialize_with = "deserialize_counter_id_as_symbol",
1535        default
1536    )]
1537    pub symbol: String,
1538    /// Localized names (locale → name, e.g. `zh-CN` → `英伟达`)
1539    #[serde(rename = "name_locales_map", default)]
1540    pub name_locales: HashMap<String, String>,
1541    /// Holding detail (holdings only)
1542    #[serde(default)]
1543    pub holding_detail: Option<HoldingDetail>,
1544}
1545
1546/// One ETF asset allocation group (grouped by element type)
1547#[derive(Debug, Clone, Serialize, Deserialize)]
1548pub struct AssetAllocationGroup {
1549    /// Report date (e.g. `20260601`)
1550    pub report_date: String,
1551    /// Element type of this group
1552    pub asset_type: ElementType,
1553    /// Elements
1554    #[serde(default)]
1555    pub lists: Vec<AssetAllocationItem>,
1556}
1557
1558/// Response for [`crate::FundamentalContext::etf_asset_allocation`]
1559#[derive(Debug, Clone, Serialize, Deserialize)]
1560pub struct AssetAllocationResponse {
1561    /// Asset allocation groups
1562    #[serde(default)]
1563    pub info: Vec<AssetAllocationGroup>,
1564}
1565
1566// ── macroeconomic ─────────────────────────────────────────────────────
1567
1568/// Country for filtering macroeconomic indicators
1569#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
1570pub enum MacroeconomicCountry {
1571    /// Hong Kong SAR China
1572    #[serde(rename = "Hong Kong SAR China")]
1573    HongKong,
1574    /// China (Mainland)
1575    #[serde(rename = "China (Mainland)")]
1576    China,
1577    /// United States
1578    #[serde(rename = "United States")]
1579    UnitedStates,
1580    /// Euro Zone
1581    #[serde(rename = "Euro Zone")]
1582    EuroZone,
1583    /// Japan
1584    #[serde(rename = "Japan")]
1585    Japan,
1586    /// Singapore
1587    #[serde(rename = "Singapore")]
1588    Singapore,
1589}
1590
1591/// Importance level of a macroeconomic indicator
1592#[derive(Debug, Copy, Clone, Eq, PartialEq)]
1593pub enum MacroeconomicImportance {
1594    /// Low importance
1595    Low = 1,
1596    /// Medium importance
1597    Medium = 2,
1598    /// High importance
1599    High = 3,
1600}
1601
1602impl MacroeconomicImportance {
1603    /// Convert from raw API integer value
1604    pub fn from_i32(v: i32) -> Option<Self> {
1605        match v {
1606            1 => Some(Self::Low),
1607            2 => Some(Self::Medium),
1608            3 => Some(Self::High),
1609            _ => None,
1610        }
1611    }
1612}
1613
1614/// Localized text in simplified Chinese, traditional Chinese, and English
1615#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1616pub struct MultiLanguageText {
1617    /// English
1618    #[serde(default)]
1619    pub english: String,
1620    /// Simplified Chinese
1621    #[serde(default)]
1622    pub simplified_chinese: String,
1623    /// Traditional Chinese
1624    #[serde(default)]
1625    pub traditional_chinese: String,
1626}
1627
1628/// Metadata for one macroeconomic indicator
1629#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1630pub struct MacroeconomicIndicator {
1631    /// External vendor code (used as input to `macroeconomic`)
1632    pub indicator_code: String,
1633    /// Publishing organisation
1634    #[serde(default)]
1635    pub source_org: String,
1636    /// Country
1637    #[serde(default)]
1638    pub country: String,
1639    /// Indicator name
1640    #[serde(default)]
1641    pub name: String,
1642    /// Adjustment factor
1643    #[serde(default)]
1644    pub adjustment_factor: String,
1645    /// Release periodicity (e.g. `monthly` / `quarterly`)
1646    #[serde(default)]
1647    pub periodicity: String,
1648    /// Indicator category
1649    #[serde(default)]
1650    pub category: String,
1651    /// Description
1652    #[serde(default)]
1653    pub describe: String,
1654    /// Importance — higher is more important
1655    #[serde(default)]
1656    pub importance: i32,
1657    /// Start date of data coverage
1658    #[serde(
1659        default,
1660        with = "crate::serde_utils::rfc3339_opt",
1661        rename = "start_date"
1662    )]
1663    pub start_date: Option<OffsetDateTime>,
1664}
1665
1666/// Response for [`crate::FundamentalContext::macroeconomic_indicators`]
1667#[derive(Debug, Clone, Serialize, Deserialize)]
1668pub struct MacroeconomicIndicatorListResponse {
1669    /// Indicator list
1670    #[serde(default, rename = "list")]
1671    pub data: Vec<MacroeconomicIndicator>,
1672    /// Total number of indicators matching the query
1673    #[serde(default)]
1674    pub count: i32,
1675}
1676
1677/// One historical data point for a macroeconomic indicator
1678#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1679pub struct Macroeconomic {
1680    /// Statistical period (e.g. `2024-Q1`, `2024-03`)
1681    #[serde(default)]
1682    pub period: String,
1683    /// Release datetime
1684    #[serde(default, with = "crate::serde_utils::rfc3339_opt")]
1685    pub release_at: Option<OffsetDateTime>,
1686    /// Actual value
1687    #[serde(default)]
1688    pub actual_value: String,
1689    /// Previous value
1690    #[serde(default)]
1691    pub previous_value: String,
1692    /// Forecast value (market consensus)
1693    #[serde(default)]
1694    pub forecast_value: String,
1695    /// Revised value
1696    #[serde(default)]
1697    pub revised_value: String,
1698    /// Next release datetime
1699    #[serde(default, with = "crate::serde_utils::rfc3339_opt")]
1700    pub next_release_at: Option<OffsetDateTime>,
1701    /// Unit
1702    #[serde(default)]
1703    pub unit: String,
1704    /// Unit prefix / data scale (e.g. millions / billions)
1705    #[serde(default)]
1706    pub unit_prefix: String,
1707}
1708
1709/// Response for [`crate::FundamentalContext::macroeconomic`]
1710#[derive(Debug, Clone, Serialize, Deserialize)]
1711pub struct MacroeconomicResponse {
1712    /// Indicator metadata
1713    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1714    pub info: MacroeconomicIndicator,
1715    /// Historical data points
1716    #[serde(default)]
1717    pub data: Vec<Macroeconomic>,
1718    /// Total number of historical data points
1719    #[serde(default)]
1720    pub count: i32,
1721}
1722
1723// ── v2 wire types (internal, used for mapping to existing public types) ──────
1724
1725/// v2 wire: one indicator from GET /v2/quote/macrodata
1726#[derive(Debug, Clone, Serialize, Deserialize)]
1727pub(crate) struct V2MacroIndicator {
1728    #[serde(default)]
1729    pub indicator_id: i32,
1730    #[serde(default)]
1731    pub indicator_name: String,
1732    #[serde(default)]
1733    pub market: String,
1734    #[serde(default)]
1735    pub importance: i32,
1736    #[serde(default)]
1737    pub description: String,
1738    /// Update frequency: day/week/month/quarter/half_year/year
1739    #[serde(default)]
1740    pub frequence: String,
1741}
1742
1743/// v2 wire: response from GET /v2/quote/macrodata
1744#[derive(Debug, Clone, Serialize, Deserialize)]
1745pub(crate) struct V2MacroIndicatorListResponse {
1746    #[serde(default)]
1747    pub indicator_list: Vec<V2MacroIndicator>,
1748    /// Total count for pagination
1749    #[serde(default)]
1750    pub total: i32,
1751}
1752
1753/// v2 wire: one data point from GET /v2/quote/macrodata/:id
1754#[derive(Debug, Clone, Serialize, Deserialize)]
1755pub(crate) struct V2IndicatorDataDetail {
1756    #[serde(default)]
1757    pub actual_data: String,
1758    #[serde(default)]
1759    pub previous_data: String,
1760    #[serde(default)]
1761    pub estimated_data: String,
1762    #[serde(default)]
1763    pub published_time: String,
1764    #[serde(default)]
1765    pub observation_date: String,
1766}
1767
1768/// v2 wire: one indicator with data from GET /v2/quote/macrodata/:id
1769#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1770pub(crate) struct V2MacroIndicatorDetail {
1771    #[serde(default)]
1772    pub indicator_id: i32,
1773    #[serde(default)]
1774    pub indicator_name: String,
1775    #[serde(default)]
1776    pub unit: String,
1777    #[serde(default)]
1778    pub description: String,
1779    #[serde(default)]
1780    pub market: String,
1781    #[serde(default)]
1782    pub frequence: String,
1783    #[serde(default)]
1784    pub importance: i32,
1785    #[serde(default)]
1786    pub indicator_data: Vec<V2IndicatorDataDetail>,
1787}
1788
1789/// v2 wire: response from GET /v2/quote/macrodata/:id
1790/// (GetMacroIndicatorHistoryResp)
1791#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1792pub(crate) struct V2MacroIndicatorDataResponse {
1793    /// Single indicator with paginated data points
1794    #[serde(default)]
1795    pub indicator: V2MacroIndicatorDetail,
1796    /// Total data points matching the query (for pagination)
1797    #[serde(default)]
1798    pub total: i32,
1799}