Skip to main content

longbridge/fundamental/
types.rs

1#![allow(missing_docs)]
2
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5use strum_macros::{Display, EnumString};
6use time::OffsetDateTime;
7
8use crate::utils::counter::deserialize_counter_id_as_symbol;
9
10// ── financial_report ─────────────────────────────────────────────
11
12/// Response for [`crate::FundamentalContext::financial_report`]
13///
14/// The `list` field contains deeply-nested indicator/account/value data keyed
15/// by report kind (`"IS"`, `"BS"`, `"CF"`).  The exact structure varies and is
16/// preserved as raw JSON.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct FinancialReports {
19    /// Raw nested financial data. Top-level keys are report kinds such as
20    /// `"IS"` (income statement), `"BS"` (balance sheet), `"CF"` (cash flow).
21    pub list: serde_json::Value,
22}
23
24// ── dividend ─────────────────────────────────────────────────────
25
26/// Response for [`crate::FundamentalContext::dividend`] and
27/// [`crate::FundamentalContext::dividend_detail`]
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct DividendList {
30    /// List of dividend events
31    pub list: Vec<DividendItem>,
32}
33
34/// A single dividend / distribution event
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct DividendItem {
37    /// Security symbol, e.g. `"700.HK"`
38    #[serde(
39        rename = "counter_id",
40        deserialize_with = "deserialize_counter_id_as_symbol"
41    )]
42    pub symbol: String,
43    /// Internal record ID (may be absent in dividend_detail response)
44    #[serde(default)]
45    pub id: String,
46    /// Human-readable description, e.g. `"每股派息 5.3 HKD"`
47    pub desc: String,
48    /// Record / book-close date, e.g. `"2026.05.18"`
49    pub record_date: String,
50    /// Ex-dividend date, e.g. `"2026.05.15"`
51    pub ex_date: String,
52    /// Payment date, e.g. `"2026.06.01"`
53    pub payment_date: String,
54}
55
56// ── institution_rating ────────────────────────────────────────────
57
58/// Combined analyst-rating response for
59/// [`crate::FundamentalContext::institution_rating`]
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct InstitutionRating {
62    /// Latest snapshot from `/v1/quote/institution-rating-latest`
63    pub latest: InstitutionRatingLatest,
64    /// Consensus summary from `/v1/quote/institution-ratings`
65    pub summary: InstitutionRatingSummary,
66}
67
68/// Latest analyst-rating snapshot
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct InstitutionRatingLatest {
71    /// Rating distribution counts and date range
72    pub evaluate: RatingEvaluate,
73    /// Target price range
74    pub target: RatingTarget,
75    /// Industry classification ID
76    pub industry_id: i64,
77    /// Industry name
78    pub industry_name: String,
79    /// Rank of this security within the industry (1 = highest)
80    pub industry_rank: i32,
81    /// Total number of securities in the industry
82    pub industry_total: i32,
83    /// Mean analyst count in the industry
84    pub industry_mean: i32,
85    /// Median analyst count in the industry
86    pub industry_median: i32,
87}
88
89/// Analyst rating distribution counts
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct RatingEvaluate {
92    /// Number of "Buy" ratings
93    pub buy: i32,
94    /// Number of "Strong Buy" / "Outperform" ratings
95    pub over: i32,
96    /// Number of "Hold" / "Neutral" ratings
97    pub hold: i32,
98    /// Number of "Underperform" ratings
99    pub under: i32,
100    /// Number of "Sell" ratings
101    pub sell: i32,
102    /// Number of "No Opinion" ratings
103    pub no_opinion: i32,
104    /// Total analyst count
105    pub total: i32,
106    /// Window start (unix timestamp string; `"0"` means unset)
107    pub start_date: String,
108    /// Window end (unix timestamp string; `"0"` means unset)
109    pub end_date: String,
110}
111
112/// Analyst target price range
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RatingTarget {
115    /// Highest price target
116    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
117    pub highest_price: Option<Decimal>,
118    /// Lowest price target
119    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
120    pub lowest_price: Option<Decimal>,
121    /// Previous close price
122    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
123    pub prev_close: Option<Decimal>,
124    /// Window start (unix timestamp string)
125    pub start_date: String,
126    /// Window end (unix timestamp string)
127    pub end_date: String,
128}
129
130/// Consensus summary from `/v1/quote/institution-ratings`
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct InstitutionRatingSummary {
133    /// Currency symbol, e.g. `"HK$"`
134    pub ccy_symbol: String,
135    /// Change vs previous period
136    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
137    pub change: Option<Decimal>,
138    /// Simplified rating distribution
139    pub evaluate: RatingSummaryEvaluate,
140    /// Overall recommendation
141    pub recommend: InstitutionRecommend,
142    /// Consensus target price
143    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
144    pub target: Option<Decimal>,
145    /// Last updated display string, e.g. `"2026 年 5 月 5 日"`
146    pub updated_at: String,
147}
148
149/// Simplified rating distribution for the consensus summary
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct RatingSummaryEvaluate {
152    /// Number of "Buy" ratings
153    pub buy: i32,
154    /// Date of the latest update
155    pub date: String,
156    /// Number of "Hold" ratings
157    pub hold: i32,
158    /// Number of "Sell" ratings
159    pub sell: i32,
160    /// Number of "Strong Buy" ratings
161    pub strong_buy: i32,
162    /// Number of "Underperform" ratings
163    pub under: i32,
164}
165
166// ── institution_rating_detail ─────────────────────────────────────
167
168/// Response for [`crate::FundamentalContext::institution_rating_detail`]
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct InstitutionRatingDetail {
171    /// Currency symbol, e.g. `"HK$"`
172    pub ccy_symbol: String,
173    /// Historical rating distribution time-series
174    pub evaluate: InstitutionRatingDetailEvaluate,
175    /// Historical target price time-series
176    pub target: InstitutionRatingDetailTarget,
177}
178
179/// Historical rating distribution time-series
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct InstitutionRatingDetailEvaluate {
182    /// Weekly snapshots ordered from oldest to newest
183    pub list: Vec<InstitutionRatingDetailEvaluateItem>,
184}
185
186/// One weekly rating distribution snapshot
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct InstitutionRatingDetailEvaluateItem {
189    /// Number of "Buy" ratings
190    pub buy: i32,
191    /// Date in `"2021/05/14"` format
192    pub date: String,
193    /// Number of "Hold" ratings
194    pub hold: i32,
195    /// Number of "Sell" ratings
196    pub sell: i32,
197    /// Number of "Strong Buy" / "Outperform" ratings
198    #[serde(default)]
199    pub strong_buy: i32,
200    /// Number of "No Opinion" ratings
201    #[serde(default)]
202    pub no_opinion: i32,
203    /// Number of "Underperform" ratings
204    pub under: i32,
205}
206
207/// Historical target price time-series
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct InstitutionRatingDetailTarget {
210    /// Prediction accuracy ratio, e.g. `"0.9934"` (may be `null`)
211    pub data_percent: Option<Decimal>,
212    /// Overall prediction accuracy percentage string
213    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
214    pub prediction_accuracy: Option<Decimal>,
215    /// Last updated display string
216    pub updated_at: String,
217    /// Weekly target price snapshots
218    pub list: Vec<InstitutionRatingDetailTargetItem>,
219}
220
221/// One weekly target price snapshot
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct InstitutionRatingDetailTargetItem {
224    /// Average target price
225    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
226    pub avg_target: Option<Decimal>,
227    /// Date in `"2021/05/16"` format
228    pub date: String,
229    /// Highest target price
230    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
231    pub max_target: Option<Decimal>,
232    /// Lowest target price
233    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
234    pub min_target: Option<Decimal>,
235    /// Whether the stock price reached the target
236    pub meet: bool,
237    /// Actual stock price at this date
238    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
239    pub price: Option<Decimal>,
240    /// Unix timestamp string
241    pub timestamp: String,
242}
243
244// ── forecast_eps ──────────────────────────────────────────────────
245
246/// Response for [`crate::FundamentalContext::forecast_eps`]
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct ForecastEps {
249    /// EPS forecast snapshots ordered by `forecast_start_date` ascending
250    pub items: Vec<ForecastEpsItem>,
251}
252
253/// One EPS forecast snapshot
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct ForecastEpsItem {
256    /// Median EPS estimate
257    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
258    pub forecast_eps_median: Option<Decimal>,
259    /// Mean EPS estimate
260    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
261    pub forecast_eps_mean: Option<Decimal>,
262    /// Lowest EPS estimate
263    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
264    pub forecast_eps_lowest: Option<Decimal>,
265    /// Highest EPS estimate
266    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
267    pub forecast_eps_highest: Option<Decimal>,
268    /// Total number of forecasting institutions
269    pub institution_total: i32,
270    /// Number of institutions that raised their estimate
271    pub institution_up: i32,
272    /// Number of institutions that lowered their estimate
273    pub institution_down: i32,
274    /// Forecast window start
275    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
276    pub forecast_start_date: OffsetDateTime,
277    /// Forecast window end
278    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
279    pub forecast_end_date: OffsetDateTime,
280}
281
282// ── consensus ─────────────────────────────────────────────────────
283
284/// Response for [`crate::FundamentalContext::consensus`]
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct FinancialConsensus {
287    /// Per-period consensus reports
288    pub list: Vec<ConsensusReport>,
289    /// Index into `list` of the most recently released period
290    pub current_index: i32,
291    /// Reporting currency, e.g. `"HKD"`
292    pub currency: String,
293    /// Available period types, e.g. `["qf", "saf", "af"]`
294    #[serde(default)]
295    pub opt_periods: Vec<String>,
296    /// Currently returned period type
297    pub current_period: String,
298}
299
300/// Consensus report for one fiscal period
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct ConsensusReport {
303    /// Fiscal year, e.g. `2025`
304    pub fiscal_year: i32,
305    /// Fiscal period code, e.g. `"Q4"`
306    pub fiscal_period: String,
307    /// Human-readable period label, e.g. `"Q4 FY2025"`
308    pub period_text: String,
309    /// Per-metric consensus details
310    pub details: Vec<ConsensusDetail>,
311}
312
313/// Consensus estimate for one financial metric
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct ConsensusDetail {
316    /// Metric key, e.g. `"revenue"`, `"eps"`
317    pub key: String,
318    /// Display name
319    pub name: String,
320    /// Metric description
321    pub description: String,
322    /// Actual reported value (empty string if not yet released)
323    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
324    pub actual: Option<Decimal>,
325    /// Consensus estimate value
326    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
327    pub estimate: Option<Decimal>,
328    /// Actual minus estimate
329    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
330    pub comp_value: Option<Decimal>,
331    /// Beat/miss description, e.g. `"超出预期"`
332    pub comp_desc: String,
333    /// Comparison result code for colour coding
334    pub comp: String,
335    /// Whether the actual results have been published
336    pub is_released: bool,
337}
338
339// ── valuation ─────────────────────────────────────────────────────
340
341/// Response for [`crate::FundamentalContext::valuation`]
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct ValuationData {
344    /// Valuation metrics (PE / PB / PS / dividend yield)
345    pub metrics: ValuationMetricsData,
346}
347
348/// Container for all valuation metrics
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct ValuationMetricsData {
351    /// Price-to-Earnings ratio history
352    pub pe: Option<ValuationMetricData>,
353    /// Price-to-Book ratio history
354    pub pb: Option<ValuationMetricData>,
355    /// Price-to-Sales ratio history
356    pub ps: Option<ValuationMetricData>,
357    /// Dividend yield history
358    pub dvd_yld: Option<ValuationMetricData>,
359}
360
361/// Historical time-series for one valuation metric
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct ValuationMetricData {
364    /// Human-readable description with current value and percentile
365    pub desc: String,
366    /// Historical high value
367    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
368    pub high: Option<Decimal>,
369    /// Historical low value
370    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
371    pub low: Option<Decimal>,
372    /// Historical median value
373    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
374    pub median: Option<Decimal>,
375    /// Historical data points
376    pub list: Vec<ValuationPoint>,
377}
378
379/// One valuation data point
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct ValuationPoint {
382    /// Date of the data point
383    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
384    pub timestamp: OffsetDateTime,
385    /// Metric value
386    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
387    pub value: Option<Decimal>,
388}
389
390// ── valuation_history ─────────────────────────────────────────────
391
392/// Response for [`crate::FundamentalContext::valuation_history`]
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct ValuationHistoryResponse {
395    /// Historical valuation data
396    pub history: ValuationHistoryData,
397}
398
399/// Container for historical valuation metrics
400#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct ValuationHistoryData {
402    /// Historical metrics (PE / PB / PS)
403    pub metrics: ValuationHistoryMetrics,
404}
405
406/// Historical valuation metrics container
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct ValuationHistoryMetrics {
409    /// Price-to-Earnings history
410    pub pe: Option<ValuationHistoryMetric>,
411    /// Price-to-Book history
412    pub pb: Option<ValuationHistoryMetric>,
413    /// Price-to-Sales history
414    pub ps: Option<ValuationHistoryMetric>,
415}
416
417/// Historical data for one valuation metric including statistical bounds
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct ValuationHistoryMetric {
420    /// Human-readable description
421    pub desc: String,
422    /// Historical high over the period
423    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
424    pub high: Option<Decimal>,
425    /// Historical low over the period
426    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
427    pub low: Option<Decimal>,
428    /// Historical median over the period
429    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
430    pub median: Option<Decimal>,
431    /// Historical data points
432    pub list: Vec<ValuationPoint>,
433}
434
435// ── industry_valuation ────────────────────────────────────────────
436
437/// Response for [`crate::FundamentalContext::industry_valuation`]
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct IndustryValuationList {
440    /// List of peer securities with their valuation data
441    pub list: Vec<IndustryValuationItem>,
442}
443
444/// Valuation data for one peer security
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct IndustryValuationItem {
447    /// Security symbol, e.g. `"700.HK"`
448    #[serde(
449        rename = "counter_id",
450        deserialize_with = "deserialize_counter_id_as_symbol"
451    )]
452    pub symbol: String,
453    /// Company name
454    pub name: String,
455    /// Reporting currency
456    pub currency: String,
457    /// Total assets
458    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
459    pub assets: Option<Decimal>,
460    /// Book value per share
461    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
462    pub bps: Option<Decimal>,
463    /// Earnings per share
464    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
465    pub eps: Option<Decimal>,
466    /// Dividends per share
467    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
468    pub dps: Option<Decimal>,
469    /// Dividend yield
470    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
471    pub div_yld: Option<Decimal>,
472    /// Dividend payout ratio
473    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
474    pub div_payout_ratio: Option<Decimal>,
475    /// 5-year average dividends per share
476    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
477    pub five_y_avg_dps: Option<Decimal>,
478    /// Current PE ratio
479    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
480    pub pe: Option<Decimal>,
481    /// Historical PE/PB/PS snapshots
482    pub history: Vec<IndustryValuationHistory>,
483}
484
485/// Historical valuation snapshot for an industry peer
486#[derive(Debug, Clone, Serialize, Deserialize)]
487pub struct IndustryValuationHistory {
488    /// Unix timestamp string
489    pub date: String,
490    /// Price-to-Earnings ratio
491    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
492    pub pe: Option<Decimal>,
493    /// Price-to-Book ratio
494    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
495    pub pb: Option<Decimal>,
496    /// Price-to-Sales ratio
497    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
498    pub ps: Option<Decimal>,
499}
500
501// ── industry_valuation_dist ───────────────────────────────────────
502
503/// Response for [`crate::FundamentalContext::industry_valuation_dist`]
504#[derive(Debug, Clone, Serialize, Deserialize)]
505pub struct IndustryValuationDist {
506    /// PE ratio distribution within the industry
507    pub pe: Option<ValuationDist>,
508    /// PB ratio distribution within the industry
509    pub pb: Option<ValuationDist>,
510    /// PS ratio distribution within the industry
511    pub ps: Option<ValuationDist>,
512}
513
514/// Distribution statistics for one valuation metric within an industry
515#[derive(Debug, Clone, Serialize, Deserialize)]
516pub struct ValuationDist {
517    /// Minimum value in the industry
518    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
519    pub low: Option<Decimal>,
520    /// Maximum value in the industry
521    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
522    pub high: Option<Decimal>,
523    /// Median value in the industry
524    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
525    pub median: Option<Decimal>,
526    /// Current value of the queried security
527    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
528    pub value: Option<Decimal>,
529    /// Percentile ranking (0–1 range as string)
530    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
531    pub ranking: Option<Decimal>,
532    /// Ordinal rank index (1-based)
533    pub rank_index: String,
534    /// Total number of securities in the industry
535    pub rank_total: String,
536}
537
538// ── company ───────────────────────────────────────────────────────
539
540/// Response for [`crate::FundamentalContext::company`]
541#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct CompanyOverview {
543    /// Short name, e.g. `"腾讯控股"`
544    pub name: String,
545    /// Full legal name
546    pub company_name: String,
547    /// Founding date
548    pub founded: String,
549    /// Listing date
550    pub listing_date: String,
551    /// Primary listing market display name
552    pub market: String,
553    /// Market region code, e.g. `"HK"`
554    pub region: String,
555    /// Registered address
556    pub address: String,
557    /// Principal office address
558    pub office_address: String,
559    /// Company website
560    pub website: String,
561    /// IPO issue price
562    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
563    pub issue_price: Option<Decimal>,
564    /// Number of shares offered at IPO
565    pub shares_offered: String,
566    /// Chairman name
567    pub chairman: String,
568    /// Company secretary name
569    pub secretary: String,
570    /// Auditing institution
571    pub audit_inst: String,
572    /// Company classification category
573    pub category: String,
574    /// Fiscal year end, e.g. `"12 月 31 日"`
575    pub year_end: String,
576    /// Number of employees
577    pub employees: String,
578    /// Phone number (API field name is `"Phone"`)
579    #[serde(rename = "Phone")]
580    pub phone: String,
581    /// Fax number
582    pub fax: String,
583    /// Investor relations email
584    pub email: String,
585    /// Legal representative
586    pub legal_repr: String,
587    /// CEO / Managing Director
588    pub manager: String,
589    /// Business licence number
590    pub bus_license: String,
591    /// Accounting firm
592    pub accounting_firm: String,
593    /// Securities representative
594    pub securities_rep: String,
595    /// Legal counsel
596    pub legal_counsel: String,
597    /// Postal code
598    pub zip_code: String,
599    /// Exchange ticker code, e.g. `"00700"`
600    pub ticker: String,
601    /// URL to the company's logo icon
602    pub icon: String,
603    /// Business profile / description
604    pub profile: String,
605    /// ADS ratio (may be empty)
606    #[serde(default)]
607    pub ads_ratio: String,
608    /// Industry sector code
609    pub sector: i32,
610}
611
612// ── executive ─────────────────────────────────────────────────────
613
614/// Response for [`crate::FundamentalContext::executive`]
615#[derive(Debug, Clone, Serialize, Deserialize)]
616pub struct ExecutiveList {
617    /// Groups of executives per security (usually one group)
618    pub professional_list: Vec<ExecutiveGroup>,
619}
620
621/// Executives for one security
622#[derive(Debug, Clone, Serialize, Deserialize)]
623pub struct ExecutiveGroup {
624    /// Security symbol
625    #[serde(
626        rename = "counter_id",
627        deserialize_with = "deserialize_counter_id_as_symbol"
628    )]
629    pub symbol: String,
630    /// Link to the company wiki page
631    pub forward_url: String,
632    /// Total number of executives
633    pub total: i32,
634    /// Individual executive entries
635    pub professionals: Vec<Professional>,
636}
637
638/// One executive / board member
639#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct Professional {
641    /// Internal wiki person ID (string form)
642    pub id: String,
643    /// Full name
644    pub name: String,
645    /// Full name in Simplified Chinese
646    pub name_zhcn: String,
647    /// Full name in English
648    pub name_en: String,
649    /// Job title, e.g. `"Co-Founder, Chairman & CEO"`
650    pub title: String,
651    /// Biography text
652    pub biography: String,
653    /// URL to the person's photo
654    pub photo: String,
655    /// URL to the wiki profile page
656    pub wiki_url: String,
657}
658
659// ── shareholder ───────────────────────────────────────────────────
660
661/// Response for [`crate::FundamentalContext::shareholder`]
662#[derive(Debug, Clone, Serialize, Deserialize)]
663pub struct ShareholderList {
664    /// List of major shareholders
665    pub shareholder_list: Vec<Shareholder>,
666    /// Link to the full shareholder page
667    #[serde(default)]
668    pub forward_url: String,
669    /// Total number of shareholders returned
670    pub total: i32,
671}
672
673/// One major shareholder
674#[derive(Debug, Clone, Serialize, Deserialize)]
675pub struct Shareholder {
676    /// Internal shareholder ID (string form)
677    pub shareholder_id: String,
678    /// Shareholder name
679    pub shareholder_name: String,
680    /// Institution type (may be empty)
681    pub institution_type: String,
682    /// Percentage of shares held
683    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
684    pub percent_of_shares: Option<Decimal>,
685    /// Change in shares held (positive = bought, negative = sold)
686    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
687    pub shares_changed: Option<Decimal>,
688    /// Date of the most recent filing, e.g. `"2026-05-04"`
689    pub report_date: String,
690    /// Other securities held by this shareholder (cross-holdings)
691    #[serde(default)]
692    pub stocks: Vec<ShareholderStock>,
693}
694
695/// A security in an institutional shareholder's cross-holdings
696#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct ShareholderStock {
698    /// Security symbol of the cross-held stock
699    #[serde(
700        rename = "counter_id",
701        deserialize_with = "deserialize_counter_id_as_symbol"
702    )]
703    pub symbol: String,
704    /// Ticker code, e.g. `"BLK"`
705    pub code: String,
706    /// Market, e.g. `"US"`
707    pub market: String,
708    /// Day change percentage, e.g. `"-0.32%"`
709    pub chg: String,
710}
711
712// ── fund_holder ───────────────────────────────────────────────────
713
714/// Response for [`crate::FundamentalContext::fund_holder`]
715#[derive(Debug, Clone, Serialize, Deserialize)]
716pub struct FundHolders {
717    /// Funds and ETFs that hold the queried security
718    pub lists: Vec<FundHolder>,
719}
720
721/// A fund or ETF that holds the queried security
722#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct FundHolder {
724    /// Fund/ETF ticker code, e.g. `"513050"`
725    pub code: String,
726    /// Fund/ETF symbol, e.g. `"ETF/SH/513050"` → converted to `"513050.SH"`
727    #[serde(
728        rename = "counter_id",
729        deserialize_with = "deserialize_counter_id_as_symbol"
730    )]
731    pub symbol: String,
732    /// Reporting currency, e.g. `"CNY"`
733    pub currency: String,
734    /// Fund/ETF full name
735    pub name: String,
736    /// Position ratio as a percentage decimal
737    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
738    pub position_ratio: Decimal,
739    /// Report date, e.g. `"2025.12.31"`
740    pub report_date: String,
741}
742
743// ── corp_action ───────────────────────────────────────────────────
744
745/// Response for [`crate::FundamentalContext::corp_action`]
746#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct CorpActions {
748    /// Corporate action events
749    pub items: Vec<CorpActionItem>,
750}
751
752/// One corporate action event
753#[derive(Debug, Clone, Serialize, Deserialize)]
754pub struct CorpActionItem {
755    /// Internal event ID
756    pub id: String,
757    /// Date in `YYYYMMDD` format, e.g. `"20260601"`
758    pub date: String,
759    /// Short display date, e.g. `"06.01"`
760    pub date_str: String,
761    /// Date type label, e.g. `"派息日"`, `"除权日"`
762    pub date_type: String,
763    /// Time zone description, e.g. `"北京时间"`
764    pub date_zone: String,
765    /// Event category, e.g. `"分配方案"`
766    pub act_type: String,
767    /// Human-readable event description
768    pub act_desc: String,
769    /// Machine-readable action code, e.g. `"DividendExDate"`
770    pub action: String,
771    /// Whether this is a recent event
772    pub recent: bool,
773    /// Whether publication was delayed
774    pub is_delay: bool,
775    /// Delay announcement content (if `is_delay` is `true`)
776    pub delay_content: String,
777    /// Associated live stream (if any)
778    pub live: Option<CorpActionLive>,
779    /// Associated security info (rarely populated; preserved as raw JSON)
780    pub security: Option<serde_json::Value>,
781}
782
783/// Live stream associated with a corporate action
784#[derive(Debug, Clone, Serialize, Deserialize)]
785pub struct CorpActionLive {
786    /// Live stream ID
787    pub id: String,
788    /// Status code: 1=preview, 2=live, 3=ended, 4=replay, 5=processing
789    pub status: serde_json::Value, // API may return int or string
790    /// Start time
791    pub started_at: String,
792    /// Stream title
793    pub name: String,
794    /// Icon URL
795    pub icon: String,
796}
797
798// ── invest_relation ───────────────────────────────────────────────
799
800/// Response for [`crate::FundamentalContext::invest_relation`]
801#[derive(Debug, Clone, Serialize, Deserialize)]
802pub struct InvestRelations {
803    /// Link to the full investor-relations page
804    #[serde(default)]
805    pub forward_url: String,
806    /// Securities in which the queried company holds a stake
807    pub invest_securities: Vec<InvestSecurity>,
808}
809
810/// A security in which the queried company has an investment stake
811#[derive(Debug, Clone, Serialize, Deserialize)]
812pub struct InvestSecurity {
813    /// Internal company ID (string form; may be `"0"`)
814    pub company_id: String,
815    /// Company name (locale-aware)
816    pub company_name: String,
817    /// Company name in English
818    pub company_name_en: String,
819    /// Company name in Simplified Chinese
820    pub company_name_zhcn: String,
821    /// Security symbol of the invested company
822    #[serde(
823        rename = "counter_id",
824        deserialize_with = "deserialize_counter_id_as_symbol"
825    )]
826    pub symbol: String,
827    /// Reporting currency
828    pub currency: String,
829    /// Percentage of shares held
830    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
831    pub percent_of_shares: Option<Decimal>,
832    /// Shareholder rank, e.g. `"1"` = largest shareholder
833    pub shares_rank: String,
834    /// Market value of the holding
835    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
836    pub shares_value: Option<Decimal>,
837}
838
839// ── operating ─────────────────────────────────────────────────────
840
841/// Response for [`crate::FundamentalContext::operating`]
842#[derive(Debug, Clone, Serialize, Deserialize)]
843pub struct OperatingList {
844    /// List of operating summary reports
845    pub list: Vec<OperatingItem>,
846}
847
848/// One operating summary report (annual / quarterly)
849#[derive(Debug, Clone, Serialize, Deserialize)]
850pub struct OperatingItem {
851    /// Internal report ID
852    pub id: String,
853    /// Report period code, e.g. `"af"` (annual), `"qf"` (quarterly)
854    pub report: String,
855    /// Report title, e.g. `"2025 财年年报"`
856    pub title: String,
857    /// Management discussion text
858    pub txt: String,
859    /// Whether this is the most recent report
860    pub latest: bool,
861    /// Keyword tags (structure undocumented; usually empty)
862    #[serde(default)]
863    pub keywords: Vec<serde_json::Value>,
864    /// URL to the full community report page
865    #[serde(default)]
866    pub web_url: String,
867    /// Key financial metrics extracted from the report
868    pub financial: OperatingFinancial,
869}
870
871/// Key financial metrics extracted from an operating report
872#[derive(Debug, Clone, Serialize, Deserialize)]
873pub struct OperatingFinancial {
874    /// Ticker code (may be empty)
875    pub code: String,
876    /// Raw counter ID (may be empty)
877    pub counter_id: String,
878    /// Reporting currency
879    pub currency: String,
880    /// Company name
881    pub name: String,
882    /// Market region
883    pub region: String,
884    /// Report period code
885    pub report: String,
886    /// Report period display text
887    pub report_txt: String,
888    /// Financial indicators
889    pub indicators: Vec<OperatingIndicator>,
890}
891
892/// One financial indicator in an operating report
893#[derive(Debug, Clone, Serialize, Deserialize)]
894pub struct OperatingIndicator {
895    /// Field name key, e.g. `"operating_revenue"`
896    pub field_name: String,
897    /// Display name, e.g. `"营业收入"`
898    pub indicator_name: String,
899    /// Formatted value, e.g. `"8217 亿"`
900    pub indicator_value: String,
901    /// Year-over-year change
902    #[serde(default, with = "crate::serde_utils::decimal_opt_str_is_none")]
903    pub yoy: Option<Decimal>,
904}
905
906// ── buyback ───────────────────────────────────────────────────────
907
908/// Response for [`crate::FundamentalContext::buyback`]
909#[derive(Debug, Clone, Serialize, Deserialize)]
910pub struct BuybackData {
911    /// Most recent buyback summary (TTM)
912    #[serde(default)]
913    pub recent_buybacks: Option<RecentBuybacks>,
914    /// Historical annual buyback data
915    #[serde(default)]
916    pub buyback_history: Vec<BuybackHistoryItem>,
917    /// Buyback payout and cash-flow ratios
918    #[serde(default)]
919    pub buyback_ratios: Vec<BuybackRatios>,
920}
921
922/// TTM (trailing twelve months) buyback summary
923#[derive(Debug, Clone, Serialize, Deserialize)]
924pub struct RecentBuybacks {
925    /// Reporting currency
926    pub currency: String,
927    /// Net buyback amount TTM
928    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
929    pub net_buyback_ttm: Option<Decimal>,
930    /// Net buyback yield TTM
931    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
932    pub net_buyback_yield_ttm: Option<Decimal>,
933}
934
935/// Historical annual buyback data point
936#[derive(Debug, Clone, Serialize, Deserialize)]
937pub struct BuybackHistoryItem {
938    /// Fiscal year label, e.g. `"FY2024"`
939    pub fiscal_year: String,
940    /// Fiscal year date range string
941    pub fiscal_year_range: String,
942    /// Net buyback amount
943    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
944    pub net_buyback: Option<Decimal>,
945    /// Net buyback yield
946    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
947    pub net_buyback_yield: Option<Decimal>,
948    /// Year-over-year net buyback growth rate
949    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
950    pub net_buyback_growth_rate: Option<Decimal>,
951    /// Reporting currency
952    pub currency: String,
953}
954
955/// Buyback payout and cash-flow ratios
956#[derive(Debug, Clone, Serialize, Deserialize)]
957pub struct BuybackRatios {
958    /// Net buyback payout ratio
959    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
960    pub net_buyback_payout_ratio: Option<Decimal>,
961    /// Net buyback to free cash-flow ratio
962    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
963    pub net_buyback_to_cashflow_ratio: Option<Decimal>,
964}
965
966// ── ratings ───────────────────────────────────────────────────────
967
968/// Response for [`crate::FundamentalContext::ratings`]
969#[derive(Debug, Clone, Serialize, Deserialize)]
970pub struct StockRatings {
971    /// Style display name
972    #[serde(default)]
973    pub style_txt_name: String,
974    /// Scale display name
975    #[serde(default)]
976    pub scale_txt_name: String,
977    /// Report period display text
978    #[serde(default)]
979    pub report_period_txt: String,
980    /// Composite score (may be int, float, or null)
981    #[serde(default)]
982    pub multi_score: serde_json::Value,
983    /// Composite score letter grade
984    #[serde(default)]
985    pub multi_letter: String,
986    /// Score change vs previous period
987    #[serde(default)]
988    pub multi_score_change: i32,
989    /// Industry name
990    #[serde(default)]
991    pub industry_name: String,
992    /// Industry rank (may be int or null)
993    #[serde(default)]
994    pub industry_rank: serde_json::Value,
995    /// Total securities in the industry
996    #[serde(default)]
997    pub industry_total: serde_json::Value,
998    /// Industry mean score
999    #[serde(default)]
1000    pub industry_mean_score: serde_json::Value,
1001    /// Industry median score
1002    #[serde(default)]
1003    pub industry_median_score: serde_json::Value,
1004    /// Detailed rating categories
1005    #[serde(default)]
1006    pub ratings: Vec<RatingCategory>,
1007}
1008
1009/// One rating category (e.g. growth, profitability)
1010#[derive(Debug, Clone, Serialize, Deserialize)]
1011pub struct RatingCategory {
1012    /// Category type code
1013    #[serde(rename = "type")]
1014    pub kind: i32,
1015    /// Sub-indicator groups within this category
1016    #[serde(default)]
1017    pub sub_indicators: Vec<RatingSubIndicatorGroup>,
1018}
1019
1020/// A group of sub-indicators under one category indicator
1021#[derive(Debug, Clone, Serialize, Deserialize)]
1022pub struct RatingSubIndicatorGroup {
1023    /// Parent indicator for this group
1024    pub indicator: RatingIndicator,
1025    /// Leaf sub-indicators
1026    #[serde(default)]
1027    pub sub_indicators: Vec<RatingLeafIndicator>,
1028}
1029
1030/// A rating indicator node (may be a parent or a leaf)
1031#[derive(Debug, Clone, Serialize, Deserialize)]
1032pub struct RatingIndicator {
1033    /// Indicator display name
1034    pub name: String,
1035    /// Score (may be int, float, or null)
1036    #[serde(default)]
1037    pub score: serde_json::Value,
1038    /// Letter grade
1039    #[serde(default)]
1040    pub letter: String,
1041}
1042
1043/// A leaf rating indicator with a raw value
1044#[derive(Debug, Clone, Serialize, Deserialize)]
1045pub struct RatingLeafIndicator {
1046    /// Indicator display name
1047    pub name: String,
1048    /// Formatted value string
1049    #[serde(default)]
1050    pub value: String,
1051    /// Value type hint, e.g. `"percent"`
1052    #[serde(default)]
1053    pub value_type: String,
1054    /// Score (may be int, float, or null)
1055    #[serde(default)]
1056    pub score: serde_json::Value,
1057    /// Letter grade
1058    #[serde(default)]
1059    pub letter: String,
1060}
1061
1062// ── enums ─────────────────────────────────────────────────────────
1063
1064/// Institutional analyst recommendation
1065#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
1066pub enum InstitutionRecommend {
1067    /// Unknown
1068    Unknown,
1069    /// Strong buy
1070    #[strum(serialize = "strong_buy")]
1071    StrongBuy,
1072    /// Buy
1073    #[strum(serialize = "buy")]
1074    Buy,
1075    /// Hold
1076    #[strum(serialize = "hold")]
1077    Hold,
1078    /// Sell
1079    #[strum(serialize = "sell")]
1080    Sell,
1081    /// Strong sell
1082    #[strum(serialize = "strong_sell")]
1083    StrongSell,
1084    /// Underperform
1085    #[strum(serialize = "underperform")]
1086    Underperform,
1087    /// No opinion
1088    #[strum(serialize = "no_opinion")]
1089    NoOpinion,
1090}
1091
1092impl_default_for_enum_string!(InstitutionRecommend);
1093impl_serde_for_enum_string!(InstitutionRecommend);
1094
1095/// Financial report kind
1096#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
1097pub enum FinancialReportKind {
1098    /// Income statement
1099    #[serde(rename = "IS")]
1100    IncomeStatement,
1101    /// Balance sheet
1102    #[serde(rename = "BS")]
1103    BalanceSheet,
1104    /// Cash flow statement
1105    #[serde(rename = "CF")]
1106    CashFlow,
1107    /// All statements
1108    #[default]
1109    #[serde(rename = "ALL")]
1110    All,
1111}
1112
1113/// Financial report period type
1114#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
1115pub enum FinancialReportPeriod {
1116    /// Annual report
1117    #[serde(rename = "af")]
1118    Annual,
1119    /// Semi-annual report
1120    #[serde(rename = "saf")]
1121    SemiAnnual,
1122    /// Q1 report
1123    #[serde(rename = "q1")]
1124    Q1,
1125    /// Q2 report
1126    #[serde(rename = "q2")]
1127    Q2,
1128    /// Q3 report
1129    #[serde(rename = "q3")]
1130    Q3,
1131    /// Full quarterly report
1132    #[serde(rename = "qf")]
1133    QuarterlyFull,
1134    /// Three-quarter report (first three quarters)
1135    #[serde(rename = "3q")]
1136    ThreeQ,
1137}