Skip to main content

longbridge/market/
types.rs

1use rust_decimal::Decimal;
2use serde::{Deserialize, Serialize};
3use serde_repr::{Deserialize_repr, Serialize_repr};
4use strum_macros::{FromRepr, IntoStaticStr};
5use time::OffsetDateTime;
6
7use crate::types::Market;
8
9// ── market_status ─────────────────────────────────────────────────
10
11/// Market trading status code.
12#[allow(non_camel_case_types)]
13#[derive(
14    Debug,
15    Clone,
16    Copy,
17    Default,
18    Hash,
19    PartialOrd,
20    Ord,
21    PartialEq,
22    Eq,
23    FromRepr,
24    IntoStaticStr,
25    Serialize_repr,
26    Deserialize_repr,
27)]
28#[repr(i32)]
29pub enum TradeStatus {
30    /// Unknown status
31    #[default]
32    #[serde(other)]
33    UNKNOWN = -1,
34    /// Quote is not registered
35    NO_REGISTER_QUOTE = 0,
36    /// Clearing before the market opens.
37    CLEAN = 101,
38    /// Opening auction.
39    OPEN_BID = 102,
40    /// Morning break, currently used by VIX indexes.
41    MORNING_CLOSING = 103,
42    /// Regular trading.
43    TRADING = 105,
44    /// Midday break.
45    NOON_CLOSING = 106,
46    /// Closing auction.
47    CLOSE_BID = 107,
48    /// Market closed.
49    CLOSING = 108,
50    /// Dark trading waiting to open.
51    DARK_WAIT = 110,
52    /// Dark trading.
53    DARK_TRADING = 111,
54    /// Dark trading closed.
55    DARK_CLOSING = 112,
56    /// After-hours fixed-price trading.
57    AFTER_FIX = 120,
58    /// Half-day market closed. Defined by the market status table but currently
59    /// unused.
60    HALF_CLOSING = 121,
61    /// Not opened because the exchange is waiting to open under special
62    /// conditions.
63    NOT_OPENED = 122,
64    /// Temporary intraday break. The historical variant name is kept for
65    /// compatibility.
66    REALTIME_QUOTE = 123,
67    /// US pre-market.
68    US_PREV = 201,
69    /// US regular trading.
70    US_TRADING = 202,
71    /// US post-market.
72    US_AFTER = 203,
73    /// US closed.
74    US_CLOSING = 204,
75    /// US halted.
76    US_STOP = 205,
77    /// US clearing plus pre-market.
78    US_CLEAN = 206,
79    /// US overnight trading.
80    US_NIGHT = 207,
81    /// US pre-market clearing alias returned by the quote engine.
82    US_PREV_MARKET_CLEAN = 209,
83    /// US post-market clearing alias returned by the quote engine.
84    US_AFTER_MARKET_CLEAN = 210,
85    /// Stock refresh. Deprecated in the status definition.
86    REFRESH = 1000,
87    /// Delisted.
88    DELIST = 1001,
89    /// Preparing to list.
90    PREPARE = 1002,
91    /// Code changed.
92    CODE_CHANGE = 1003,
93    /// Halted.
94    STOP = 1004,
95    /// Waiting to open, typically for a US IPO auction.
96    WILL_OPEN = 1005,
97    /// Split or merge suspended.
98    COMMON_SUSPEND = 1006,
99    /// Expired.
100    EXPIRE = 1007,
101    /// No quote data.
102    NO_QUOTE = 1008,
103    /// Not listed. The historical variant name is kept for compatibility.
104    UNITED = 1009,
105    /// Terminated trading, usually for warrants.
106    TRADING_HALT = 1010,
107    /// Waiting to list, usually for new warrants.
108    WAIT_LISTING = 1011,
109    /// Fuse.
110    FUSE = 2001,
111}
112
113impl From<i32> for TradeStatus {
114    fn from(value: i32) -> Self {
115        Self::from_repr(value).unwrap_or_default()
116    }
117}
118
119impl TradeStatus {
120    /// Converts an isize value to a market trading status.
121    pub fn from_isize(value: isize) -> TradeStatus {
122        (value as i32).into()
123    }
124
125    /// Returns the raw numeric status code.
126    pub fn code(self) -> i32 {
127        self as i32
128    }
129
130    /// Returns the static enum variant name.
131    pub fn as_static(self) -> &'static str {
132        self.into()
133    }
134
135    /// Returns a simplified label for key display states.
136    pub fn label(self) -> &'static str {
137        let status = self.normalize();
138        match status {
139            TradeStatus::US_PREV
140            | TradeStatus::US_TRADING
141            | TradeStatus::US_AFTER
142            | TradeStatus::US_NIGHT
143            | TradeStatus::US_CLOSING
144            | TradeStatus::TRADING
145            | TradeStatus::CLOSING => status.name(),
146            _ => "",
147        }
148    }
149
150    /// Returns the full English status name.
151    pub fn name(self) -> &'static str {
152        match self.normalize() {
153            TradeStatus::UNKNOWN | TradeStatus::NO_REGISTER_QUOTE => "Unknown",
154            TradeStatus::OPEN_BID => "Open Bid",
155            TradeStatus::MORNING_CLOSING => "Morning Break",
156            TradeStatus::TRADING | TradeStatus::US_TRADING | TradeStatus::US_AFTER_MARKET_CLEAN => {
157                "Trading"
158            }
159            TradeStatus::NOON_CLOSING => "Mid-Day Break",
160            TradeStatus::CLOSE_BID => "Close Bid",
161            TradeStatus::CLOSING
162            | TradeStatus::CLEAN
163            | TradeStatus::HALF_CLOSING
164            | TradeStatus::US_CLOSING
165            | TradeStatus::US_PREV_MARKET_CLEAN => "Closed",
166            TradeStatus::DARK_WAIT => "Dark Wait",
167            TradeStatus::DARK_TRADING => "Dark Trading",
168            TradeStatus::DARK_CLOSING => "Closing",
169            TradeStatus::AFTER_FIX => "After Fix",
170            TradeStatus::NOT_OPENED => "Not Open",
171            TradeStatus::REALTIME_QUOTE => "Temporary Break",
172            TradeStatus::US_PREV | TradeStatus::US_CLEAN => "Pre-Market",
173            TradeStatus::US_AFTER => "Post-Market",
174            TradeStatus::US_STOP | TradeStatus::STOP => "Stop",
175            TradeStatus::US_NIGHT => "Overnight",
176            TradeStatus::REFRESH => "Refresh",
177            TradeStatus::DELIST => "Delist",
178            TradeStatus::PREPARE => "Prepare",
179            TradeStatus::CODE_CHANGE => "Code Change",
180            TradeStatus::WILL_OPEN => "Will Open",
181            TradeStatus::COMMON_SUSPEND => "Common Suspend",
182            TradeStatus::EXPIRE => "Expire",
183            TradeStatus::NO_QUOTE => "No Quote",
184            TradeStatus::UNITED => "Not Listed",
185            TradeStatus::TRADING_HALT => "Terminated",
186            TradeStatus::WAIT_LISTING => "Wait Listing",
187            TradeStatus::FUSE => "Fuse",
188        }
189    }
190
191    /// Returns whether this is a US market status.
192    pub fn is_us_market(self) -> bool {
193        self.code() >= 200 && self.code() < 300
194    }
195
196    /// Returns whether this is a US pre/post-market status.
197    pub fn is_us_pre_post(self) -> bool {
198        self.is_us_prev() || self.is_us_after()
199    }
200
201    /// Returns whether this is a US overnight status.
202    pub fn is_us_night(self) -> bool {
203        matches!(self, TradeStatus::US_NIGHT)
204    }
205
206    /// Returns whether this is a US closed status.
207    pub fn is_us_closing(self) -> bool {
208        matches!(
209            self,
210            TradeStatus::US_CLOSING | TradeStatus::US_PREV_MARKET_CLEAN
211        )
212    }
213
214    /// Returns whether this is a closed status.
215    pub fn is_closing(self) -> bool {
216        matches!(
217            self,
218            TradeStatus::US_CLOSING
219                | TradeStatus::US_PREV_MARKET_CLEAN
220                | TradeStatus::CLOSING
221                | TradeStatus::HALF_CLOSING
222        )
223    }
224
225    /// Returns whether this is a US pre-market status.
226    pub fn is_us_prev(self) -> bool {
227        matches!(self, TradeStatus::US_PREV | TradeStatus::US_CLEAN)
228    }
229
230    /// Returns whether this is a US post-market status.
231    pub fn is_us_after(self) -> bool {
232        matches!(self, TradeStatus::US_AFTER)
233    }
234
235    /// Returns whether this is a trading status.
236    pub fn is_trading(self) -> bool {
237        matches!(
238            self,
239            TradeStatus::TRADING | TradeStatus::US_TRADING | TradeStatus::US_AFTER_MARKET_CLEAN
240        )
241    }
242
243    /// Returns whether this is a dark-pool status.
244    pub fn is_dark(self) -> bool {
245        matches!(
246            self,
247            TradeStatus::DARK_WAIT | TradeStatus::DARK_TRADING | TradeStatus::DARK_CLOSING
248        )
249    }
250
251    /// Returns whether this status allows trading.
252    pub fn allow_trading(self) -> bool {
253        matches!(
254            self,
255            TradeStatus::OPEN_BID
256                | TradeStatus::TRADING
257                | TradeStatus::CLOSE_BID
258                | TradeStatus::NOT_OPENED
259                | TradeStatus::NOON_CLOSING
260                | TradeStatus::US_TRADING
261                | TradeStatus::US_AFTER_MARKET_CLEAN
262        )
263    }
264
265    /// Normalizes clearing aliases to their display-equivalent status.
266    #[must_use]
267    pub fn normalize(self) -> Self {
268        match self {
269            TradeStatus::CLEAN => TradeStatus::CLOSING,
270            TradeStatus::US_PREV_MARKET_CLEAN => TradeStatus::US_CLOSING,
271            TradeStatus::US_CLEAN => TradeStatus::US_PREV,
272            TradeStatus::US_AFTER_MARKET_CLEAN => TradeStatus::US_TRADING,
273            _ => self,
274        }
275    }
276
277    /// Returns whether this is a special non-regular status.
278    pub fn is_special(self) -> bool {
279        self.code() < 100 || self == Self::US_STOP || self.code() >= 1000
280    }
281}
282
283/// Response for [`crate::MarketContext::market_status`]
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct MarketStatusResponse {
286    /// Per-market trading status items
287    pub market_time: Vec<MarketTimeItem>,
288}
289
290/// Trading status for one market
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct MarketTimeItem {
293    /// Market code
294    pub market: Market,
295    /// Market trade status. See [`TradeStatus`] for the code table.
296    pub trade_status: TradeStatus,
297    /// Current market time (unix timestamp string)
298    pub timestamp: String,
299    /// Delayed-quote market trade status. See [`TradeStatus`] for the code
300    /// table.
301    pub delay_trade_status: TradeStatus,
302    /// Delayed-quote market time (unix timestamp string)
303    pub delay_timestamp: String,
304    /// Sub-status code
305    pub sub_status: i32,
306    /// Delayed-quote sub-status code
307    pub delay_sub_status: i32,
308}
309
310#[cfg(test)]
311mod tests {
312    use crate::market::TradeStatus;
313
314    #[test]
315    fn market_trade_status_deserializes_numeric_codes() {
316        assert_eq!(
317            serde_json::from_str::<TradeStatus>("202")
318                .expect("202 should deserialize as market trade status"),
319            TradeStatus::US_TRADING
320        );
321        assert_eq!(
322            serde_json::from_str::<TradeStatus>("456")
323                .expect("unknown numeric status should deserialize"),
324            TradeStatus::UNKNOWN
325        );
326    }
327
328    #[test]
329    fn market_trade_status_serializes_as_numeric_code() {
330        let value = serde_json::to_string(&TradeStatus::US_CLEAN)
331            .expect("market trade status should serialize");
332        assert_eq!(value, "206");
333    }
334
335    #[test]
336    fn market_trade_status_normalizes_engine_aliases() {
337        assert_eq!(TradeStatus::CLEAN.normalize(), TradeStatus::CLOSING);
338        assert_eq!(TradeStatus::US_CLEAN.normalize(), TradeStatus::US_PREV);
339        assert_eq!(
340            TradeStatus::US_PREV_MARKET_CLEAN.normalize(),
341            TradeStatus::US_CLOSING
342        );
343        assert_eq!(
344            TradeStatus::US_AFTER_MARKET_CLEAN.normalize(),
345            TradeStatus::US_TRADING
346        );
347    }
348
349    #[test]
350    fn market_trade_status_label_matches_engine_simplified_display() {
351        assert_eq!(TradeStatus::US_PREV.label(), "Pre-Market");
352        assert_eq!(TradeStatus::US_CLEAN.label(), "Pre-Market");
353        assert_eq!(TradeStatus::US_AFTER.label(), "Post-Market");
354        assert_eq!(TradeStatus::US_CLOSING.label(), "Closed");
355        assert_eq!(TradeStatus::US_AFTER_MARKET_CLEAN.label(), "Trading");
356        assert_eq!(TradeStatus::US_TRADING.label(), "Trading");
357        assert_eq!(TradeStatus::TRADING.label(), "Trading");
358        assert_eq!(TradeStatus::CLEAN.label(), "Closed");
359        assert_eq!(TradeStatus::OPEN_BID.label(), "");
360        assert_eq!(TradeStatus::NOON_CLOSING.label(), "");
361    }
362
363    #[test]
364    fn market_trade_status_name_covers_full_status_set() {
365        let cases = [
366            (TradeStatus::MORNING_CLOSING, "Morning Break"),
367            (TradeStatus::NOON_CLOSING, "Mid-Day Break"),
368            (TradeStatus::REALTIME_QUOTE, "Temporary Break"),
369            (TradeStatus::US_STOP, "Stop"),
370            (TradeStatus::TRADING_HALT, "Terminated"),
371            (TradeStatus::WAIT_LISTING, "Wait Listing"),
372            (TradeStatus::FUSE, "Fuse"),
373            (TradeStatus::UNKNOWN, "Unknown"),
374            (TradeStatus::NO_REGISTER_QUOTE, "Unknown"),
375        ];
376
377        for (status, expected) in cases {
378            assert_eq!(status.name(), expected, "status {status:?}");
379        }
380    }
381
382    #[test]
383    fn market_trade_status_codes_match_phase_definition_document() {
384        let codes = [
385            101, 102, 103, 105, 106, 107, 108, 110, 111, 112, 120, 121, 122, 123, 201, 202, 203,
386            204, 206, 207, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011,
387            2001,
388        ];
389
390        for code in codes {
391            assert_eq!(TradeStatus::from(code).code(), code, "status code {code}");
392        }
393    }
394
395    #[test]
396    fn market_trade_status_names_match_phase_definition_document() {
397        let cases = [
398            (123, "Temporary Break"),
399            (1009, "Not Listed"),
400            (1010, "Terminated"),
401            (2001, "Fuse"),
402        ];
403
404        for (code, expected) in cases {
405            assert_eq!(
406                TradeStatus::from(code).name(),
407                expected,
408                "status code {code}"
409            );
410        }
411    }
412
413    #[test]
414    fn market_time_item_uses_market_trade_status_type() {
415        let item = serde_json::from_str::<crate::market::MarketTimeItem>(
416            r#"{
417                "market": "US",
418                "trade_status": 202,
419                "timestamp": "1717200000",
420                "delay_trade_status": 204,
421                "delay_timestamp": "1717200000",
422                "sub_status": 0,
423                "delay_sub_status": 0
424            }"#,
425        )
426        .expect("market time item should deserialize");
427
428        assert_eq!(item.trade_status, TradeStatus::US_TRADING);
429        assert_eq!(item.delay_trade_status, TradeStatus::US_CLOSING);
430    }
431}
432
433// ── broker_holding ────────────────────────────────────────────────
434
435/// Response for [`crate::MarketContext::broker_holding`]
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct BrokerHoldingTop {
438    /// Top brokers by net buying
439    pub buy: Vec<BrokerHoldingEntry>,
440    /// Top brokers by net selling
441    pub sell: Vec<BrokerHoldingEntry>,
442    /// Last updated (may be empty)
443    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
444    pub updated_at: String,
445}
446
447/// One broker entry in a top-holding list
448#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct BrokerHoldingEntry {
450    /// Broker name
451    pub name: String,
452    /// Participant number / broker code
453    pub parti_number: String,
454    /// Net change in shares held
455    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
456    pub chg: Option<Decimal>,
457    /// Whether this is a "strengthening" broker
458    pub strong: bool,
459}
460
461// ── broker_holding_detail ─────────────────────────────────────────
462
463/// Response for [`crate::MarketContext::broker_holding_detail`]
464#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct BrokerHoldingDetail {
466    /// Full list of broker holdings
467    pub list: Vec<BrokerHoldingDetailItem>,
468    /// Last updated (may be empty)
469    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
470    pub updated_at: String,
471}
472
473/// One broker's full holding detail
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct BrokerHoldingDetailItem {
476    /// Broker name
477    pub name: String,
478    /// Participant number / broker code
479    pub parti_number: String,
480    /// Holding ratio changes over various periods
481    pub ratio: BrokerHoldingChanges,
482    /// Share count changes over various periods
483    pub shares: BrokerHoldingChanges,
484    /// Whether this is a "strengthening" broker
485    pub strong: bool,
486}
487
488/// Changes in broker holding over 1 / 5 / 20 / 60 day periods
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct BrokerHoldingChanges {
491    /// Current value
492    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
493    pub value: Option<Decimal>,
494    /// 1-day change
495    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
496    pub chg_1: Option<Decimal>,
497    /// 5-day change
498    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
499    pub chg_5: Option<Decimal>,
500    /// 20-day change
501    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
502    pub chg_20: Option<Decimal>,
503    /// 60-day change
504    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
505    pub chg_60: Option<Decimal>,
506}
507
508// ── broker_holding_daily ──────────────────────────────────────────
509
510/// Response for [`crate::MarketContext::broker_holding_daily`]
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct BrokerHoldingDailyHistory {
513    /// Daily broker holding records
514    pub list: Vec<BrokerHoldingDailyItem>,
515}
516
517/// One day's broker holding record
518#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct BrokerHoldingDailyItem {
520    /// Date in `"2026.05.05"` format
521    pub date: String,
522    /// Total shares held
523    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
524    pub holding: Option<Decimal>,
525    /// Holding ratio as a decimal
526    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
527    pub ratio: Option<Decimal>,
528    /// Change vs previous day
529    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
530    pub chg: Option<Decimal>,
531}
532
533// ── ah_premium ────────────────────────────────────────────────────
534
535/// Response for [`crate::MarketContext::ah_premium`]
536#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct AhPremiumKlines {
538    /// K-line data points
539    pub klines: Vec<AhPremiumKline>,
540}
541
542/// Response for [`crate::MarketContext::ah_premium_intraday`]
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct AhPremiumIntraday {
545    /// Intraday data points (field name is `klines` in the API)
546    pub klines: Vec<AhPremiumKline>,
547}
548
549/// One A/H premium data point
550#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct AhPremiumKline {
552    /// A-share price
553    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
554    pub aprice: Decimal,
555    /// A-share previous close
556    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
557    pub apreclose: Decimal,
558    /// H-share price
559    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
560    pub hprice: Decimal,
561    /// H-share previous close
562    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
563    pub hpreclose: Decimal,
564    /// CNY/HKD exchange rate
565    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
566    pub currency_rate: Decimal,
567    /// A/H premium rate (negative = H-share at premium)
568    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
569    pub ahpremium_rate: Decimal,
570    /// Price spread
571    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
572    pub price_spread: Decimal,
573    /// Data point timestamp
574    #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
575    pub timestamp: OffsetDateTime,
576}
577
578// ── trade_stats ───────────────────────────────────────────────────
579
580/// Response for [`crate::MarketContext::trade_stats`]
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct TradeStatsResponse {
583    /// Summary statistics
584    pub statistics: TradeStatistics,
585    /// Per-price-level breakdown
586    pub trades: Vec<TradePriceLevel>,
587}
588
589/// Summary trade statistics
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub struct TradeStatistics {
592    /// Volume-weighted average price
593    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
594    pub avgprice: Decimal,
595    /// Total buy volume (shares)
596    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
597    pub buy: Decimal,
598    /// Total neutral / unknown-direction volume
599    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
600    pub neutral: Decimal,
601    /// Previous close price
602    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
603    pub preclose: Decimal,
604    /// Total sell volume (shares)
605    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
606    pub sell: Decimal,
607    /// Data timestamp (unix timestamp string)
608    pub timestamp: String,
609    /// Total trading volume (shares)
610    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
611    pub total_amount: Decimal,
612    /// Unix timestamps for the last 5 trading days
613    pub trade_date: Vec<String>,
614    /// Total number of trades
615    pub trades_count: String,
616}
617
618/// Trade volume at one price level
619#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct TradePriceLevel {
621    /// Buy volume at this price
622    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
623    pub buy_amount: Decimal,
624    /// Neutral (unknown direction) volume at this price
625    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
626    pub neutral_amount: Decimal,
627    /// Price level
628    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
629    pub price: Decimal,
630    /// Sell volume at this price
631    #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
632    pub sell_amount: Decimal,
633}
634
635// ── anomaly ───────────────────────────────────────────────────────
636
637/// Response for [`crate::MarketContext::anomaly`]
638#[derive(Debug, Clone, Serialize, Deserialize)]
639pub struct AnomalyResponse {
640    /// Whether anomaly alerts are globally disabled
641    pub all_off: bool,
642    /// List of market anomaly events
643    pub changes: Vec<AnomalyItem>,
644}
645
646/// One market anomaly event (e.g. large block trade, margin buying surge)
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct AnomalyItem {
649    /// Security symbol
650    pub symbol: String,
651    /// Security name
652    pub name: String,
653    /// Anomaly type name, e.g. `"大宗交易"`, `"融资买入"`
654    pub alert_name: String,
655    /// Time of the anomaly (unix timestamp in milliseconds)
656    pub alert_time: i64,
657    /// Change values — items are accessed as strings by the client
658    pub change_values: Vec<String>,
659    /// Sentiment direction: 1 = positive/up, 2 = negative/down
660    pub emotion: i32,
661}
662
663// ── constituent ───────────────────────────────────────────────────
664
665/// Response for [`crate::MarketContext::constituent`]
666#[derive(Debug, Clone, Serialize, Deserialize)]
667pub struct IndexConstituents {
668    /// Number of constituent stocks that fell today
669    pub fall_num: i32,
670    /// Number of constituent stocks unchanged today
671    pub flat_num: i32,
672    /// Number of constituent stocks that rose today
673    pub rise_num: i32,
674    /// Constituent stock details
675    pub stocks: Vec<ConstituentStock>,
676}
677
678/// One constituent stock of an index
679#[derive(Debug, Clone, Serialize, Deserialize)]
680pub struct ConstituentStock {
681    /// Security symbol
682    pub symbol: String,
683    /// Security name
684    pub name: String,
685    /// Latest price
686    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
687    pub last_done: Option<Decimal>,
688    /// Previous close
689    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
690    pub prev_close: Option<Decimal>,
691    /// Net capital inflow today
692    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
693    pub inflow: Option<Decimal>,
694    /// Turnover amount
695    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
696    pub balance: Option<Decimal>,
697    /// Trading volume (shares)
698    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
699    pub amount: Option<Decimal>,
700    /// Total shares outstanding
701    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
702    pub total_shares: Option<Decimal>,
703    /// Tags, e.g. `["领涨龙头"]`
704    pub tags: Vec<String>,
705    /// Brief description
706    pub intro: String,
707    /// Market, e.g. `"HK"`
708    pub market: String,
709    /// Circulating shares
710    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
711    pub circulating_shares: Option<Decimal>,
712    /// Whether this is a delayed quote
713    pub delay: bool,
714    /// Day change percentage
715    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
716    pub chg: Option<Decimal>,
717    /// Raw trade status code
718    pub trade_status: i32,
719}
720
721// ── top_movers ────────────────────────────────────────────────────
722
723/// Stock information within a top-movers event.
724#[derive(Debug, Clone, Serialize, Deserialize)]
725pub struct TopMoversStock {
726    /// Symbol, e.g. `"NVDA.US"`
727    pub symbol: String,
728    /// Ticker code (e.g. `"NVDA"`)
729    pub code: String,
730    /// Security name
731    pub name: String,
732    /// Full name
733    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
734    pub full_name: String,
735    /// Price change (decimal ratio)
736    pub change: String,
737    /// Latest price
738    pub last_done: String,
739    /// Market code
740    pub market: String,
741    /// Labels / tags
742    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
743    pub labels: Vec<String>,
744    /// Logo URL
745    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
746    pub logo: String,
747}
748
749/// One top-movers event entry.
750#[derive(Debug, Clone, Serialize, Deserialize)]
751pub struct TopMoversEvent {
752    /// Event time (RFC 3339)
753    pub timestamp: String,
754    /// Alert reason description
755    pub alert_reason: String,
756    /// Alert type code
757    pub alert_type: i64,
758    /// Stock information
759    pub stock: TopMoversStock,
760    /// Associated news post (raw JSON, complex structure)
761    pub post: serde_json::Value,
762}
763
764/// Response for [`crate::MarketContext::top_movers`]
765#[derive(Debug, Clone, Serialize, Deserialize)]
766pub struct TopMoversResponse {
767    /// Top-mover events
768    pub events: Vec<TopMoversEvent>,
769    /// Pagination cursor for next page (pass as-is to the next call; empty
770    /// string means no more pages)
771    pub next_params: String,
772}
773
774// ── rank_categories ───────────────────────────────────────────────
775
776/// A leaf rank sub-category whose `key` can be passed to
777/// [`MarketContext::rank_list`](crate::MarketContext::rank_list).
778#[derive(Debug, Clone, Serialize, Deserialize)]
779pub struct RankSubCategory {
780    /// Sub-category key, e.g. `"hot_all-us"`. Pass directly to `rank_list`.
781    pub key: String,
782    /// Display name, e.g. `"美股总热度"`
783    pub name: String,
784    /// Market code, e.g. `"US"`, `"HK"`, `"CN"`, `"SG"`
785    pub market: String,
786}
787
788/// A top-level rank category grouping one or more sub-categories.
789#[derive(Debug, Clone, Serialize, Deserialize)]
790pub struct RankCategory {
791    /// Top-level key, e.g. `"hot"`
792    pub key: String,
793    /// Display name, e.g. `"热度排行"`
794    pub name: String,
795    /// Sub-categories
796    pub sub_categories: Vec<RankSubCategory>,
797}
798
799/// Response for [`crate::MarketContext::rank_categories`]
800#[derive(Debug, Clone, Serialize, Deserialize)]
801pub struct RankCategoriesResponse {
802    /// All top-level rank categories
803    pub categories: Vec<RankCategory>,
804}
805
806// ── rank_list ─────────────────────────────────────────────────────
807
808/// One ranked security item.
809#[derive(Debug, Clone, Serialize, Deserialize)]
810pub struct RankListItem {
811    /// Symbol, e.g. `"MU.US"`
812    pub symbol: String,
813    /// Ticker code (e.g. `"MU"`)
814    pub code: String,
815    /// Security name
816    pub name: String,
817    /// Latest price
818    pub last_done: String,
819    /// Price change ratio (decimal)
820    pub chg: String,
821    /// Absolute price change
822    pub change: String,
823    /// Net inflow
824    pub inflow: String,
825    /// Market cap
826    pub market_cap: String,
827    /// Industry name
828    pub industry: String,
829    /// Pre/post market price
830    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
831    pub pre_post_price: String,
832    /// Pre/post market change
833    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
834    pub pre_post_chg: String,
835    /// Amplitude
836    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
837    pub amplitude: String,
838    /// 5-day change
839    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
840    pub five_day_chg: String,
841    /// Turnover rate
842    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
843    pub turnover_rate: String,
844    /// Volume ratio
845    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
846    pub volume_rate: String,
847    /// P/B ratio (TTM)
848    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
849    pub pb_ttm: String,
850}
851
852/// Response for [`crate::MarketContext::rank_list`]
853#[derive(Debug, Clone, Serialize, Deserialize)]
854pub struct RankListResponse {
855    /// Whether delayed / BMP data
856    pub bmp: bool,
857    /// Ranked security items
858    pub lists: Vec<RankListItem>,
859}
860
861// ── enums ─────────────────────────────────────────────────────────
862
863/// Broker holding lookback period
864#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
865pub enum BrokerHoldingPeriod {
866    /// 1-day change
867    #[default]
868    #[serde(rename = "rct_1")]
869    Rct1,
870    /// 5-day change
871    #[serde(rename = "rct_5")]
872    Rct5,
873    /// 20-day change
874    #[serde(rename = "rct_20")]
875    Rct20,
876    /// 60-day change
877    #[serde(rename = "rct_60")]
878    Rct60,
879}
880
881/// A/H premium K-line period
882#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
883pub enum AhPremiumPeriod {
884    /// 1-minute
885    Min1,
886    /// 5-minute
887    Min5,
888    /// 15-minute
889    Min15,
890    /// 30-minute
891    Min30,
892    /// 60-minute
893    Min60,
894    /// Daily
895    #[default]
896    Day,
897    /// Weekly
898    Week,
899    /// Monthly
900    Month,
901    /// Yearly
902    Year,
903}
904
905impl AhPremiumPeriod {
906    /// Convert to the API's `line_type` parameter value
907    pub(crate) fn to_line_type(self) -> &'static str {
908        match self {
909            AhPremiumPeriod::Min1 => "1",
910            AhPremiumPeriod::Min5 => "5",
911            AhPremiumPeriod::Min15 => "15",
912            AhPremiumPeriod::Min30 => "30",
913            AhPremiumPeriod::Min60 => "60",
914            AhPremiumPeriod::Day => "1000",
915            AhPremiumPeriod::Week => "2000",
916            AhPremiumPeriod::Month => "3000",
917            AhPremiumPeriod::Year => "4000",
918        }
919    }
920}