1use num_enum::{FromPrimitive, IntoPrimitive};
2use rust_decimal::Decimal;
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use strum_macros::{Display, EnumString};
5use time::{Date, OffsetDateTime};
6
7use crate::{Market, serde_utils};
8
9#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
11#[allow(clippy::upper_case_acronyms)]
12pub enum OrderType {
13 Unknown,
15 #[strum(serialize = "LO")]
17 LO,
18 #[strum(serialize = "ELO")]
20 ELO,
21 #[strum(serialize = "MO")]
23 MO,
24 #[strum(serialize = "AO")]
26 AO,
27 #[strum(serialize = "ALO")]
29 ALO,
30 #[strum(serialize = "ODD")]
32 ODD,
33 #[strum(serialize = "LIT")]
35 LIT,
36 #[strum(serialize = "MIT")]
38 MIT,
39 #[strum(serialize = "TSLPAMT")]
41 TSLPAMT,
42 #[strum(serialize = "TSLPPCT")]
44 TSLPPCT,
45 #[strum(serialize = "TSMAMT")]
47 TSMAMT,
48 #[strum(serialize = "TSMPCT")]
50 TSMPCT,
51 #[strum(serialize = "SLO")]
53 SLO,
54}
55
56#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
58pub enum OrderStatus {
59 Unknown,
61 #[strum(serialize = "NotReported")]
63 NotReported,
64 #[strum(serialize = "ReplacedNotReported")]
66 ReplacedNotReported,
67 #[strum(serialize = "ProtectedNotReported")]
69 ProtectedNotReported,
70 #[strum(serialize = "VarietiesNotReported")]
72 VarietiesNotReported,
73 #[strum(serialize = "FilledStatus")]
75 Filled,
76 #[strum(serialize = "WaitToNew")]
78 WaitToNew,
79 #[strum(serialize = "NewStatus")]
81 New,
82 #[strum(serialize = "WaitToReplace")]
84 WaitToReplace,
85 #[strum(serialize = "PendingReplaceStatus")]
87 PendingReplace,
88 #[strum(serialize = "ReplacedStatus")]
90 Replaced,
91 #[strum(serialize = "PartialFilledStatus")]
93 PartialFilled,
94 #[strum(serialize = "WaitToCancel")]
96 WaitToCancel,
97 #[strum(serialize = "PendingCancelStatus")]
99 PendingCancel,
100 #[strum(serialize = "RejectedStatus")]
102 Rejected,
103 #[strum(serialize = "CanceledStatus")]
105 Canceled,
106 #[strum(serialize = "ExpiredStatus")]
108 Expired,
109 #[strum(serialize = "PartialWithdrawal")]
111 PartialWithdrawal,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct Execution {
117 pub order_id: String,
119 pub trade_id: String,
121 pub symbol: String,
123 #[serde(
125 serialize_with = "time::serde::rfc3339::serialize",
126 deserialize_with = "serde_utils::timestamp::deserialize"
127 )]
128 pub trade_done_at: OffsetDateTime,
129 pub quantity: Decimal,
131 pub price: Decimal,
133}
134
135#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
137pub enum OrderSide {
138 Unknown,
140 #[strum(serialize = "Buy")]
142 Buy,
143 #[strum(serialize = "Sell")]
145 Sell,
146}
147
148#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
150pub enum TriggerPriceType {
151 Unknown,
153 #[strum(serialize = "LIT")]
155 LimitIfTouched,
156 #[strum(serialize = "MIT")]
158 MarketIfTouched,
159}
160
161#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
163pub enum OrderTag {
164 Unknown,
166 #[strum(serialize = "Normal")]
168 Normal,
169 #[strum(serialize = "GTC")]
171 LongTerm,
172 #[strum(serialize = "Grey")]
174 Grey,
175 MarginCall,
177 Offline,
179 Creditor,
181 Debtor,
183 NonExercise,
185 AllocatedSub,
187}
188
189#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
191pub enum TimeInForceType {
192 Unknown,
194 #[strum(serialize = "Day")]
196 Day,
197 #[strum(serialize = "GTC")]
199 GoodTilCanceled,
200 #[strum(serialize = "GTD")]
202 GoodTilDate,
203}
204
205#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
207pub enum TriggerStatus {
208 Unknown,
210 #[strum(serialize = "DEACTIVE")]
212 Deactive,
213 #[strum(serialize = "ACTIVE")]
215 Active,
216 #[strum(serialize = "RELEASED")]
218 Released,
219}
220
221#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
223pub enum OutsideRTH {
224 Unknown,
226 #[strum(serialize = "RTH_ONLY")]
228 RTHOnly,
229 #[strum(serialize = "ANY_TIME")]
231 AnyTime,
232 #[strum(serialize = "OVERNIGHT")]
234 Overnight,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct Order {
240 pub order_id: String,
242 pub status: OrderStatus,
244 pub stock_name: String,
246 pub quantity: Decimal,
248 pub executed_quantity: Decimal,
250 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
252 pub price: Option<Decimal>,
253 #[serde(with = "serde_utils::decimal_opt_0_is_none")]
255 pub executed_price: Option<Decimal>,
256 #[serde(
258 serialize_with = "time::serde::rfc3339::serialize",
259 deserialize_with = "serde_utils::timestamp::deserialize"
260 )]
261 pub submitted_at: OffsetDateTime,
262 pub side: OrderSide,
264 pub symbol: String,
266 pub order_type: OrderType,
268 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
270 pub last_done: Option<Decimal>,
271 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
273 pub trigger_price: Option<Decimal>,
274 pub msg: String,
276 pub tag: OrderTag,
278 pub time_in_force: TimeInForceType,
280 #[serde(with = "serde_utils::date_opt")]
282 pub expire_date: Option<Date>,
283 #[serde(
285 deserialize_with = "serde_utils::timestamp_opt::deserialize",
286 serialize_with = "serde_utils::rfc3339_opt::serialize"
287 )]
288 pub updated_at: Option<OffsetDateTime>,
289 #[serde(
291 deserialize_with = "serde_utils::timestamp_opt::deserialize",
292 serialize_with = "serde_utils::rfc3339_opt::serialize"
293 )]
294 pub trigger_at: Option<OffsetDateTime>,
295 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
297 pub trailing_amount: Option<Decimal>,
298 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
300 pub trailing_percent: Option<Decimal>,
301 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
303 pub limit_offset: Option<Decimal>,
304 #[serde(with = "serde_utils::trigger_status")]
306 pub trigger_status: Option<TriggerStatus>,
307 pub currency: String,
309 #[serde(with = "serde_utils::outside_rth")]
311 pub outside_rth: Option<OutsideRTH>,
312 #[serde(with = "serde_utils::int32_opt_0_is_none")]
314 pub limit_depth_level: Option<i32>,
315 #[serde(with = "serde_utils::int32_opt_0_is_none")]
317 pub trigger_count: Option<i32>,
318 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
320 pub monitor_price: Option<Decimal>,
321 pub remark: String,
323}
324
325#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
327pub enum CommissionFreeStatus {
328 Unknown,
330 None,
332 Calculated,
334 Pending,
336 Ready,
338}
339
340#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
342pub enum DeductionStatus {
343 Unknown,
345 #[strum(serialize = "NONE")]
347 None,
348 #[strum(serialize = "NO_DATA")]
350 NoData,
351 #[strum(serialize = "PENDING")]
353 Pending,
354 #[strum(serialize = "DONE")]
356 Done,
357}
358
359#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
361pub enum ChargeCategoryCode {
362 Unknown,
364 #[strum(serialize = "BROKER_FEES")]
366 Broker,
367 #[strum(serialize = "THIRD_FEES")]
369 Third,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct OrderHistoryDetail {
375 #[serde(with = "serde_utils::decimal_empty_is_0")]
378 pub price: Decimal,
379 #[serde(with = "serde_utils::decimal_empty_is_0")]
382 pub quantity: Decimal,
383 pub status: OrderStatus,
385 pub msg: String,
387 #[serde(
389 serialize_with = "time::serde::rfc3339::serialize",
390 deserialize_with = "serde_utils::timestamp::deserialize"
391 )]
392 pub time: OffsetDateTime,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct OrderChargeFee {
398 pub code: String,
400 pub name: String,
402 #[serde(with = "serde_utils::decimal_empty_is_0")]
404 pub amount: Decimal,
405 pub currency: String,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct OrderChargeItem {
412 pub code: ChargeCategoryCode,
414 pub name: String,
416 pub fees: Vec<OrderChargeFee>,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct OrderChargeDetail {
423 pub total_amount: Decimal,
425 pub currency: String,
427 pub items: Vec<OrderChargeItem>,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct OrderDetail {
434 pub order_id: String,
436 pub status: OrderStatus,
438 pub stock_name: String,
440 pub quantity: Decimal,
442 pub executed_quantity: Decimal,
444 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
446 pub price: Option<Decimal>,
447 #[serde(with = "serde_utils::decimal_opt_0_is_none")]
449 pub executed_price: Option<Decimal>,
450 #[serde(
452 serialize_with = "time::serde::rfc3339::serialize",
453 deserialize_with = "serde_utils::timestamp::deserialize"
454 )]
455 pub submitted_at: OffsetDateTime,
456 pub side: OrderSide,
458 pub symbol: String,
460 pub order_type: OrderType,
462 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
464 pub last_done: Option<Decimal>,
465 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
467 pub trigger_price: Option<Decimal>,
468 pub msg: String,
470 pub tag: OrderTag,
472 pub time_in_force: TimeInForceType,
474 #[serde(with = "serde_utils::date_opt")]
476 pub expire_date: Option<Date>,
477 #[serde(
479 deserialize_with = "serde_utils::timestamp_opt::deserialize",
480 serialize_with = "serde_utils::rfc3339_opt::serialize"
481 )]
482 pub updated_at: Option<OffsetDateTime>,
483 #[serde(
485 deserialize_with = "serde_utils::timestamp_opt::deserialize",
486 serialize_with = "serde_utils::rfc3339_opt::serialize"
487 )]
488 pub trigger_at: Option<OffsetDateTime>,
489 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
491 pub trailing_amount: Option<Decimal>,
492 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
494 pub trailing_percent: Option<Decimal>,
495 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
497 pub limit_offset: Option<Decimal>,
498 #[serde(with = "serde_utils::trigger_status")]
500 pub trigger_status: Option<TriggerStatus>,
501 pub currency: String,
503 #[serde(with = "serde_utils::outside_rth")]
505 pub outside_rth: Option<OutsideRTH>,
506 #[serde(with = "serde_utils::int32_opt_0_is_none")]
508 pub limit_depth_level: Option<i32>,
509 #[serde(with = "serde_utils::int32_opt_0_is_none")]
511 pub trigger_count: Option<i32>,
512 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
514 pub monitor_price: Option<Decimal>,
515 pub remark: String,
517 pub free_status: CommissionFreeStatus,
519 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
521 pub free_amount: Option<Decimal>,
522 #[serde(with = "serde_utils::symbol_opt")]
524 pub free_currency: Option<String>,
525 pub deductions_status: DeductionStatus,
527 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
529 pub deductions_amount: Option<Decimal>,
530 #[serde(with = "serde_utils::symbol_opt")]
532 pub deductions_currency: Option<String>,
533 pub platform_deducted_status: DeductionStatus,
535 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
537 pub platform_deducted_amount: Option<Decimal>,
538 #[serde(with = "serde_utils::symbol_opt")]
540 pub platform_deducted_currency: Option<String>,
541 pub history: Vec<OrderHistoryDetail>,
543 pub charge_detail: OrderChargeDetail,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct CashInfo {
550 pub withdraw_cash: Decimal,
552 pub available_cash: Decimal,
554 pub frozen_cash: Decimal,
556 pub settling_cash: Decimal,
558 pub currency: String,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
564pub struct FrozenTransactionFee {
565 pub currency: String,
567 pub frozen_transaction_fee: Decimal,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct AccountBalance {
574 pub total_cash: Decimal,
576 pub max_finance_amount: Decimal,
578 pub remaining_finance_amount: Decimal,
580 #[serde(with = "serde_utils::risk_level")]
582 pub risk_level: i32,
583 pub margin_call: Decimal,
585 pub currency: String,
587 #[serde(default)]
589 pub cash_infos: Vec<CashInfo>,
590 #[serde(with = "serde_utils::decimal_empty_is_0")]
592 pub net_assets: Decimal,
593 #[serde(with = "serde_utils::decimal_empty_is_0")]
595 pub init_margin: Decimal,
596 #[serde(with = "serde_utils::decimal_empty_is_0")]
598 pub maintenance_margin: Decimal,
599 #[serde(with = "serde_utils::decimal_empty_is_0")]
601 pub buy_power: Decimal,
602 pub frozen_transaction_fees: Vec<FrozenTransactionFee>,
604}
605
606#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, IntoPrimitive)]
608#[repr(i32)]
609pub enum BalanceType {
610 #[num_enum(default)]
612 Unknown = 0,
613 Cash = 1,
615 Stock = 2,
617 Fund = 3,
619}
620
621impl Serialize for BalanceType {
622 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
623 let value: i32 = (*self).into();
624 value.serialize(serializer)
625 }
626}
627
628impl<'de> Deserialize<'de> for BalanceType {
629 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
630 let value = i32::deserialize(deserializer)?;
631 Ok(BalanceType::from(value))
632 }
633}
634
635#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, Serialize)]
637#[repr(i32)]
638pub enum CashFlowDirection {
639 #[num_enum(default)]
641 Unknown,
642 Out = 1,
644 In = 2,
646}
647
648impl<'de> Deserialize<'de> for CashFlowDirection {
649 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
650 let value = i32::deserialize(deserializer)?;
651 Ok(CashFlowDirection::from(value))
652 }
653}
654
655#[derive(Debug, Clone, Serialize, Deserialize)]
657pub struct CashFlow {
658 pub transaction_flow_name: String,
660 pub direction: CashFlowDirection,
662 pub business_type: BalanceType,
664 pub balance: Decimal,
666 pub currency: String,
668 #[serde(
670 serialize_with = "time::serde::rfc3339::serialize",
671 deserialize_with = "serde_utils::timestamp::deserialize"
672 )]
673 pub business_time: OffsetDateTime,
674 #[serde(with = "serde_utils::symbol_opt")]
676 pub symbol: Option<String>,
677 pub description: String,
679}
680
681#[derive(Debug, Clone, Serialize, Deserialize)]
683pub struct FundPositionsResponse {
684 #[serde(rename = "list")]
686 pub channels: Vec<FundPositionChannel>,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize)]
691pub struct FundPositionChannel {
692 pub account_channel: String,
694
695 #[serde(default, rename = "fund_info")]
697 pub positions: Vec<FundPosition>,
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize)]
702pub struct FundPosition {
703 pub symbol: String,
705 #[serde(with = "serde_utils::decimal_empty_is_0")]
707 pub current_net_asset_value: Decimal,
708 #[serde(
710 serialize_with = "time::serde::rfc3339::serialize",
711 deserialize_with = "serde_utils::timestamp::deserialize"
712 )]
713 pub net_asset_value_day: OffsetDateTime,
714 pub symbol_name: String,
716 pub currency: String,
718 #[serde(with = "serde_utils::decimal_empty_is_0")]
720 pub cost_net_asset_value: Decimal,
721 #[serde(with = "serde_utils::decimal_empty_is_0")]
723 pub holding_units: Decimal,
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize)]
728pub struct StockPositionsResponse {
729 #[serde(rename = "list")]
731 pub channels: Vec<StockPositionChannel>,
732}
733
734#[derive(Debug, Clone, Serialize, Deserialize)]
736pub struct StockPositionChannel {
737 pub account_channel: String,
739
740 #[serde(default, rename = "stock_info")]
742 pub positions: Vec<StockPosition>,
743}
744
745#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct StockPosition {
748 pub symbol: String,
750 pub symbol_name: String,
752 pub quantity: Decimal,
754 pub available_quantity: Decimal,
756 pub currency: String,
758 #[serde(with = "serde_utils::decimal_empty_is_0")]
761 pub cost_price: Decimal,
762 pub market: Market,
764 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
766 pub init_quantity: Option<Decimal>,
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize)]
771pub struct MarginRatio {
772 pub im_factor: Decimal,
774 pub mm_factor: Decimal,
776 pub fm_factor: Decimal,
778}
779
780impl_serde_for_enum_string!(
781 OrderType,
782 OrderStatus,
783 OrderSide,
784 TriggerPriceType,
785 OrderTag,
786 TimeInForceType,
787 TriggerStatus,
788 OutsideRTH,
789 CommissionFreeStatus,
790 DeductionStatus,
791 ChargeCategoryCode
792);
793impl_default_for_enum_string!(
794 OrderType,
795 OrderStatus,
796 OrderSide,
797 TriggerPriceType,
798 OrderTag,
799 TimeInForceType,
800 TriggerStatus,
801 OutsideRTH,
802 CommissionFreeStatus,
803 DeductionStatus,
804 ChargeCategoryCode
805);
806
807#[cfg(test)]
808mod tests {
809 use time::macros::datetime;
810
811 use super::*;
812
813 #[test]
814 fn fund_position_response() {
815 let data = r#"
816 {
817 "list": [{
818 "account_channel": "lb",
819 "fund_info": [{
820 "symbol": "HK0000447943",
821 "symbol_name": "高腾亚洲收益基金",
822 "currency": "USD",
823 "holding_units": "5.000",
824 "current_net_asset_value": "0",
825 "cost_net_asset_value": "0.00",
826 "net_asset_value_day": "1649865600"
827 }]
828 }]
829 }
830 "#;
831
832 let resp: FundPositionsResponse = serde_json::from_str(data).unwrap();
833 assert_eq!(resp.channels.len(), 1);
834
835 let channel = &resp.channels[0];
836 assert_eq!(channel.account_channel, "lb");
837 assert_eq!(channel.positions.len(), 1);
838
839 let position = &channel.positions[0];
840 assert_eq!(position.symbol, "HK0000447943");
841 assert_eq!(position.symbol_name, "高腾亚洲收益基金");
842 assert_eq!(position.currency, "USD");
843 assert_eq!(position.current_net_asset_value, decimal!(0i32));
844 assert_eq!(position.cost_net_asset_value, decimal!(0i32));
845 assert_eq!(position.holding_units, decimal!(5i32));
846 assert_eq!(position.net_asset_value_day, datetime!(2022-4-14 0:00 +8));
847 }
848
849 #[test]
850 fn stock_position_response() {
851 let data = r#"
852 {
853 "list": [
854 {
855 "account_channel": "lb",
856 "stock_info": [
857 {
858 "symbol": "700.HK",
859 "symbol_name": "腾讯控股",
860 "currency": "HK",
861 "quantity": "650",
862 "available_quantity": "-450",
863 "cost_price": "457.53",
864 "market": "HK",
865 "init_quantity": "2000"
866 },
867 {
868 "symbol": "9991.HK",
869 "symbol_name": "宝尊电商-SW",
870 "currency": "HK",
871 "quantity": "200",
872 "available_quantity": "0",
873 "cost_price": "32.25",
874 "market": "HK",
875 "init_quantity": ""
876 }
877 ]
878 }
879 ]
880 }
881 "#;
882
883 let resp: StockPositionsResponse = serde_json::from_str(data).unwrap();
884 assert_eq!(resp.channels.len(), 1);
885
886 let channel = &resp.channels[0];
887 assert_eq!(channel.account_channel, "lb");
888 assert_eq!(channel.positions.len(), 2);
889
890 let position = &channel.positions[0];
891 assert_eq!(position.symbol, "700.HK");
892 assert_eq!(position.symbol_name, "腾讯控股");
893 assert_eq!(position.currency, "HK");
894 assert_eq!(position.quantity, decimal!(650));
895 assert_eq!(position.available_quantity, decimal!(-450));
896 assert_eq!(position.cost_price, decimal!(457.53f32));
897 assert_eq!(position.market, Market::HK);
898 assert_eq!(position.init_quantity, Some(decimal!(2000)));
899
900 let position = &channel.positions[0];
901 assert_eq!(position.symbol, "700.HK");
902 assert_eq!(position.symbol_name, "腾讯控股");
903 assert_eq!(position.currency, "HK");
904 assert_eq!(position.quantity, decimal!(650));
905 assert_eq!(position.available_quantity, decimal!(-450));
906 assert_eq!(position.cost_price, decimal!(457.53f32));
907 assert_eq!(position.market, Market::HK);
908
909 let position = &channel.positions[1];
910 assert_eq!(position.symbol, "9991.HK");
911 assert_eq!(position.symbol_name, "宝尊电商-SW");
912 assert_eq!(position.currency, "HK");
913 assert_eq!(position.quantity, decimal!(200));
914 assert_eq!(position.available_quantity, decimal!(0));
915 assert_eq!(position.cost_price, decimal!(32.25f32));
916 assert_eq!(position.init_quantity, None);
917 }
918
919 #[test]
920 fn cash_flow() {
921 let data = r#"
922 {
923 "list": [
924 {
925 "transaction_flow_name": "BuyContract-Stocks",
926 "direction": 1,
927 "balance": "-248.60",
928 "currency": "USD",
929 "business_type": 1,
930 "business_time": "1621507957",
931 "symbol": "AAPL.US",
932 "description": "AAPL"
933 },
934 {
935 "transaction_flow_name": "BuyContract-Stocks",
936 "direction": 1,
937 "balance": "-125.16",
938 "currency": "USD",
939 "business_type": 2,
940 "business_time": "1621504824",
941 "symbol": "AAPL.US",
942 "description": "AAPL"
943 }
944 ]
945 }
946 "#;
947
948 #[derive(Debug, Deserialize)]
949 struct Response {
950 list: Vec<CashFlow>,
951 }
952
953 let resp: Response = serde_json::from_str(data).unwrap();
954 assert_eq!(resp.list.len(), 2);
955
956 let cashflow = &resp.list[0];
957 assert_eq!(cashflow.transaction_flow_name, "BuyContract-Stocks");
958 assert_eq!(cashflow.direction, CashFlowDirection::Out);
959 assert_eq!(cashflow.balance, decimal!(-248.60f32));
960 assert_eq!(cashflow.currency, "USD");
961 assert_eq!(cashflow.business_type, BalanceType::Cash);
962 assert_eq!(cashflow.business_time, datetime!(2021-05-20 18:52:37 +8));
963 assert_eq!(cashflow.symbol.as_deref(), Some("AAPL.US"));
964 assert_eq!(cashflow.description, "AAPL");
965
966 let cashflow = &resp.list[1];
967 assert_eq!(cashflow.transaction_flow_name, "BuyContract-Stocks");
968 assert_eq!(cashflow.direction, CashFlowDirection::Out);
969 assert_eq!(cashflow.balance, decimal!(-125.16f32));
970 assert_eq!(cashflow.currency, "USD");
971 assert_eq!(cashflow.business_type, BalanceType::Stock);
972 assert_eq!(cashflow.business_time, datetime!(2021-05-20 18:00:24 +8));
973 assert_eq!(cashflow.symbol.as_deref(), Some("AAPL.US"));
974 assert_eq!(cashflow.description, "AAPL");
975 }
976
977 #[test]
978 fn account_balance() {
979 let data = r#"
980 {
981 "list": [
982 {
983 "total_cash": "1759070010.72",
984 "max_finance_amount": "977582000",
985 "remaining_finance_amount": "0",
986 "risk_level": "1",
987 "margin_call": "2598051051.50",
988 "currency": "HKD",
989 "cash_infos": [
990 {
991 "withdraw_cash": "97592.30",
992 "available_cash": "195902464.37",
993 "frozen_cash": "11579339.13",
994 "settling_cash": "207288537.81",
995 "currency": "HKD"
996 },
997 {
998 "withdraw_cash": "199893416.74",
999 "available_cash": "199893416.74",
1000 "frozen_cash": "28723.76",
1001 "settling_cash": "-276806.51",
1002 "currency": "USD"
1003 }
1004 ],
1005 "net_assets": "11111.12",
1006 "init_margin": "2222.23",
1007 "maintenance_margin": "3333.45",
1008 "buy_power": "1234.67",
1009 "frozen_transaction_fees": [
1010 {
1011 "currency": "HKD",
1012 "frozen_transaction_fee": "123"
1013 }
1014 ]
1015 }
1016 ]
1017 }"#;
1018
1019 #[derive(Debug, Deserialize)]
1020 struct Response {
1021 list: Vec<AccountBalance>,
1022 }
1023
1024 let resp: Response = serde_json::from_str(data).unwrap();
1025 assert_eq!(resp.list.len(), 1);
1026
1027 let balance = &resp.list[0];
1028 assert_eq!(balance.total_cash, "1759070010.72".parse().unwrap());
1029 assert_eq!(balance.max_finance_amount, "977582000".parse().unwrap());
1030 assert_eq!(balance.remaining_finance_amount, decimal!(0i32));
1031 assert_eq!(balance.risk_level, 1);
1032 assert_eq!(balance.margin_call, "2598051051.50".parse().unwrap());
1033 assert_eq!(balance.currency, "HKD");
1034 assert_eq!(balance.net_assets, "11111.12".parse().unwrap());
1035 assert_eq!(balance.init_margin, "2222.23".parse().unwrap());
1036 assert_eq!(balance.maintenance_margin, "3333.45".parse().unwrap());
1037 assert_eq!(balance.buy_power, "1234.67".parse().unwrap());
1038
1039 assert_eq!(balance.cash_infos.len(), 2);
1040
1041 let cash_info = &balance.cash_infos[0];
1042 assert_eq!(cash_info.withdraw_cash, "97592.30".parse().unwrap());
1043 assert_eq!(cash_info.available_cash, "195902464.37".parse().unwrap());
1044 assert_eq!(cash_info.frozen_cash, "11579339.13".parse().unwrap());
1045 assert_eq!(cash_info.settling_cash, "207288537.81".parse().unwrap());
1046 assert_eq!(cash_info.currency, "HKD");
1047
1048 let cash_info = &balance.cash_infos[1];
1049 assert_eq!(cash_info.withdraw_cash, "199893416.74".parse().unwrap());
1050 assert_eq!(cash_info.available_cash, "199893416.74".parse().unwrap());
1051 assert_eq!(cash_info.frozen_cash, "28723.76".parse().unwrap());
1052 assert_eq!(cash_info.settling_cash, "-276806.51".parse().unwrap());
1053 assert_eq!(cash_info.currency, "USD");
1054
1055 assert_eq!(balance.frozen_transaction_fees.len(), 1);
1056
1057 let frozen_transaction_fee = &balance.frozen_transaction_fees[0];
1058 assert_eq!(frozen_transaction_fee.currency, "HKD");
1059 assert_eq!(
1060 frozen_transaction_fee.frozen_transaction_fee,
1061 "123".parse().unwrap()
1062 );
1063 }
1064
1065 #[test]
1066 fn history_orders() {
1067 let data = r#"
1068 {
1069 "orders": [
1070 {
1071 "currency": "HKD",
1072 "executed_price": "0.000",
1073 "executed_quantity": "0",
1074 "expire_date": "",
1075 "last_done": "",
1076 "limit_offset": "",
1077 "msg": "",
1078 "order_id": "706388312699592704",
1079 "order_type": "ELO",
1080 "outside_rth": "UnknownOutsideRth",
1081 "price": "11.900",
1082 "quantity": "200",
1083 "side": "Buy",
1084 "status": "RejectedStatus",
1085 "stock_name": "Bank of East Asia Ltd/The",
1086 "submitted_at": "1651644897",
1087 "symbol": "23.HK",
1088 "tag": "Normal",
1089 "time_in_force": "Day",
1090 "trailing_amount": "",
1091 "trailing_percent": "",
1092 "trigger_at": "0",
1093 "trigger_price": "",
1094 "trigger_status": "NOT_USED",
1095 "updated_at": "1651644898",
1096 "limit_depth_level": 0,
1097 "trigger_count": 0,
1098 "monitor_price": "",
1099 "remark": "abc"
1100 }
1101 ]
1102 }
1103 "#;
1104
1105 #[derive(Deserialize)]
1106 struct Response {
1107 orders: Vec<Order>,
1108 }
1109
1110 let resp: Response = serde_json::from_str(data).unwrap();
1111 assert_eq!(resp.orders.len(), 1);
1112
1113 let order = &resp.orders[0];
1114 assert_eq!(order.currency, "HKD");
1115 assert!(order.executed_price.is_none());
1116 assert_eq!(order.executed_quantity, decimal!(0));
1117 assert!(order.expire_date.is_none());
1118 assert!(order.last_done.is_none());
1119 assert!(order.limit_offset.is_none());
1120 assert_eq!(order.msg, "");
1121 assert_eq!(order.order_id, "706388312699592704");
1122 assert_eq!(order.order_type, OrderType::ELO);
1123 assert!(order.outside_rth.is_none());
1124 assert_eq!(order.price, Some("11.900".parse().unwrap()));
1125 assert_eq!(order.quantity, decimal!(200));
1126 assert_eq!(order.side, OrderSide::Buy);
1127 assert_eq!(order.status, OrderStatus::Rejected);
1128 assert_eq!(order.stock_name, "Bank of East Asia Ltd/The");
1129 assert_eq!(order.submitted_at, datetime!(2022-05-04 14:14:57 +8));
1130 assert_eq!(order.symbol, "23.HK");
1131 assert_eq!(order.tag, OrderTag::Normal);
1132 assert_eq!(order.time_in_force, TimeInForceType::Day);
1133 assert!(order.trailing_amount.is_none());
1134 assert!(order.trailing_percent.is_none());
1135 assert!(order.trigger_at.is_none());
1136 assert!(order.trigger_price.is_none());
1137 assert!(order.trigger_status.is_none());
1138 assert_eq!(order.updated_at, Some(datetime!(2022-05-04 14:14:58 +8)));
1139 assert_eq!(order.remark, "abc");
1140 }
1141
1142 #[test]
1143 fn today_orders() {
1144 let data = r#"
1145 {
1146 "orders": [
1147 {
1148 "currency": "HKD",
1149 "executed_price": "0.000",
1150 "executed_quantity": "0",
1151 "expire_date": "",
1152 "last_done": "",
1153 "limit_offset": "",
1154 "msg": "",
1155 "order_id": "706388312699592704",
1156 "order_type": "ELO",
1157 "outside_rth": "UnknownOutsideRth",
1158 "price": "11.900",
1159 "quantity": "200",
1160 "side": "Buy",
1161 "status": "RejectedStatus",
1162 "stock_name": "Bank of East Asia Ltd/The",
1163 "submitted_at": "1651644897",
1164 "symbol": "23.HK",
1165 "tag": "Normal",
1166 "time_in_force": "Day",
1167 "trailing_amount": "",
1168 "trailing_percent": "",
1169 "trigger_at": "0",
1170 "trigger_price": "",
1171 "trigger_status": "NOT_USED",
1172 "updated_at": "1651644898",
1173 "limit_depth_level": 0,
1174 "trigger_count": 0,
1175 "monitor_price": "",
1176 "remark": "abc"
1177 }
1178 ]
1179 }
1180 "#;
1181
1182 #[derive(Deserialize)]
1183 struct Response {
1184 orders: Vec<Order>,
1185 }
1186
1187 let resp: Response = serde_json::from_str(data).unwrap();
1188 assert_eq!(resp.orders.len(), 1);
1189
1190 let order = &resp.orders[0];
1191 assert_eq!(order.currency, "HKD");
1192 assert!(order.executed_price.is_none());
1193 assert_eq!(order.executed_quantity, decimal!(0));
1194 assert!(order.expire_date.is_none());
1195 assert!(order.last_done.is_none());
1196 assert!(order.limit_offset.is_none());
1197 assert_eq!(order.msg, "");
1198 assert_eq!(order.order_id, "706388312699592704");
1199 assert_eq!(order.order_type, OrderType::ELO);
1200 assert!(order.outside_rth.is_none());
1201 assert_eq!(order.price, Some("11.900".parse().unwrap()));
1202 assert_eq!(order.quantity, decimal!(200));
1203 assert_eq!(order.side, OrderSide::Buy);
1204 assert_eq!(order.status, OrderStatus::Rejected);
1205 assert_eq!(order.stock_name, "Bank of East Asia Ltd/The");
1206 assert_eq!(order.submitted_at, datetime!(2022-05-04 14:14:57 +8));
1207 assert_eq!(order.symbol, "23.HK");
1208 assert_eq!(order.tag, OrderTag::Normal);
1209 assert_eq!(order.time_in_force, TimeInForceType::Day);
1210 assert!(order.trailing_amount.is_none());
1211 assert!(order.trailing_percent.is_none());
1212 assert!(order.trigger_at.is_none());
1213 assert!(order.trigger_price.is_none());
1214 assert!(order.trigger_status.is_none());
1215 assert_eq!(order.updated_at, Some(datetime!(2022-05-04 14:14:58 +8)));
1216 assert_eq!(order.remark, "abc");
1217 }
1218
1219 #[test]
1220 fn history_executions() {
1221 let data = r#"
1222 {
1223 "has_more": false,
1224 "trades": [
1225 {
1226 "order_id": "693664675163312128",
1227 "price": "388",
1228 "quantity": "100",
1229 "symbol": "700.HK",
1230 "trade_done_at": "1648611351",
1231 "trade_id": "693664675163312128-1648611351433741210"
1232 }
1233 ]
1234 }
1235 "#;
1236
1237 #[derive(Deserialize)]
1238 struct Response {
1239 trades: Vec<Execution>,
1240 }
1241
1242 let resp: Response = serde_json::from_str(data).unwrap();
1243 assert_eq!(resp.trades.len(), 1);
1244
1245 let execution = &resp.trades[0];
1246 assert_eq!(execution.order_id, "693664675163312128");
1247 assert_eq!(execution.price, "388".parse().unwrap());
1248 assert_eq!(execution.quantity, decimal!(100));
1249 assert_eq!(execution.symbol, "700.HK");
1250 assert_eq!(execution.trade_done_at, datetime!(2022-03-30 11:35:51 +8));
1251 assert_eq!(execution.trade_id, "693664675163312128-1648611351433741210");
1252 }
1253
1254 #[test]
1255 fn order_detail() {
1256 let data = r#"
1257 {
1258 "order_id": "828940451093708800",
1259 "status": "FilledStatus",
1260 "stock_name": "Apple",
1261 "quantity": "10",
1262 "executed_quantity": "10",
1263 "price": "200.000",
1264 "executed_price": "164.660",
1265 "submitted_at": "1680863604",
1266 "side": "Buy",
1267 "symbol": "AAPL.US",
1268 "order_type": "LO",
1269 "last_done": "164.660",
1270 "trigger_price": "0.0000",
1271 "msg": "",
1272 "tag": "Normal",
1273 "time_in_force": "Day",
1274 "expire_date": "2023-04-10",
1275 "updated_at": "1681113000",
1276 "trigger_at": "0",
1277 "trailing_amount": "",
1278 "trailing_percent": "",
1279 "limit_offset": "",
1280 "trigger_status": "NOT_USED",
1281 "outside_rth": "ANY_TIME",
1282 "currency": "USD",
1283 "limit_depth_level": 0,
1284 "trigger_count": 0,
1285 "monitor_price": "",
1286 "remark": "1680863603.927165",
1287 "free_status": "None",
1288 "free_amount": "",
1289 "free_currency": "",
1290 "deductions_status": "NONE",
1291 "deductions_amount": "",
1292 "deductions_currency": "",
1293 "platform_deducted_status": "NONE",
1294 "platform_deducted_amount": "",
1295 "platform_deducted_currency": "",
1296 "history": [{
1297 "price": "164.6600",
1298 "quantity": "10",
1299 "status": "FilledStatus",
1300 "msg": "Execution of 10",
1301 "time": "1681113000"
1302 }, {
1303 "price": "200.0000",
1304 "quantity": "10",
1305 "status": "NewStatus",
1306 "msg": "",
1307 "time": "1681113000"
1308 }],
1309 "charge_detail": {
1310 "items": [{
1311 "code": "BROKER_FEES",
1312 "name": "Broker Fees",
1313 "fees": []
1314 }, {
1315 "code": "THIRD_FEES",
1316 "name": "Third-party Fees",
1317 "fees": []
1318 }],
1319 "total_amount": "0",
1320 "currency": "USD"
1321 }
1322 }
1323 "#;
1324
1325 _ = serde_json::from_str::<OrderDetail>(data).unwrap();
1326 }
1327}