1#![allow(missing_docs)]
2
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5use serde_repr::{Deserialize_repr, Serialize_repr};
6use strum_macros::{FromRepr, IntoStaticStr};
7use time::OffsetDateTime;
8
9use crate::{types::Market, utils::counter::deserialize_counter_id_as_symbol};
10
11#[allow(non_camel_case_types)]
15#[derive(
16 Debug,
17 Clone,
18 Copy,
19 Default,
20 Hash,
21 PartialOrd,
22 Ord,
23 PartialEq,
24 Eq,
25 FromRepr,
26 IntoStaticStr,
27 Serialize_repr,
28 Deserialize_repr,
29)]
30#[repr(i32)]
31pub enum TradeStatus {
32 #[default]
34 #[serde(other)]
35 UNKNOWN = -1,
36 NO_REGISTER_QUOTE = 0,
38 CLEAN = 101,
40 OPEN_BID = 102,
42 MORNING_CLOSING = 103,
44 TRADING = 105,
46 NOON_CLOSING = 106,
48 CLOSE_BID = 107,
50 CLOSING = 108,
52 DARK_WAIT = 110,
54 DARK_TRADING = 111,
56 DARK_CLOSING = 112,
58 AFTER_FIX = 120,
60 HALF_CLOSING = 121,
63 NOT_OPENED = 122,
66 REALTIME_QUOTE = 123,
69 US_PREV = 201,
71 US_TRADING = 202,
73 US_AFTER = 203,
75 US_CLOSING = 204,
77 US_STOP = 205,
79 US_CLEAN = 206,
81 US_NIGHT = 207,
83 US_PREV_MARKET_CLEAN = 209,
85 US_AFTER_MARKET_CLEAN = 210,
87 REFRESH = 1000,
89 DELIST = 1001,
91 PREPARE = 1002,
93 CODE_CHANGE = 1003,
95 STOP = 1004,
97 WILL_OPEN = 1005,
99 COMMON_SUSPEND = 1006,
101 EXPIRE = 1007,
103 NO_QUOTE = 1008,
105 UNITED = 1009,
107 TRADING_HALT = 1010,
109 WAIT_LISTING = 1011,
111 FUSE = 2001,
113}
114
115impl From<i32> for TradeStatus {
116 fn from(value: i32) -> Self {
117 Self::from_repr(value).unwrap_or_default()
118 }
119}
120
121impl TradeStatus {
122 pub fn from_isize(value: isize) -> TradeStatus {
124 (value as i32).into()
125 }
126
127 pub fn code(self) -> i32 {
129 self as i32
130 }
131
132 pub fn as_static(self) -> &'static str {
134 self.into()
135 }
136
137 pub fn label(self) -> &'static str {
139 let status = self.normalize();
140 match status {
141 TradeStatus::US_PREV
142 | TradeStatus::US_TRADING
143 | TradeStatus::US_AFTER
144 | TradeStatus::US_NIGHT
145 | TradeStatus::US_CLOSING
146 | TradeStatus::TRADING
147 | TradeStatus::CLOSING => status.name(),
148 _ => "",
149 }
150 }
151
152 pub fn name(self) -> &'static str {
154 match self.normalize() {
155 TradeStatus::UNKNOWN | TradeStatus::NO_REGISTER_QUOTE => "Unknown",
156 TradeStatus::OPEN_BID => "Open Bid",
157 TradeStatus::MORNING_CLOSING => "Morning Break",
158 TradeStatus::TRADING | TradeStatus::US_TRADING | TradeStatus::US_AFTER_MARKET_CLEAN => {
159 "Trading"
160 }
161 TradeStatus::NOON_CLOSING => "Mid-Day Break",
162 TradeStatus::CLOSE_BID => "Close Bid",
163 TradeStatus::CLOSING
164 | TradeStatus::CLEAN
165 | TradeStatus::HALF_CLOSING
166 | TradeStatus::US_CLOSING
167 | TradeStatus::US_PREV_MARKET_CLEAN => "Closed",
168 TradeStatus::DARK_WAIT => "Dark Wait",
169 TradeStatus::DARK_TRADING => "Dark Trading",
170 TradeStatus::DARK_CLOSING => "Closing",
171 TradeStatus::AFTER_FIX => "After Fix",
172 TradeStatus::NOT_OPENED => "Not Open",
173 TradeStatus::REALTIME_QUOTE => "Temporary Break",
174 TradeStatus::US_PREV | TradeStatus::US_CLEAN => "Pre-Market",
175 TradeStatus::US_AFTER => "Post-Market",
176 TradeStatus::US_STOP | TradeStatus::STOP => "Stop",
177 TradeStatus::US_NIGHT => "Overnight",
178 TradeStatus::REFRESH => "Refresh",
179 TradeStatus::DELIST => "Delist",
180 TradeStatus::PREPARE => "Prepare",
181 TradeStatus::CODE_CHANGE => "Code Change",
182 TradeStatus::WILL_OPEN => "Will Open",
183 TradeStatus::COMMON_SUSPEND => "Common Suspend",
184 TradeStatus::EXPIRE => "Expire",
185 TradeStatus::NO_QUOTE => "No Quote",
186 TradeStatus::UNITED => "Not Listed",
187 TradeStatus::TRADING_HALT => "Terminated",
188 TradeStatus::WAIT_LISTING => "Wait Listing",
189 TradeStatus::FUSE => "Fuse",
190 }
191 }
192
193 pub fn is_us_market(self) -> bool {
195 self.code() >= 200 && self.code() < 300
196 }
197
198 pub fn is_us_pre_post(self) -> bool {
200 self.is_us_prev() || self.is_us_after()
201 }
202
203 pub fn is_us_night(self) -> bool {
205 matches!(self, TradeStatus::US_NIGHT)
206 }
207
208 pub fn is_us_closing(self) -> bool {
210 matches!(
211 self,
212 TradeStatus::US_CLOSING | TradeStatus::US_PREV_MARKET_CLEAN
213 )
214 }
215
216 pub fn is_closing(self) -> bool {
218 matches!(
219 self,
220 TradeStatus::US_CLOSING
221 | TradeStatus::US_PREV_MARKET_CLEAN
222 | TradeStatus::CLOSING
223 | TradeStatus::HALF_CLOSING
224 )
225 }
226
227 pub fn is_us_prev(self) -> bool {
229 matches!(self, TradeStatus::US_PREV | TradeStatus::US_CLEAN)
230 }
231
232 pub fn is_us_after(self) -> bool {
234 matches!(self, TradeStatus::US_AFTER)
235 }
236
237 pub fn is_trading(self) -> bool {
239 matches!(
240 self,
241 TradeStatus::TRADING | TradeStatus::US_TRADING | TradeStatus::US_AFTER_MARKET_CLEAN
242 )
243 }
244
245 pub fn is_dark(self) -> bool {
247 matches!(
248 self,
249 TradeStatus::DARK_WAIT | TradeStatus::DARK_TRADING | TradeStatus::DARK_CLOSING
250 )
251 }
252
253 pub fn allow_trading(self) -> bool {
255 matches!(
256 self,
257 TradeStatus::OPEN_BID
258 | TradeStatus::TRADING
259 | TradeStatus::CLOSE_BID
260 | TradeStatus::NOT_OPENED
261 | TradeStatus::NOON_CLOSING
262 | TradeStatus::US_TRADING
263 | TradeStatus::US_AFTER_MARKET_CLEAN
264 )
265 }
266
267 #[must_use]
269 pub fn normalize(self) -> Self {
270 match self {
271 TradeStatus::CLEAN => TradeStatus::CLOSING,
272 TradeStatus::US_PREV_MARKET_CLEAN => TradeStatus::US_CLOSING,
273 TradeStatus::US_CLEAN => TradeStatus::US_PREV,
274 TradeStatus::US_AFTER_MARKET_CLEAN => TradeStatus::US_TRADING,
275 _ => self,
276 }
277 }
278
279 pub fn is_special(self) -> bool {
281 self.code() < 100 || self == Self::US_STOP || self.code() >= 1000
282 }
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct MarketStatusResponse {
288 pub market_time: Vec<MarketTimeItem>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct MarketTimeItem {
295 pub market: Market,
297 pub trade_status: TradeStatus,
299 pub timestamp: String,
301 pub delay_trade_status: TradeStatus,
304 pub delay_timestamp: String,
306 pub sub_status: i32,
308 pub delay_sub_status: i32,
310}
311
312#[cfg(test)]
313mod tests {
314 use crate::market::TradeStatus;
315
316 #[test]
317 fn market_trade_status_deserializes_numeric_codes() {
318 assert_eq!(
319 serde_json::from_str::<TradeStatus>("202")
320 .expect("202 should deserialize as market trade status"),
321 TradeStatus::US_TRADING
322 );
323 assert_eq!(
324 serde_json::from_str::<TradeStatus>("456")
325 .expect("unknown numeric status should deserialize"),
326 TradeStatus::UNKNOWN
327 );
328 }
329
330 #[test]
331 fn market_trade_status_serializes_as_numeric_code() {
332 let value = serde_json::to_string(&TradeStatus::US_CLEAN)
333 .expect("market trade status should serialize");
334 assert_eq!(value, "206");
335 }
336
337 #[test]
338 fn market_trade_status_normalizes_engine_aliases() {
339 assert_eq!(TradeStatus::CLEAN.normalize(), TradeStatus::CLOSING);
340 assert_eq!(TradeStatus::US_CLEAN.normalize(), TradeStatus::US_PREV);
341 assert_eq!(
342 TradeStatus::US_PREV_MARKET_CLEAN.normalize(),
343 TradeStatus::US_CLOSING
344 );
345 assert_eq!(
346 TradeStatus::US_AFTER_MARKET_CLEAN.normalize(),
347 TradeStatus::US_TRADING
348 );
349 }
350
351 #[test]
352 fn market_trade_status_label_matches_engine_simplified_display() {
353 assert_eq!(TradeStatus::US_PREV.label(), "Pre-Market");
354 assert_eq!(TradeStatus::US_CLEAN.label(), "Pre-Market");
355 assert_eq!(TradeStatus::US_AFTER.label(), "Post-Market");
356 assert_eq!(TradeStatus::US_CLOSING.label(), "Closed");
357 assert_eq!(TradeStatus::US_AFTER_MARKET_CLEAN.label(), "Trading");
358 assert_eq!(TradeStatus::US_TRADING.label(), "Trading");
359 assert_eq!(TradeStatus::TRADING.label(), "Trading");
360 assert_eq!(TradeStatus::CLEAN.label(), "Closed");
361 assert_eq!(TradeStatus::OPEN_BID.label(), "");
362 assert_eq!(TradeStatus::NOON_CLOSING.label(), "");
363 }
364
365 #[test]
366 fn market_trade_status_name_covers_full_status_set() {
367 let cases = [
368 (TradeStatus::MORNING_CLOSING, "Morning Break"),
369 (TradeStatus::NOON_CLOSING, "Mid-Day Break"),
370 (TradeStatus::REALTIME_QUOTE, "Temporary Break"),
371 (TradeStatus::US_STOP, "Stop"),
372 (TradeStatus::TRADING_HALT, "Terminated"),
373 (TradeStatus::WAIT_LISTING, "Wait Listing"),
374 (TradeStatus::FUSE, "Fuse"),
375 (TradeStatus::UNKNOWN, "Unknown"),
376 (TradeStatus::NO_REGISTER_QUOTE, "Unknown"),
377 ];
378
379 for (status, expected) in cases {
380 assert_eq!(status.name(), expected, "status {status:?}");
381 }
382 }
383
384 #[test]
385 fn market_trade_status_codes_match_phase_definition_document() {
386 let codes = [
387 101, 102, 103, 105, 106, 107, 108, 110, 111, 112, 120, 121, 122, 123, 201, 202, 203,
388 204, 206, 207, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011,
389 2001,
390 ];
391
392 for code in codes {
393 assert_eq!(TradeStatus::from(code).code(), code, "status code {code}");
394 }
395 }
396
397 #[test]
398 fn market_trade_status_names_match_phase_definition_document() {
399 let cases = [
400 (123, "Temporary Break"),
401 (1009, "Not Listed"),
402 (1010, "Terminated"),
403 (2001, "Fuse"),
404 ];
405
406 for (code, expected) in cases {
407 assert_eq!(
408 TradeStatus::from(code).name(),
409 expected,
410 "status code {code}"
411 );
412 }
413 }
414
415 #[test]
416 fn market_time_item_uses_market_trade_status_type() {
417 let item = serde_json::from_str::<crate::market::MarketTimeItem>(
418 r#"{
419 "market": "US",
420 "trade_status": 202,
421 "timestamp": "1717200000",
422 "delay_trade_status": 204,
423 "delay_timestamp": "1717200000",
424 "sub_status": 0,
425 "delay_sub_status": 0
426 }"#,
427 )
428 .expect("market time item should deserialize");
429
430 assert_eq!(item.trade_status, TradeStatus::US_TRADING);
431 assert_eq!(item.delay_trade_status, TradeStatus::US_CLOSING);
432 }
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct BrokerHoldingTop {
440 pub buy: Vec<BrokerHoldingEntry>,
442 pub sell: Vec<BrokerHoldingEntry>,
444 #[serde(default)]
446 pub updated_at: String,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct BrokerHoldingEntry {
452 pub name: String,
454 pub parti_number: String,
456 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
458 pub chg: Option<Decimal>,
459 pub strong: bool,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct BrokerHoldingDetail {
468 pub list: Vec<BrokerHoldingDetailItem>,
470 #[serde(default)]
472 pub updated_at: String,
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct BrokerHoldingDetailItem {
478 pub name: String,
480 pub parti_number: String,
482 pub ratio: BrokerHoldingChanges,
484 pub shares: BrokerHoldingChanges,
486 pub strong: bool,
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct BrokerHoldingChanges {
493 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
495 pub value: Option<Decimal>,
496 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
498 pub chg_1: Option<Decimal>,
499 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
501 pub chg_5: Option<Decimal>,
502 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
504 pub chg_20: Option<Decimal>,
505 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
507 pub chg_60: Option<Decimal>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
514pub struct BrokerHoldingDailyHistory {
515 pub list: Vec<BrokerHoldingDailyItem>,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct BrokerHoldingDailyItem {
522 pub date: String,
524 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
526 pub holding: Option<Decimal>,
527 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
529 pub ratio: Option<Decimal>,
530 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
532 pub chg: Option<Decimal>,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct AhPremiumKlines {
540 pub klines: Vec<AhPremiumKline>,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct AhPremiumIntraday {
547 pub klines: Vec<AhPremiumKline>,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct AhPremiumKline {
554 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
556 pub aprice: Decimal,
557 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
559 pub apreclose: Decimal,
560 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
562 pub hprice: Decimal,
563 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
565 pub hpreclose: Decimal,
566 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
568 pub currency_rate: Decimal,
569 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
571 pub ahpremium_rate: Decimal,
572 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
574 pub price_spread: Decimal,
575 #[serde(deserialize_with = "crate::serde_utils::deserialize_timestamp")]
577 pub timestamp: OffsetDateTime,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct TradeStatsResponse {
585 pub statistics: TradeStatistics,
587 pub trades: Vec<TradePriceLevel>,
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct TradeStatistics {
594 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
596 pub avgprice: Decimal,
597 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
599 pub buy: Decimal,
600 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
602 pub neutral: Decimal,
603 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
605 pub preclose: Decimal,
606 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
608 pub sell: Decimal,
609 pub timestamp: String,
611 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
613 pub total_amount: Decimal,
614 pub trade_date: Vec<String>,
616 pub trades_count: String,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct TradePriceLevel {
623 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
625 pub buy_amount: Decimal,
626 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
628 pub neutral_amount: Decimal,
629 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
631 pub price: Decimal,
632 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
634 pub sell_amount: Decimal,
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
641pub struct AnomalyResponse {
642 pub all_off: bool,
644 pub changes: Vec<AnomalyItem>,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct AnomalyItem {
651 #[serde(
653 rename = "counter_id",
654 deserialize_with = "deserialize_counter_id_as_symbol"
655 )]
656 pub symbol: String,
657 pub name: String,
659 pub alert_name: String,
661 pub alert_time: i64,
663 pub change_values: Vec<String>,
665 pub emotion: i32,
667}
668
669#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct IndexConstituents {
674 pub fall_num: i32,
676 pub flat_num: i32,
678 pub rise_num: i32,
680 pub stocks: Vec<ConstituentStock>,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize)]
686pub struct ConstituentStock {
687 #[serde(
689 rename = "counter_id",
690 deserialize_with = "deserialize_counter_id_as_symbol"
691 )]
692 pub symbol: String,
693 pub name: String,
695 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
697 pub last_done: Option<Decimal>,
698 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
700 pub prev_close: Option<Decimal>,
701 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
703 pub inflow: Option<Decimal>,
704 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
706 pub balance: Option<Decimal>,
707 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
709 pub amount: Option<Decimal>,
710 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
712 pub total_shares: Option<Decimal>,
713 pub tags: Vec<String>,
715 pub intro: String,
717 pub market: String,
719 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
721 pub circulating_shares: Option<Decimal>,
722 pub delay: bool,
724 #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
726 pub chg: Option<Decimal>,
727 pub trade_status: i32,
729}
730
731#[derive(Debug, Clone, Serialize, Deserialize)]
735pub struct TopMoversStock {
736 pub symbol: String,
738 pub code: String,
740 pub name: String,
742 #[serde(default)]
744 pub full_name: String,
745 pub change: String,
747 pub last_done: String,
749 pub market: String,
751 #[serde(default)]
753 pub labels: Vec<String>,
754 #[serde(default)]
756 pub logo: String,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct TopMoversEvent {
762 pub timestamp: String,
764 pub alert_reason: String,
766 pub alert_type: i64,
768 pub stock: TopMoversStock,
770 pub post: serde_json::Value,
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize)]
776pub struct TopMoversResponse {
777 pub events: Vec<TopMoversEvent>,
779 pub next_params: serde_json::Value,
781}
782
783#[derive(Debug, Clone, Serialize, Deserialize)]
790pub struct RankCategoriesResponse {
791 pub data: serde_json::Value,
793}
794
795#[derive(Debug, Clone, Serialize, Deserialize)]
799pub struct RankListItem {
800 pub symbol: String,
802 pub code: String,
804 pub name: String,
806 pub last_done: String,
808 pub chg: String,
810 pub change: String,
812 pub inflow: String,
814 pub market_cap: String,
816 pub industry: String,
818 #[serde(default)]
820 pub pre_post_price: String,
821 #[serde(default)]
823 pub pre_post_chg: String,
824 #[serde(default)]
826 pub amplitude: String,
827 #[serde(default)]
829 pub five_day_chg: String,
830 #[serde(default)]
832 pub turnover_rate: String,
833 #[serde(default)]
835 pub volume_rate: String,
836 #[serde(default)]
838 pub pb_ttm: String,
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize)]
843pub struct RankListResponse {
844 pub bmp: bool,
846 pub lists: Vec<RankListItem>,
848}
849
850#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
854pub enum BrokerHoldingPeriod {
855 #[default]
857 #[serde(rename = "rct_1")]
858 Rct1,
859 #[serde(rename = "rct_5")]
861 Rct5,
862 #[serde(rename = "rct_20")]
864 Rct20,
865 #[serde(rename = "rct_60")]
867 Rct60,
868}
869
870#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
872pub enum AhPremiumPeriod {
873 Min1,
875 Min5,
877 Min15,
879 Min30,
881 Min60,
883 #[default]
885 Day,
886 Week,
888 Month,
890 Year,
892}
893
894impl AhPremiumPeriod {
895 pub(crate) fn to_line_type(self) -> &'static str {
897 match self {
898 AhPremiumPeriod::Min1 => "1",
899 AhPremiumPeriod::Min5 => "5",
900 AhPremiumPeriod::Min15 => "15",
901 AhPremiumPeriod::Min30 => "30",
902 AhPremiumPeriod::Min60 => "60",
903 AhPremiumPeriod::Day => "1000",
904 AhPremiumPeriod::Week => "2000",
905 AhPremiumPeriod::Month => "3000",
906 AhPremiumPeriod::Year => "4000",
907 }
908 }
909}