longbridge 4.2.0

Longbridge OpenAPI SDK for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
#![allow(missing_docs)]

use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use strum_macros::{Display, EnumString};
use time::OffsetDateTime;

use crate::utils::counter::deserialize_counter_id_as_symbol;

// ── financial_report ─────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::financial_report`]
///
/// The `list` field contains deeply-nested indicator/account/value data keyed
/// by report kind (`"IS"`, `"BS"`, `"CF"`).  The exact structure varies and is
/// preserved as raw JSON.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FinancialReports {
    /// Raw nested financial data. Top-level keys are report kinds such as
    /// `"IS"` (income statement), `"BS"` (balance sheet), `"CF"` (cash flow).
    pub list: serde_json::Value,
}

// ── dividend ─────────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::dividend`] and
/// [`crate::FundamentalContext::dividend_detail`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DividendList {
    /// List of dividend events
    pub list: Vec<DividendItem>,
}

/// A single dividend / distribution event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DividendItem {
    /// Security symbol, e.g. `"700.HK"`
    #[serde(
        rename = "counter_id",
        deserialize_with = "deserialize_counter_id_as_symbol"
    )]
    pub symbol: String,
    /// Internal record ID (may be absent in dividend_detail response)
    #[serde(default)]
    pub id: String,
    /// Human-readable description, e.g. `"每股派息 5.3 HKD"`
    pub desc: String,
    /// Record / book-close date, e.g. `"2026.05.18"`
    pub record_date: String,
    /// Ex-dividend date, e.g. `"2026.05.15"`
    pub ex_date: String,
    /// Payment date, e.g. `"2026.06.01"`
    pub payment_date: String,
}

// ── institution_rating ────────────────────────────────────────────

/// Combined analyst-rating response for
/// [`crate::FundamentalContext::institution_rating`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionRating {
    /// Latest snapshot from `/v1/quote/institution-rating-latest`
    pub latest: InstitutionRatingLatest,
    /// Consensus summary from `/v1/quote/institution-ratings`
    pub summary: InstitutionRatingSummary,
}

/// Latest analyst-rating snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionRatingLatest {
    /// Rating distribution counts and date range
    pub evaluate: RatingEvaluate,
    /// Target price range
    pub target: RatingTarget,
    /// Industry classification ID
    pub industry_id: i64,
    /// Industry name
    pub industry_name: String,
    /// Rank of this security within the industry (1 = highest)
    pub industry_rank: i32,
    /// Total number of securities in the industry
    pub industry_total: i32,
    /// Mean analyst count in the industry
    pub industry_mean: i32,
    /// Median analyst count in the industry
    pub industry_median: i32,
}

/// Analyst rating distribution counts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatingEvaluate {
    /// Number of "Buy" ratings
    pub buy: i32,
    /// Number of "Strong Buy" / "Outperform" ratings
    pub over: i32,
    /// Number of "Hold" / "Neutral" ratings
    pub hold: i32,
    /// Number of "Underperform" ratings
    pub under: i32,
    /// Number of "Sell" ratings
    pub sell: i32,
    /// Number of "No Opinion" ratings
    pub no_opinion: i32,
    /// Total analyst count
    pub total: i32,
    /// Window start (unix timestamp string; `"0"` means unset)
    pub start_date: String,
    /// Window end (unix timestamp string; `"0"` means unset)
    pub end_date: String,
}

/// Analyst target price range
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatingTarget {
    /// Highest price target
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub highest_price: Option<Decimal>,
    /// Lowest price target
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub lowest_price: Option<Decimal>,
    /// Previous close price
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub prev_close: Option<Decimal>,
    /// Window start (unix timestamp string)
    pub start_date: String,
    /// Window end (unix timestamp string)
    pub end_date: String,
}

/// Consensus summary from `/v1/quote/institution-ratings`
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionRatingSummary {
    /// Currency symbol, e.g. `"HK$"`
    pub ccy_symbol: String,
    /// Change vs previous period
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub change: Option<Decimal>,
    /// Simplified rating distribution
    pub evaluate: RatingSummaryEvaluate,
    /// Overall recommendation
    pub recommend: InstitutionRecommend,
    /// Consensus target price
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub target: Option<Decimal>,
    /// Last updated display string, e.g. `"2026 年 5 月 5 日"`
    pub updated_at: String,
}

/// Simplified rating distribution for the consensus summary
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatingSummaryEvaluate {
    /// Number of "Buy" ratings
    pub buy: i32,
    /// Date of the latest update
    pub date: String,
    /// Number of "Hold" ratings
    pub hold: i32,
    /// Number of "Sell" ratings
    pub sell: i32,
    /// Number of "Strong Buy" ratings
    pub strong_buy: i32,
    /// Number of "Underperform" ratings
    pub under: i32,
}

// ── institution_rating_detail ─────────────────────────────────────

/// Response for [`crate::FundamentalContext::institution_rating_detail`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionRatingDetail {
    /// Currency symbol, e.g. `"HK$"`
    pub ccy_symbol: String,
    /// Historical rating distribution time-series
    pub evaluate: InstitutionRatingDetailEvaluate,
    /// Historical target price time-series
    pub target: InstitutionRatingDetailTarget,
}

/// Historical rating distribution time-series
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionRatingDetailEvaluate {
    /// Weekly snapshots ordered from oldest to newest
    pub list: Vec<InstitutionRatingDetailEvaluateItem>,
}

/// One weekly rating distribution snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionRatingDetailEvaluateItem {
    /// Number of "Buy" ratings
    pub buy: i32,
    /// Date in `"2021/05/14"` format
    pub date: String,
    /// Number of "Hold" ratings
    pub hold: i32,
    /// Number of "Sell" ratings
    pub sell: i32,
    /// Number of "Strong Buy" / "Outperform" ratings
    #[serde(default)]
    pub strong_buy: i32,
    /// Number of "No Opinion" ratings
    #[serde(default)]
    pub no_opinion: i32,
    /// Number of "Underperform" ratings
    pub under: i32,
}

/// Historical target price time-series
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionRatingDetailTarget {
    /// Prediction accuracy ratio, e.g. `"0.9934"` (may be `null`)
    pub data_percent: Option<Decimal>,
    /// Overall prediction accuracy percentage string
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub prediction_accuracy: Option<Decimal>,
    /// Last updated display string
    pub updated_at: String,
    /// Weekly target price snapshots
    pub list: Vec<InstitutionRatingDetailTargetItem>,
}

/// One weekly target price snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionRatingDetailTargetItem {
    /// Average target price
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub avg_target: Option<Decimal>,
    /// Date in `"2021/05/16"` format
    pub date: String,
    /// Highest target price
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub max_target: Option<Decimal>,
    /// Lowest target price
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub min_target: Option<Decimal>,
    /// Whether the stock price reached the target
    pub meet: bool,
    /// Actual stock price at this date
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub price: Option<Decimal>,
    /// Unix timestamp string
    pub timestamp: String,
}

// ── forecast_eps ──────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::forecast_eps`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForecastEps {
    /// EPS forecast snapshots ordered by `forecast_start_date` ascending
    pub items: Vec<ForecastEpsItem>,
}

/// One EPS forecast snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForecastEpsItem {
    /// Median EPS estimate
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub forecast_eps_median: Option<Decimal>,
    /// Mean EPS estimate
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub forecast_eps_mean: Option<Decimal>,
    /// Lowest EPS estimate
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub forecast_eps_lowest: Option<Decimal>,
    /// Highest EPS estimate
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub forecast_eps_highest: Option<Decimal>,
    /// Total number of forecasting institutions
    pub institution_total: i32,
    /// Number of institutions that raised their estimate
    pub institution_up: i32,
    /// Number of institutions that lowered their estimate
    pub institution_down: i32,
    /// Forecast window start
    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
    pub forecast_start_date: OffsetDateTime,
    /// Forecast window end
    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
    pub forecast_end_date: OffsetDateTime,
}

// ── consensus ─────────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::consensus`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FinancialConsensus {
    /// Per-period consensus reports
    pub list: Vec<ConsensusReport>,
    /// Index into `list` of the most recently released period
    pub current_index: i32,
    /// Reporting currency, e.g. `"HKD"`
    pub currency: String,
    /// Available period types, e.g. `["qf", "saf", "af"]`
    #[serde(default)]
    pub opt_periods: Vec<String>,
    /// Currently returned period type
    pub current_period: String,
}

/// Consensus report for one fiscal period
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsensusReport {
    /// Fiscal year, e.g. `2025`
    pub fiscal_year: i32,
    /// Fiscal period code, e.g. `"Q4"`
    pub fiscal_period: String,
    /// Human-readable period label, e.g. `"Q4 FY2025"`
    pub period_text: String,
    /// Per-metric consensus details
    pub details: Vec<ConsensusDetail>,
}

/// Consensus estimate for one financial metric
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsensusDetail {
    /// Metric key, e.g. `"revenue"`, `"eps"`
    pub key: String,
    /// Display name
    pub name: String,
    /// Metric description
    pub description: String,
    /// Actual reported value (empty string if not yet released)
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub actual: Option<Decimal>,
    /// Consensus estimate value
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub estimate: Option<Decimal>,
    /// Actual minus estimate
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub comp_value: Option<Decimal>,
    /// Beat/miss description, e.g. `"超出预期"`
    pub comp_desc: String,
    /// Comparison result code for colour coding
    pub comp: String,
    /// Whether the actual results have been published
    pub is_released: bool,
}

// ── valuation ─────────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::valuation`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationData {
    /// Valuation metrics (PE / PB / PS / dividend yield)
    pub metrics: ValuationMetricsData,
}

/// Container for all valuation metrics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationMetricsData {
    /// Price-to-Earnings ratio history
    pub pe: Option<ValuationMetricData>,
    /// Price-to-Book ratio history
    pub pb: Option<ValuationMetricData>,
    /// Price-to-Sales ratio history
    pub ps: Option<ValuationMetricData>,
    /// Dividend yield history
    pub dvd_yld: Option<ValuationMetricData>,
}

/// Historical time-series for one valuation metric
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationMetricData {
    /// Human-readable description with current value and percentile
    pub desc: String,
    /// Historical high value
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub high: Option<Decimal>,
    /// Historical low value
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub low: Option<Decimal>,
    /// Historical median value
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub median: Option<Decimal>,
    /// Historical data points
    pub list: Vec<ValuationPoint>,
}

/// One valuation data point
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationPoint {
    /// Date of the data point
    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
    pub timestamp: OffsetDateTime,
    /// Metric value
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub value: Option<Decimal>,
}

// ── valuation_history ─────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::valuation_history`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationHistoryResponse {
    /// Historical valuation data
    pub history: ValuationHistoryData,
}

/// Container for historical valuation metrics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationHistoryData {
    /// Historical metrics (PE / PB / PS)
    pub metrics: ValuationHistoryMetrics,
}

/// Historical valuation metrics container
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationHistoryMetrics {
    /// Price-to-Earnings history
    pub pe: Option<ValuationHistoryMetric>,
    /// Price-to-Book history
    pub pb: Option<ValuationHistoryMetric>,
    /// Price-to-Sales history
    pub ps: Option<ValuationHistoryMetric>,
}

/// Historical data for one valuation metric including statistical bounds
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationHistoryMetric {
    /// Human-readable description
    pub desc: String,
    /// Historical high over the period
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub high: Option<Decimal>,
    /// Historical low over the period
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub low: Option<Decimal>,
    /// Historical median over the period
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub median: Option<Decimal>,
    /// Historical data points
    pub list: Vec<ValuationPoint>,
}

// ── industry_valuation ────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::industry_valuation`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryValuationList {
    /// List of peer securities with their valuation data
    pub list: Vec<IndustryValuationItem>,
}

/// Valuation data for one peer security
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryValuationItem {
    /// Security symbol, e.g. `"700.HK"`
    #[serde(
        rename = "counter_id",
        deserialize_with = "deserialize_counter_id_as_symbol"
    )]
    pub symbol: String,
    /// Company name
    pub name: String,
    /// Reporting currency
    pub currency: String,
    /// Total assets
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub assets: Option<Decimal>,
    /// Book value per share
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub bps: Option<Decimal>,
    /// Earnings per share
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub eps: Option<Decimal>,
    /// Dividends per share
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub dps: Option<Decimal>,
    /// Dividend yield
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub div_yld: Option<Decimal>,
    /// Dividend payout ratio
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub div_payout_ratio: Option<Decimal>,
    /// 5-year average dividends per share
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub five_y_avg_dps: Option<Decimal>,
    /// Current PE ratio
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub pe: Option<Decimal>,
    /// Historical PE/PB/PS snapshots
    pub history: Vec<IndustryValuationHistory>,
}

/// Historical valuation snapshot for an industry peer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryValuationHistory {
    /// Unix timestamp string
    pub date: String,
    /// Price-to-Earnings ratio
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub pe: Option<Decimal>,
    /// Price-to-Book ratio
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub pb: Option<Decimal>,
    /// Price-to-Sales ratio
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub ps: Option<Decimal>,
}

// ── industry_valuation_dist ───────────────────────────────────────

/// Response for [`crate::FundamentalContext::industry_valuation_dist`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryValuationDist {
    /// PE ratio distribution within the industry
    pub pe: Option<ValuationDist>,
    /// PB ratio distribution within the industry
    pub pb: Option<ValuationDist>,
    /// PS ratio distribution within the industry
    pub ps: Option<ValuationDist>,
}

/// Distribution statistics for one valuation metric within an industry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationDist {
    /// Minimum value in the industry
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub low: Option<Decimal>,
    /// Maximum value in the industry
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub high: Option<Decimal>,
    /// Median value in the industry
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub median: Option<Decimal>,
    /// Current value of the queried security
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub value: Option<Decimal>,
    /// Percentile ranking (0–1 range as string)
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub ranking: Option<Decimal>,
    /// Ordinal rank index (1-based)
    pub rank_index: String,
    /// Total number of securities in the industry
    pub rank_total: String,
}

// ── company ───────────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::company`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompanyOverview {
    /// Short name, e.g. `"腾讯控股"`
    pub name: String,
    /// Full legal name
    pub company_name: String,
    /// Founding date
    pub founded: String,
    /// Listing date
    pub listing_date: String,
    /// Primary listing market display name
    pub market: String,
    /// Market region code, e.g. `"HK"`
    pub region: String,
    /// Registered address
    pub address: String,
    /// Principal office address
    pub office_address: String,
    /// Company website
    pub website: String,
    /// IPO issue price
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub issue_price: Option<Decimal>,
    /// Number of shares offered at IPO
    pub shares_offered: String,
    /// Chairman name
    pub chairman: String,
    /// Company secretary name
    pub secretary: String,
    /// Auditing institution
    pub audit_inst: String,
    /// Company classification category
    pub category: String,
    /// Fiscal year end, e.g. `"12 月 31 日"`
    pub year_end: String,
    /// Number of employees
    pub employees: String,
    /// Phone number (API field name is `"Phone"`)
    #[serde(rename = "Phone")]
    pub phone: String,
    /// Fax number
    pub fax: String,
    /// Investor relations email
    pub email: String,
    /// Legal representative
    pub legal_repr: String,
    /// CEO / Managing Director
    pub manager: String,
    /// Business licence number
    pub bus_license: String,
    /// Accounting firm
    pub accounting_firm: String,
    /// Securities representative
    pub securities_rep: String,
    /// Legal counsel
    pub legal_counsel: String,
    /// Postal code
    pub zip_code: String,
    /// Exchange ticker code, e.g. `"00700"`
    pub ticker: String,
    /// URL to the company's logo icon
    pub icon: String,
    /// Business profile / description
    pub profile: String,
    /// ADS ratio (may be empty)
    #[serde(default)]
    pub ads_ratio: String,
    /// Industry sector code
    pub sector: i32,
}

// ── executive ─────────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::executive`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutiveList {
    /// Groups of executives per security (usually one group)
    pub professional_list: Vec<ExecutiveGroup>,
}

/// Executives for one security
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutiveGroup {
    /// Security symbol
    #[serde(
        rename = "counter_id",
        deserialize_with = "deserialize_counter_id_as_symbol"
    )]
    pub symbol: String,
    /// Link to the company wiki page
    pub forward_url: String,
    /// Total number of executives
    pub total: i32,
    /// Individual executive entries
    pub professionals: Vec<Professional>,
}

/// One executive / board member
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Professional {
    /// Internal wiki person ID (string form)
    pub id: String,
    /// Full name
    pub name: String,
    /// Full name in Simplified Chinese
    pub name_zhcn: String,
    /// Full name in English
    pub name_en: String,
    /// Job title, e.g. `"Co-Founder, Chairman & CEO"`
    pub title: String,
    /// Biography text
    pub biography: String,
    /// URL to the person's photo
    pub photo: String,
    /// URL to the wiki profile page
    pub wiki_url: String,
}

// ── shareholder ───────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::shareholder`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareholderList {
    /// List of major shareholders
    pub shareholder_list: Vec<Shareholder>,
    /// Link to the full shareholder page
    #[serde(default)]
    pub forward_url: String,
    /// Total number of shareholders returned
    pub total: i32,
}

/// One major shareholder
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Shareholder {
    /// Internal shareholder ID (string form)
    pub shareholder_id: String,
    /// Shareholder name
    pub shareholder_name: String,
    /// Institution type (may be empty)
    pub institution_type: String,
    /// Percentage of shares held
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub percent_of_shares: Option<Decimal>,
    /// Change in shares held (positive = bought, negative = sold)
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub shares_changed: Option<Decimal>,
    /// Date of the most recent filing, e.g. `"2026-05-04"`
    pub report_date: String,
    /// Other securities held by this shareholder (cross-holdings)
    #[serde(default)]
    pub stocks: Vec<ShareholderStock>,
}

/// A security in an institutional shareholder's cross-holdings
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareholderStock {
    /// Security symbol of the cross-held stock
    #[serde(
        rename = "counter_id",
        deserialize_with = "deserialize_counter_id_as_symbol"
    )]
    pub symbol: String,
    /// Ticker code, e.g. `"BLK"`
    pub code: String,
    /// Market, e.g. `"US"`
    pub market: String,
    /// Day change percentage, e.g. `"-0.32%"`
    pub chg: String,
}

// ── fund_holder ───────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::fund_holder`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundHolders {
    /// Funds and ETFs that hold the queried security
    pub lists: Vec<FundHolder>,
}

/// A fund or ETF that holds the queried security
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundHolder {
    /// Fund/ETF ticker code, e.g. `"513050"`
    pub code: String,
    /// Fund/ETF symbol, e.g. `"ETF/SH/513050"` → converted to `"513050.SH"`
    #[serde(
        rename = "counter_id",
        deserialize_with = "deserialize_counter_id_as_symbol"
    )]
    pub symbol: String,
    /// Reporting currency, e.g. `"CNY"`
    pub currency: String,
    /// Fund/ETF full name
    pub name: String,
    /// Position ratio as a percentage decimal
    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
    pub position_ratio: Decimal,
    /// Report date, e.g. `"2025.12.31"`
    pub report_date: String,
}

// ── corp_action ───────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::corp_action`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorpActions {
    /// Corporate action events
    pub items: Vec<CorpActionItem>,
}

/// One corporate action event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorpActionItem {
    /// Internal event ID
    pub id: String,
    /// Date in `YYYYMMDD` format, e.g. `"20260601"`
    pub date: String,
    /// Short display date, e.g. `"06.01"`
    pub date_str: String,
    /// Date type label, e.g. `"派息日"`, `"除权日"`
    pub date_type: String,
    /// Time zone description, e.g. `"北京时间"`
    pub date_zone: String,
    /// Event category, e.g. `"分配方案"`
    pub act_type: String,
    /// Human-readable event description
    pub act_desc: String,
    /// Machine-readable action code, e.g. `"DividendExDate"`
    pub action: String,
    /// Whether this is a recent event
    pub recent: bool,
    /// Whether publication was delayed
    pub is_delay: bool,
    /// Delay announcement content (if `is_delay` is `true`)
    pub delay_content: String,
    /// Associated live stream (if any)
    pub live: Option<CorpActionLive>,
    /// Associated security info (rarely populated; preserved as raw JSON)
    pub security: Option<serde_json::Value>,
}

/// Live stream associated with a corporate action
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorpActionLive {
    /// Live stream ID
    pub id: String,
    /// Status code: 1=preview, 2=live, 3=ended, 4=replay, 5=processing
    pub status: serde_json::Value, // API may return int or string
    /// Start time
    pub started_at: String,
    /// Stream title
    pub name: String,
    /// Icon URL
    pub icon: String,
}

// ── invest_relation ───────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::invest_relation`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvestRelations {
    /// Link to the full investor-relations page
    #[serde(default)]
    pub forward_url: String,
    /// Securities in which the queried company holds a stake
    pub invest_securities: Vec<InvestSecurity>,
}

/// A security in which the queried company has an investment stake
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvestSecurity {
    /// Internal company ID (string form; may be `"0"`)
    pub company_id: String,
    /// Company name (locale-aware)
    pub company_name: String,
    /// Company name in English
    pub company_name_en: String,
    /// Company name in Simplified Chinese
    pub company_name_zhcn: String,
    /// Security symbol of the invested company
    #[serde(
        rename = "counter_id",
        deserialize_with = "deserialize_counter_id_as_symbol"
    )]
    pub symbol: String,
    /// Reporting currency
    pub currency: String,
    /// Percentage of shares held
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub percent_of_shares: Option<Decimal>,
    /// Shareholder rank, e.g. `"1"` = largest shareholder
    pub shares_rank: String,
    /// Market value of the holding
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub shares_value: Option<Decimal>,
}

// ── operating ─────────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::operating`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperatingList {
    /// List of operating summary reports
    pub list: Vec<OperatingItem>,
}

/// One operating summary report (annual / quarterly)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperatingItem {
    /// Internal report ID
    pub id: String,
    /// Report period code, e.g. `"af"` (annual), `"qf"` (quarterly)
    pub report: String,
    /// Report title, e.g. `"2025 财年年报"`
    pub title: String,
    /// Management discussion text
    pub txt: String,
    /// Whether this is the most recent report
    pub latest: bool,
    /// Keyword tags (structure undocumented; usually empty)
    #[serde(default)]
    pub keywords: Vec<serde_json::Value>,
    /// URL to the full community report page
    #[serde(default)]
    pub web_url: String,
    /// Key financial metrics extracted from the report
    pub financial: OperatingFinancial,
}

/// Key financial metrics extracted from an operating report
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperatingFinancial {
    /// Ticker code (may be empty)
    pub code: String,
    /// Raw counter ID (may be empty)
    pub counter_id: String,
    /// Reporting currency
    pub currency: String,
    /// Company name
    pub name: String,
    /// Market region
    pub region: String,
    /// Report period code
    pub report: String,
    /// Report period display text
    pub report_txt: String,
    /// Financial indicators
    pub indicators: Vec<OperatingIndicator>,
}

/// One financial indicator in an operating report
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperatingIndicator {
    /// Field name key, e.g. `"operating_revenue"`
    pub field_name: String,
    /// Display name, e.g. `"营业收入"`
    pub indicator_name: String,
    /// Formatted value, e.g. `"8217 亿"`
    pub indicator_value: String,
    /// Year-over-year change
    #[serde(default, with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub yoy: Option<Decimal>,
}

// ── buyback ───────────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::buyback`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuybackData {
    /// Most recent buyback summary (TTM)
    #[serde(default)]
    pub recent_buybacks: Option<RecentBuybacks>,
    /// Historical annual buyback data
    #[serde(default)]
    pub buyback_history: Vec<BuybackHistoryItem>,
    /// Buyback payout and cash-flow ratios
    #[serde(default)]
    pub buyback_ratios: Vec<BuybackRatios>,
}

/// TTM (trailing twelve months) buyback summary
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecentBuybacks {
    /// Reporting currency
    pub currency: String,
    /// Net buyback amount TTM
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub net_buyback_ttm: Option<Decimal>,
    /// Net buyback yield TTM
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub net_buyback_yield_ttm: Option<Decimal>,
}

/// Historical annual buyback data point
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuybackHistoryItem {
    /// Fiscal year label, e.g. `"FY2024"`
    pub fiscal_year: String,
    /// Fiscal year date range string
    pub fiscal_year_range: String,
    /// Net buyback amount
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub net_buyback: Option<Decimal>,
    /// Net buyback yield
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub net_buyback_yield: Option<Decimal>,
    /// Year-over-year net buyback growth rate
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub net_buyback_growth_rate: Option<Decimal>,
    /// Reporting currency
    pub currency: String,
}

/// Buyback payout and cash-flow ratios
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuybackRatios {
    /// Net buyback payout ratio
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub net_buyback_payout_ratio: Option<Decimal>,
    /// Net buyback to free cash-flow ratio
    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
    pub net_buyback_to_cashflow_ratio: Option<Decimal>,
}

// ── ratings ───────────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::ratings`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StockRatings {
    /// Style display name
    #[serde(default)]
    pub style_txt_name: String,
    /// Scale display name
    #[serde(default)]
    pub scale_txt_name: String,
    /// Report period display text
    #[serde(default)]
    pub report_period_txt: String,
    /// Composite score (may be int, float, or null)
    #[serde(default)]
    pub multi_score: serde_json::Value,
    /// Composite score letter grade
    #[serde(default)]
    pub multi_letter: String,
    /// Score change vs previous period
    #[serde(default)]
    pub multi_score_change: i32,
    /// Industry name
    #[serde(default)]
    pub industry_name: String,
    /// Industry rank (may be int or null)
    #[serde(default)]
    pub industry_rank: serde_json::Value,
    /// Total securities in the industry
    #[serde(default)]
    pub industry_total: serde_json::Value,
    /// Industry mean score
    #[serde(default)]
    pub industry_mean_score: serde_json::Value,
    /// Industry median score
    #[serde(default)]
    pub industry_median_score: serde_json::Value,
    /// Detailed rating categories
    #[serde(default)]
    pub ratings: Vec<RatingCategory>,
}

/// One rating category (e.g. growth, profitability)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatingCategory {
    /// Category type code
    #[serde(rename = "type")]
    pub kind: i32,
    /// Sub-indicator groups within this category
    #[serde(default)]
    pub sub_indicators: Vec<RatingSubIndicatorGroup>,
}

/// A group of sub-indicators under one category indicator
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatingSubIndicatorGroup {
    /// Parent indicator for this group
    pub indicator: RatingIndicator,
    /// Leaf sub-indicators
    #[serde(default)]
    pub sub_indicators: Vec<RatingLeafIndicator>,
}

/// A rating indicator node (may be a parent or a leaf)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatingIndicator {
    /// Indicator display name
    pub name: String,
    /// Score (may be int, float, or null)
    #[serde(default)]
    pub score: serde_json::Value,
    /// Letter grade
    #[serde(default)]
    pub letter: String,
}

/// A leaf rating indicator with a raw value
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatingLeafIndicator {
    /// Indicator display name
    pub name: String,
    /// Formatted value string
    #[serde(default)]
    pub value: String,
    /// Value type hint, e.g. `"percent"`
    #[serde(default)]
    pub value_type: String,
    /// Score (may be int, float, or null)
    #[serde(default)]
    pub score: serde_json::Value,
    /// Letter grade
    #[serde(default)]
    pub letter: String,
}

// ── enums ─────────────────────────────────────────────────────────

/// Institutional analyst recommendation
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum InstitutionRecommend {
    /// Unknown
    Unknown,
    /// Strong buy
    #[strum(serialize = "strong_buy")]
    StrongBuy,
    /// Buy
    #[strum(serialize = "buy")]
    Buy,
    /// Hold
    #[strum(serialize = "hold")]
    Hold,
    /// Sell
    #[strum(serialize = "sell")]
    Sell,
    /// Strong sell
    #[strum(serialize = "strong_sell")]
    StrongSell,
    /// Underperform
    #[strum(serialize = "underperform")]
    Underperform,
    /// No opinion
    #[strum(serialize = "no_opinion")]
    NoOpinion,
}

impl_default_for_enum_string!(InstitutionRecommend);
impl_serde_for_enum_string!(InstitutionRecommend);

/// Financial report kind
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
pub enum FinancialReportKind {
    /// Income statement
    #[serde(rename = "IS")]
    IncomeStatement,
    /// Balance sheet
    #[serde(rename = "BS")]
    BalanceSheet,
    /// Cash flow statement
    #[serde(rename = "CF")]
    CashFlow,
    /// All statements
    #[default]
    #[serde(rename = "ALL")]
    All,
}

// ── business_segments ─────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::business_segments`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BusinessSegments {
    /// Report date
    pub date: String,
    /// Total revenue
    pub total: String,
    /// Reporting currency
    pub currency: String,
    /// Business segment breakdown
    #[serde(default)]
    pub business: Vec<BusinessSegmentItem>,
}

/// One business segment item (latest snapshot)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BusinessSegmentItem {
    /// Segment name
    pub name: String,
    /// Percentage of total revenue
    pub percent: String,
}

/// Response for [`crate::FundamentalContext::business_segments_history`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BusinessSegmentsHistory {
    /// Historical snapshots
    #[serde(default)]
    pub historical: Vec<BusinessSegmentsHistoricalItem>,
}

/// One historical business segments snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BusinessSegmentsHistoricalItem {
    /// Report date
    pub date: String,
    /// Total revenue
    pub total: String,
    /// Reporting currency
    pub currency: String,
    /// Business segment breakdown
    #[serde(default)]
    pub business: Vec<BusinessSegmentHistoryItem>,
    /// Regional breakdown
    #[serde(default)]
    pub regionals: Vec<BusinessSegmentHistoryItem>,
}

/// One business/regional segment item in a historical snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BusinessSegmentHistoryItem {
    /// Segment name
    pub name: String,
    /// Percentage of total
    pub percent: String,
    /// Absolute value
    pub value: String,
}

// ── institution_rating_views ──────────────────────────────────────

/// Response for [`crate::FundamentalContext::institution_rating_views`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionRatingViews {
    /// Historical rating distribution snapshots
    #[serde(default)]
    pub elist: Vec<InstitutionRatingViewItem>,
}

/// One historical rating distribution snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionRatingViewItem {
    /// Date as unix timestamp string (API returns as quoted or bare integer)
    pub date: String,
    /// Number of "Buy" ratings (API returns as string)
    pub buy: String,
    /// Number of "Outperform" ratings (API returns as string)
    pub over: String,
    /// Number of "Hold" ratings (API returns as string)
    pub hold: String,
    /// Number of "Underperform" ratings (API returns as string)
    pub under: String,
    /// Number of "Sell" ratings (API returns as string)
    pub sell: String,
    /// Total analyst count (API returns as string)
    pub total: String,
}

// ── industry_rank ─────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::industry_rank`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryRankResponse {
    /// Grouped rank items
    #[serde(default)]
    pub items: Vec<IndustryRankGroup>,
}

/// A group of ranked industry items
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryRankGroup {
    /// Items in this group
    #[serde(default)]
    pub lists: Vec<IndustryRankItem>,
}

/// One ranked industry item
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryRankItem {
    /// Industry / sector name
    pub name: String,
    /// Counter ID of the industry
    pub counter_id: String,
    /// Change percentage
    pub chg: String,
    /// Name of the leading stock
    pub leading_name: String,
    /// Ticker of the leading stock
    pub leading_ticker: String,
    /// Change percentage of the leading stock
    pub leading_chg: String,
    /// Value label name
    pub value_name: String,
    /// Value data
    pub value_data: String,
}

// ── industry_peers ────────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::industry_peers`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryPeersResponse {
    /// Top-level industry node info
    pub top: IndustryPeersTop,
    /// Root peer chain node (may be absent if no data)
    pub chain: Option<IndustryPeerNode>,
}

/// Top-level industry info in the peers response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryPeersTop {
    /// Industry name
    pub name: String,
    /// Market code
    pub market: String,
}

/// A node in the recursive industry peer chain
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryPeerNode {
    /// Node name
    pub name: String,
    /// Counter ID
    pub counter_id: String,
    /// Number of stocks in this node (API returns as integer)
    pub stock_num: i32,
    /// Change percentage
    pub chg: String,
    /// Year-to-date change
    pub ytd_chg: String,
    /// Child nodes (recursive)
    #[serde(default)]
    pub next: Vec<IndustryPeerNode>,
}

// ── financial_report_snapshot ─────────────────────────────────────

/// Response for [`crate::FundamentalContext::financial_report_snapshot`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FinancialReportSnapshot {
    /// Company name
    pub name: String,
    /// Ticker code
    pub ticker: String,
    /// Fiscal period start date
    pub fp_start: String,
    /// Fiscal period end date
    pub fp_end: String,
    /// Reporting currency
    pub currency: String,
    /// Report description
    pub report_desc: String,
    /// Forecast revenue
    pub fo_revenue: Option<SnapshotForecastMetric>,
    /// Forecast EBIT
    pub fo_ebit: Option<SnapshotForecastMetric>,
    /// Forecast EPS
    pub fo_eps: Option<SnapshotForecastMetric>,
    /// Reported revenue
    pub fr_revenue: Option<SnapshotReportedMetric>,
    /// Reported net profit
    pub fr_profit: Option<SnapshotReportedMetric>,
    /// Reported operating cash flow
    pub fr_operate_cash: Option<SnapshotReportedMetric>,
    /// Reported investing cash flow
    pub fr_invest_cash: Option<SnapshotReportedMetric>,
    /// Reported financing cash flow
    pub fr_finance_cash: Option<SnapshotReportedMetric>,
    /// Reported total assets
    pub fr_total_assets: Option<SnapshotReportedMetric>,
    /// Reported total liabilities
    pub fr_total_liability: Option<SnapshotReportedMetric>,
    /// ROE TTM
    pub fr_roe_ttm: String,
    /// Profit margin
    pub fr_profit_margin: String,
    /// Profit margin TTM
    pub fr_profit_margin_ttm: String,
    /// Asset turnover TTM
    pub fr_asset_turn_ttm: String,
    /// Leverage TTM
    pub fr_leverage_ttm: String,
    /// Debt-to-assets ratio
    pub fr_debt_assets_ratio: String,
}

/// A forecast metric in the financial report snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotForecastMetric {
    /// Actual value
    pub value: String,
    /// Year-over-year change
    pub yoy: String,
    /// Beat/miss description
    pub cmp_desc: String,
    /// Consensus estimate value
    pub est_value: String,
}

/// A reported metric in the financial report snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotReportedMetric {
    /// Actual value
    pub value: String,
    /// Year-over-year change
    pub yoy: String,
}

// ── shareholder_top ───────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::shareholder_top`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareholderTopResponse {
    /// Raw top-shareholder data
    pub data: serde_json::Value,
}

// ── shareholder_detail ────────────────────────────────────────────

/// Response for [`crate::FundamentalContext::shareholder_detail`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareholderDetailResponse {
    /// Raw shareholder detail data
    pub data: serde_json::Value,
}

// ── valuation_comparison ──────────────────────────────────────────

/// One historical valuation data point.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationHistoryPoint {
    /// Date (RFC 3339, converted from Unix timestamp)
    pub date: String,
    /// P/E ratio
    pub pe: String,
    /// P/B ratio
    pub pb: String,
    /// P/S ratio
    pub ps: String,
}

/// One security's valuation comparison item.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationComparisonItem {
    /// Symbol (converted from counter_id)
    pub symbol: String,
    /// Security name
    pub name: String,
    /// Currency
    pub currency: String,
    /// Market capitalisation
    pub market_value: String,
    /// Latest closing price
    pub price_close: String,
    /// P/E ratio
    pub pe: String,
    /// P/B ratio
    pub pb: String,
    /// P/S ratio
    pub ps: String,
    /// Return on equity
    pub roe: String,
    /// Earnings per share
    pub eps: String,
    /// Book value per share
    pub bps: String,
    /// Dividends per share
    pub dps: String,
    /// Dividend yield
    pub div_yld: String,
    /// Total assets
    pub assets: String,
    /// Historical valuation points
    pub history: Vec<ValuationHistoryPoint>,
}

/// Response for [`crate::FundamentalContext::valuation_comparison`]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationComparisonResponse {
    /// Valuation comparison items
    pub list: Vec<ValuationComparisonItem>,
}

/// Financial report period type
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum FinancialReportPeriod {
    /// Annual report
    #[serde(rename = "af")]
    Annual,
    /// Semi-annual report
    #[serde(rename = "saf")]
    SemiAnnual,
    /// Q1 report
    #[serde(rename = "q1")]
    Q1,
    /// Q2 report
    #[serde(rename = "q2")]
    Q2,
    /// Q3 report
    #[serde(rename = "q3")]
    Q3,
    /// Full quarterly report
    #[serde(rename = "qf")]
    QuarterlyFull,
    /// Three-quarter report (first three quarters)
    #[serde(rename = "3q")]
    ThreeQ,
}