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 pub remark: String,
314}
315
316#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
318pub enum CommissionFreeStatus {
319 Unknown,
321 None,
323 Calculated,
325 Pending,
327 Ready,
329}
330
331#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
333pub enum DeductionStatus {
334 Unknown,
336 #[strum(serialize = "NONE")]
338 None,
339 #[strum(serialize = "NO_DATA")]
341 NoData,
342 #[strum(serialize = "PENDING")]
344 Pending,
345 #[strum(serialize = "DONE")]
347 Done,
348}
349
350#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
352pub enum ChargeCategoryCode {
353 Unknown,
355 #[strum(serialize = "BROKER_FEES")]
357 Broker,
358 #[strum(serialize = "THIRD_FEES")]
360 Third,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct OrderHistoryDetail {
366 #[serde(with = "serde_utils::decimal_empty_is_0")]
369 pub price: Decimal,
370 pub quantity: Decimal,
373 pub status: OrderStatus,
375 pub msg: String,
377 #[serde(
379 serialize_with = "time::serde::rfc3339::serialize",
380 deserialize_with = "serde_utils::timestamp::deserialize"
381 )]
382 pub time: OffsetDateTime,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct OrderChargeFee {
388 pub code: String,
390 pub name: String,
392 #[serde(with = "serde_utils::decimal_empty_is_0")]
394 pub amount: Decimal,
395 pub currency: String,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct OrderChargeItem {
402 pub code: ChargeCategoryCode,
404 pub name: String,
406 pub fees: Vec<OrderChargeFee>,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct OrderChargeDetail {
413 pub total_amount: Decimal,
415 pub currency: String,
417 pub items: Vec<OrderChargeItem>,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct OrderDetail {
424 pub order_id: String,
426 pub status: OrderStatus,
428 pub stock_name: String,
430 pub quantity: Decimal,
432 pub executed_quantity: Decimal,
434 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
436 pub price: Option<Decimal>,
437 #[serde(with = "serde_utils::decimal_opt_0_is_none")]
439 pub executed_price: Option<Decimal>,
440 #[serde(
442 serialize_with = "time::serde::rfc3339::serialize",
443 deserialize_with = "serde_utils::timestamp::deserialize"
444 )]
445 pub submitted_at: OffsetDateTime,
446 pub side: OrderSide,
448 pub symbol: String,
450 pub order_type: OrderType,
452 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
454 pub last_done: Option<Decimal>,
455 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
457 pub trigger_price: Option<Decimal>,
458 pub msg: String,
460 pub tag: OrderTag,
462 pub time_in_force: TimeInForceType,
464 #[serde(with = "serde_utils::date_opt")]
466 pub expire_date: Option<Date>,
467 #[serde(
469 deserialize_with = "serde_utils::timestamp_opt::deserialize",
470 serialize_with = "serde_utils::rfc3339_opt::serialize"
471 )]
472 pub updated_at: Option<OffsetDateTime>,
473 #[serde(
475 deserialize_with = "serde_utils::timestamp_opt::deserialize",
476 serialize_with = "serde_utils::rfc3339_opt::serialize"
477 )]
478 pub trigger_at: Option<OffsetDateTime>,
479 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
481 pub trailing_amount: Option<Decimal>,
482 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
484 pub trailing_percent: Option<Decimal>,
485 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
487 pub limit_offset: Option<Decimal>,
488 #[serde(with = "serde_utils::trigger_status")]
490 pub trigger_status: Option<TriggerStatus>,
491 pub currency: String,
493 #[serde(with = "serde_utils::outside_rth")]
495 pub outside_rth: Option<OutsideRTH>,
496 pub remark: String,
498 pub free_status: CommissionFreeStatus,
500 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
502 pub free_amount: Option<Decimal>,
503 #[serde(with = "serde_utils::symbol_opt")]
505 pub free_currency: Option<String>,
506 pub deductions_status: DeductionStatus,
508 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
510 pub deductions_amount: Option<Decimal>,
511 #[serde(with = "serde_utils::symbol_opt")]
513 pub deductions_currency: Option<String>,
514 pub platform_deducted_status: DeductionStatus,
516 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
518 pub platform_deducted_amount: Option<Decimal>,
519 #[serde(with = "serde_utils::symbol_opt")]
521 pub platform_deducted_currency: Option<String>,
522 pub history: Vec<OrderHistoryDetail>,
524 pub charge_detail: OrderChargeDetail,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize)]
530pub struct CashInfo {
531 pub withdraw_cash: Decimal,
533 pub available_cash: Decimal,
535 pub frozen_cash: Decimal,
537 pub settling_cash: Decimal,
539 pub currency: String,
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct AccountBalance {
546 pub total_cash: Decimal,
548 pub max_finance_amount: Decimal,
550 pub remaining_finance_amount: Decimal,
552 #[serde(with = "serde_utils::risk_level")]
554 pub risk_level: i32,
555 pub margin_call: Decimal,
557 pub currency: String,
559 #[serde(default)]
561 pub cash_infos: Vec<CashInfo>,
562 #[serde(with = "serde_utils::decimal_empty_is_0")]
564 pub net_assets: Decimal,
565 #[serde(with = "serde_utils::decimal_empty_is_0")]
567 pub init_margin: Decimal,
568 #[serde(with = "serde_utils::decimal_empty_is_0")]
570 pub maintenance_margin: Decimal,
571 #[serde(with = "serde_utils::decimal_empty_is_0")]
573 pub buy_power: Decimal,
574}
575
576#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, IntoPrimitive)]
578#[repr(i32)]
579pub enum BalanceType {
580 #[num_enum(default)]
582 Unknown = 0,
583 Cash = 1,
585 Stock = 2,
587 Fund = 3,
589}
590
591impl Serialize for BalanceType {
592 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
593 let value: i32 = (*self).into();
594 value.serialize(serializer)
595 }
596}
597
598impl<'de> Deserialize<'de> for BalanceType {
599 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
600 let value = i32::deserialize(deserializer)?;
601 Ok(BalanceType::from(value))
602 }
603}
604
605#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, Serialize)]
607#[repr(i32)]
608pub enum CashFlowDirection {
609 #[num_enum(default)]
611 Unknown,
612 Out = 1,
614 In = 2,
616}
617
618impl<'de> Deserialize<'de> for CashFlowDirection {
619 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
620 let value = i32::deserialize(deserializer)?;
621 Ok(CashFlowDirection::from(value))
622 }
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize)]
627pub struct CashFlow {
628 pub transaction_flow_name: String,
630 pub direction: CashFlowDirection,
632 pub business_type: BalanceType,
634 pub balance: Decimal,
636 pub currency: String,
638 #[serde(
640 serialize_with = "time::serde::rfc3339::serialize",
641 deserialize_with = "serde_utils::timestamp::deserialize"
642 )]
643 pub business_time: OffsetDateTime,
644 #[serde(with = "serde_utils::symbol_opt")]
646 pub symbol: Option<String>,
647 pub description: String,
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct FundPositionsResponse {
654 #[serde(rename = "list")]
656 pub channels: Vec<FundPositionChannel>,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize)]
661pub struct FundPositionChannel {
662 pub account_channel: String,
664
665 #[serde(default, rename = "fund_info")]
667 pub positions: Vec<FundPosition>,
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct FundPosition {
673 pub symbol: String,
675 #[serde(with = "serde_utils::decimal_empty_is_0")]
677 pub current_net_asset_value: Decimal,
678 #[serde(
680 serialize_with = "time::serde::rfc3339::serialize",
681 deserialize_with = "serde_utils::timestamp::deserialize"
682 )]
683 pub net_asset_value_day: OffsetDateTime,
684 pub symbol_name: String,
686 pub currency: String,
688 #[serde(with = "serde_utils::decimal_empty_is_0")]
690 pub cost_net_asset_value: Decimal,
691 #[serde(with = "serde_utils::decimal_empty_is_0")]
693 pub holding_units: Decimal,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct StockPositionsResponse {
699 #[serde(rename = "list")]
701 pub channels: Vec<StockPositionChannel>,
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize)]
706pub struct StockPositionChannel {
707 pub account_channel: String,
709
710 #[serde(default, rename = "stock_info")]
712 pub positions: Vec<StockPosition>,
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct StockPosition {
718 pub symbol: String,
720 pub symbol_name: String,
722 pub quantity: Decimal,
724 pub available_quantity: Decimal,
726 pub currency: String,
728 #[serde(with = "serde_utils::decimal_empty_is_0")]
731 pub cost_price: Decimal,
732 pub market: Market,
734 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
736 pub init_quantity: Option<Decimal>,
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize)]
741pub struct MarginRatio {
742 pub im_factor: Decimal,
744 pub mm_factor: Decimal,
746 pub fm_factor: Decimal,
748}
749
750impl_serde_for_enum_string!(
751 OrderType,
752 OrderStatus,
753 OrderSide,
754 TriggerPriceType,
755 OrderTag,
756 TimeInForceType,
757 TriggerStatus,
758 OutsideRTH,
759 CommissionFreeStatus,
760 DeductionStatus,
761 ChargeCategoryCode
762);
763impl_default_for_enum_string!(
764 OrderType,
765 OrderStatus,
766 OrderSide,
767 TriggerPriceType,
768 OrderTag,
769 TimeInForceType,
770 TriggerStatus,
771 OutsideRTH,
772 CommissionFreeStatus,
773 DeductionStatus,
774 ChargeCategoryCode
775);
776
777#[cfg(test)]
778mod tests {
779 use time::macros::datetime;
780
781 use super::*;
782
783 #[test]
784 fn fund_position_response() {
785 let data = r#"
786 {
787 "list": [{
788 "account_channel": "lb",
789 "fund_info": [{
790 "symbol": "HK0000447943",
791 "symbol_name": "高腾亚洲收益基金",
792 "currency": "USD",
793 "holding_units": "5.000",
794 "current_net_asset_value": "0",
795 "cost_net_asset_value": "0.00",
796 "net_asset_value_day": "1649865600"
797 }]
798 }]
799 }
800 "#;
801
802 let resp: FundPositionsResponse = serde_json::from_str(data).unwrap();
803 assert_eq!(resp.channels.len(), 1);
804
805 let channel = &resp.channels[0];
806 assert_eq!(channel.account_channel, "lb");
807 assert_eq!(channel.positions.len(), 1);
808
809 let position = &channel.positions[0];
810 assert_eq!(position.symbol, "HK0000447943");
811 assert_eq!(position.symbol_name, "高腾亚洲收益基金");
812 assert_eq!(position.currency, "USD");
813 assert_eq!(position.current_net_asset_value, decimal!(0i32));
814 assert_eq!(position.cost_net_asset_value, decimal!(0i32));
815 assert_eq!(position.holding_units, decimal!(5i32));
816 assert_eq!(position.net_asset_value_day, datetime!(2022-4-14 0:00 +8));
817 }
818
819 #[test]
820 fn stock_position_response() {
821 let data = r#"
822 {
823 "list": [
824 {
825 "account_channel": "lb",
826 "stock_info": [
827 {
828 "symbol": "700.HK",
829 "symbol_name": "腾讯控股",
830 "currency": "HK",
831 "quantity": "650",
832 "available_quantity": "-450",
833 "cost_price": "457.53",
834 "market": "HK",
835 "init_quantity": "2000"
836 },
837 {
838 "symbol": "9991.HK",
839 "symbol_name": "宝尊电商-SW",
840 "currency": "HK",
841 "quantity": "200",
842 "available_quantity": "0",
843 "cost_price": "32.25",
844 "market": "HK",
845 "init_quantity": ""
846 }
847 ]
848 }
849 ]
850 }
851 "#;
852
853 let resp: StockPositionsResponse = serde_json::from_str(data).unwrap();
854 assert_eq!(resp.channels.len(), 1);
855
856 let channel = &resp.channels[0];
857 assert_eq!(channel.account_channel, "lb");
858 assert_eq!(channel.positions.len(), 2);
859
860 let position = &channel.positions[0];
861 assert_eq!(position.symbol, "700.HK");
862 assert_eq!(position.symbol_name, "腾讯控股");
863 assert_eq!(position.currency, "HK");
864 assert_eq!(position.quantity, decimal!(650));
865 assert_eq!(position.available_quantity, decimal!(-450));
866 assert_eq!(position.cost_price, decimal!(457.53f32));
867 assert_eq!(position.market, Market::HK);
868 assert_eq!(position.init_quantity, Some(decimal!(2000)));
869
870 let position = &channel.positions[0];
871 assert_eq!(position.symbol, "700.HK");
872 assert_eq!(position.symbol_name, "腾讯控股");
873 assert_eq!(position.currency, "HK");
874 assert_eq!(position.quantity, decimal!(650));
875 assert_eq!(position.available_quantity, decimal!(-450));
876 assert_eq!(position.cost_price, decimal!(457.53f32));
877 assert_eq!(position.market, Market::HK);
878
879 let position = &channel.positions[1];
880 assert_eq!(position.symbol, "9991.HK");
881 assert_eq!(position.symbol_name, "宝尊电商-SW");
882 assert_eq!(position.currency, "HK");
883 assert_eq!(position.quantity, decimal!(200));
884 assert_eq!(position.available_quantity, decimal!(0));
885 assert_eq!(position.cost_price, decimal!(32.25f32));
886 assert_eq!(position.init_quantity, None);
887 }
888
889 #[test]
890 fn cash_flow() {
891 let data = r#"
892 {
893 "list": [
894 {
895 "transaction_flow_name": "BuyContract-Stocks",
896 "direction": 1,
897 "balance": "-248.60",
898 "currency": "USD",
899 "business_type": 1,
900 "business_time": "1621507957",
901 "symbol": "AAPL.US",
902 "description": "AAPL"
903 },
904 {
905 "transaction_flow_name": "BuyContract-Stocks",
906 "direction": 1,
907 "balance": "-125.16",
908 "currency": "USD",
909 "business_type": 2,
910 "business_time": "1621504824",
911 "symbol": "AAPL.US",
912 "description": "AAPL"
913 }
914 ]
915 }
916 "#;
917
918 #[derive(Debug, Deserialize)]
919 struct Response {
920 list: Vec<CashFlow>,
921 }
922
923 let resp: Response = serde_json::from_str(data).unwrap();
924 assert_eq!(resp.list.len(), 2);
925
926 let cashflow = &resp.list[0];
927 assert_eq!(cashflow.transaction_flow_name, "BuyContract-Stocks");
928 assert_eq!(cashflow.direction, CashFlowDirection::Out);
929 assert_eq!(cashflow.balance, decimal!(-248.60f32));
930 assert_eq!(cashflow.currency, "USD");
931 assert_eq!(cashflow.business_type, BalanceType::Cash);
932 assert_eq!(cashflow.business_time, datetime!(2021-05-20 18:52:37 +8));
933 assert_eq!(cashflow.symbol.as_deref(), Some("AAPL.US"));
934 assert_eq!(cashflow.description, "AAPL");
935
936 let cashflow = &resp.list[1];
937 assert_eq!(cashflow.transaction_flow_name, "BuyContract-Stocks");
938 assert_eq!(cashflow.direction, CashFlowDirection::Out);
939 assert_eq!(cashflow.balance, decimal!(-125.16f32));
940 assert_eq!(cashflow.currency, "USD");
941 assert_eq!(cashflow.business_type, BalanceType::Stock);
942 assert_eq!(cashflow.business_time, datetime!(2021-05-20 18:00:24 +8));
943 assert_eq!(cashflow.symbol.as_deref(), Some("AAPL.US"));
944 assert_eq!(cashflow.description, "AAPL");
945 }
946
947 #[test]
948 fn account_balance() {
949 let data = r#"
950 {
951 "list": [
952 {
953 "total_cash": "1759070010.72",
954 "max_finance_amount": "977582000",
955 "remaining_finance_amount": "0",
956 "risk_level": "1",
957 "margin_call": "2598051051.50",
958 "currency": "HKD",
959 "cash_infos": [
960 {
961 "withdraw_cash": "97592.30",
962 "available_cash": "195902464.37",
963 "frozen_cash": "11579339.13",
964 "settling_cash": "207288537.81",
965 "currency": "HKD"
966 },
967 {
968 "withdraw_cash": "199893416.74",
969 "available_cash": "199893416.74",
970 "frozen_cash": "28723.76",
971 "settling_cash": "-276806.51",
972 "currency": "USD"
973 }
974 ],
975 "net_assets": "11111.12",
976 "init_margin": "2222.23",
977 "maintenance_margin": "3333.45",
978 "buy_power": "1234.67"
979 }
980 ]
981 }"#;
982
983 #[derive(Debug, Deserialize)]
984 struct Response {
985 list: Vec<AccountBalance>,
986 }
987
988 let resp: Response = serde_json::from_str(data).unwrap();
989 assert_eq!(resp.list.len(), 1);
990
991 let balance = &resp.list[0];
992 assert_eq!(balance.total_cash, "1759070010.72".parse().unwrap());
993 assert_eq!(balance.max_finance_amount, "977582000".parse().unwrap());
994 assert_eq!(balance.remaining_finance_amount, decimal!(0i32));
995 assert_eq!(balance.risk_level, 1);
996 assert_eq!(balance.margin_call, "2598051051.50".parse().unwrap());
997 assert_eq!(balance.currency, "HKD");
998 assert_eq!(balance.net_assets, "11111.12".parse().unwrap());
999 assert_eq!(balance.init_margin, "2222.23".parse().unwrap());
1000 assert_eq!(balance.maintenance_margin, "3333.45".parse().unwrap());
1001 assert_eq!(balance.buy_power, "1234.67".parse().unwrap());
1002
1003 assert_eq!(balance.cash_infos.len(), 2);
1004
1005 let cash_info = &balance.cash_infos[0];
1006 assert_eq!(cash_info.withdraw_cash, "97592.30".parse().unwrap());
1007 assert_eq!(cash_info.available_cash, "195902464.37".parse().unwrap());
1008 assert_eq!(cash_info.frozen_cash, "11579339.13".parse().unwrap());
1009 assert_eq!(cash_info.settling_cash, "207288537.81".parse().unwrap());
1010 assert_eq!(cash_info.currency, "HKD");
1011
1012 let cash_info = &balance.cash_infos[1];
1013 assert_eq!(cash_info.withdraw_cash, "199893416.74".parse().unwrap());
1014 assert_eq!(cash_info.available_cash, "199893416.74".parse().unwrap());
1015 assert_eq!(cash_info.frozen_cash, "28723.76".parse().unwrap());
1016 assert_eq!(cash_info.settling_cash, "-276806.51".parse().unwrap());
1017 assert_eq!(cash_info.currency, "USD");
1018 }
1019
1020 #[test]
1021 fn history_orders() {
1022 let data = r#"
1023 {
1024 "orders": [
1025 {
1026 "currency": "HKD",
1027 "executed_price": "0.000",
1028 "executed_quantity": "0",
1029 "expire_date": "",
1030 "last_done": "",
1031 "limit_offset": "",
1032 "msg": "",
1033 "order_id": "706388312699592704",
1034 "order_type": "ELO",
1035 "outside_rth": "UnknownOutsideRth",
1036 "price": "11.900",
1037 "quantity": "200",
1038 "side": "Buy",
1039 "status": "RejectedStatus",
1040 "stock_name": "Bank of East Asia Ltd/The",
1041 "submitted_at": "1651644897",
1042 "symbol": "23.HK",
1043 "tag": "Normal",
1044 "time_in_force": "Day",
1045 "trailing_amount": "",
1046 "trailing_percent": "",
1047 "trigger_at": "0",
1048 "trigger_price": "",
1049 "trigger_status": "NOT_USED",
1050 "updated_at": "1651644898",
1051 "remark": "abc"
1052 }
1053 ]
1054 }
1055 "#;
1056
1057 #[derive(Deserialize)]
1058 struct Response {
1059 orders: Vec<Order>,
1060 }
1061
1062 let resp: Response = serde_json::from_str(data).unwrap();
1063 assert_eq!(resp.orders.len(), 1);
1064
1065 let order = &resp.orders[0];
1066 assert_eq!(order.currency, "HKD");
1067 assert!(order.executed_price.is_none());
1068 assert_eq!(order.executed_quantity, decimal!(0));
1069 assert!(order.expire_date.is_none());
1070 assert!(order.last_done.is_none());
1071 assert!(order.limit_offset.is_none());
1072 assert_eq!(order.msg, "");
1073 assert_eq!(order.order_id, "706388312699592704");
1074 assert_eq!(order.order_type, OrderType::ELO);
1075 assert!(order.outside_rth.is_none());
1076 assert_eq!(order.price, Some("11.900".parse().unwrap()));
1077 assert_eq!(order.quantity, decimal!(200));
1078 assert_eq!(order.side, OrderSide::Buy);
1079 assert_eq!(order.status, OrderStatus::Rejected);
1080 assert_eq!(order.stock_name, "Bank of East Asia Ltd/The");
1081 assert_eq!(order.submitted_at, datetime!(2022-05-04 14:14:57 +8));
1082 assert_eq!(order.symbol, "23.HK");
1083 assert_eq!(order.tag, OrderTag::Normal);
1084 assert_eq!(order.time_in_force, TimeInForceType::Day);
1085 assert!(order.trailing_amount.is_none());
1086 assert!(order.trailing_percent.is_none());
1087 assert!(order.trigger_at.is_none());
1088 assert!(order.trigger_price.is_none());
1089 assert!(order.trigger_status.is_none());
1090 assert_eq!(order.updated_at, Some(datetime!(2022-05-04 14:14:58 +8)));
1091 assert_eq!(order.remark, "abc");
1092 }
1093
1094 #[test]
1095 fn today_orders() {
1096 let data = r#"
1097 {
1098 "orders": [
1099 {
1100 "currency": "HKD",
1101 "executed_price": "0.000",
1102 "executed_quantity": "0",
1103 "expire_date": "",
1104 "last_done": "",
1105 "limit_offset": "",
1106 "msg": "",
1107 "order_id": "706388312699592704",
1108 "order_type": "ELO",
1109 "outside_rth": "UnknownOutsideRth",
1110 "price": "11.900",
1111 "quantity": "200",
1112 "side": "Buy",
1113 "status": "RejectedStatus",
1114 "stock_name": "Bank of East Asia Ltd/The",
1115 "submitted_at": "1651644897",
1116 "symbol": "23.HK",
1117 "tag": "Normal",
1118 "time_in_force": "Day",
1119 "trailing_amount": "",
1120 "trailing_percent": "",
1121 "trigger_at": "0",
1122 "trigger_price": "",
1123 "trigger_status": "NOT_USED",
1124 "updated_at": "1651644898",
1125 "remark": "abc"
1126 }
1127 ]
1128 }
1129 "#;
1130
1131 #[derive(Deserialize)]
1132 struct Response {
1133 orders: Vec<Order>,
1134 }
1135
1136 let resp: Response = serde_json::from_str(data).unwrap();
1137 assert_eq!(resp.orders.len(), 1);
1138
1139 let order = &resp.orders[0];
1140 assert_eq!(order.currency, "HKD");
1141 assert!(order.executed_price.is_none());
1142 assert_eq!(order.executed_quantity, decimal!(0));
1143 assert!(order.expire_date.is_none());
1144 assert!(order.last_done.is_none());
1145 assert!(order.limit_offset.is_none());
1146 assert_eq!(order.msg, "");
1147 assert_eq!(order.order_id, "706388312699592704");
1148 assert_eq!(order.order_type, OrderType::ELO);
1149 assert!(order.outside_rth.is_none());
1150 assert_eq!(order.price, Some("11.900".parse().unwrap()));
1151 assert_eq!(order.quantity, decimal!(200));
1152 assert_eq!(order.side, OrderSide::Buy);
1153 assert_eq!(order.status, OrderStatus::Rejected);
1154 assert_eq!(order.stock_name, "Bank of East Asia Ltd/The");
1155 assert_eq!(order.submitted_at, datetime!(2022-05-04 14:14:57 +8));
1156 assert_eq!(order.symbol, "23.HK");
1157 assert_eq!(order.tag, OrderTag::Normal);
1158 assert_eq!(order.time_in_force, TimeInForceType::Day);
1159 assert!(order.trailing_amount.is_none());
1160 assert!(order.trailing_percent.is_none());
1161 assert!(order.trigger_at.is_none());
1162 assert!(order.trigger_price.is_none());
1163 assert!(order.trigger_status.is_none());
1164 assert_eq!(order.updated_at, Some(datetime!(2022-05-04 14:14:58 +8)));
1165 assert_eq!(order.remark, "abc");
1166 }
1167
1168 #[test]
1169 fn history_executions() {
1170 let data = r#"
1171 {
1172 "has_more": false,
1173 "trades": [
1174 {
1175 "order_id": "693664675163312128",
1176 "price": "388",
1177 "quantity": "100",
1178 "symbol": "700.HK",
1179 "trade_done_at": "1648611351",
1180 "trade_id": "693664675163312128-1648611351433741210"
1181 }
1182 ]
1183 }
1184 "#;
1185
1186 #[derive(Deserialize)]
1187 struct Response {
1188 trades: Vec<Execution>,
1189 }
1190
1191 let resp: Response = serde_json::from_str(data).unwrap();
1192 assert_eq!(resp.trades.len(), 1);
1193
1194 let execution = &resp.trades[0];
1195 assert_eq!(execution.order_id, "693664675163312128");
1196 assert_eq!(execution.price, "388".parse().unwrap());
1197 assert_eq!(execution.quantity, decimal!(100));
1198 assert_eq!(execution.symbol, "700.HK");
1199 assert_eq!(execution.trade_done_at, datetime!(2022-03-30 11:35:51 +8));
1200 assert_eq!(execution.trade_id, "693664675163312128-1648611351433741210");
1201 }
1202
1203 #[test]
1204 fn order_detail() {
1205 let data = r#"
1206 {
1207 "order_id": "828940451093708800",
1208 "status": "FilledStatus",
1209 "stock_name": "Apple",
1210 "quantity": "10",
1211 "executed_quantity": "10",
1212 "price": "200.000",
1213 "executed_price": "164.660",
1214 "submitted_at": "1680863604",
1215 "side": "Buy",
1216 "symbol": "AAPL.US",
1217 "order_type": "LO",
1218 "last_done": "164.660",
1219 "trigger_price": "0.0000",
1220 "msg": "",
1221 "tag": "Normal",
1222 "time_in_force": "Day",
1223 "expire_date": "2023-04-10",
1224 "updated_at": "1681113000",
1225 "trigger_at": "0",
1226 "trailing_amount": "",
1227 "trailing_percent": "",
1228 "limit_offset": "",
1229 "trigger_status": "NOT_USED",
1230 "outside_rth": "ANY_TIME",
1231 "currency": "USD",
1232 "remark": "1680863603.927165",
1233 "free_status": "None",
1234 "free_amount": "",
1235 "free_currency": "",
1236 "deductions_status": "NONE",
1237 "deductions_amount": "",
1238 "deductions_currency": "",
1239 "platform_deducted_status": "NONE",
1240 "platform_deducted_amount": "",
1241 "platform_deducted_currency": "",
1242 "history": [{
1243 "price": "164.6600",
1244 "quantity": "10",
1245 "status": "FilledStatus",
1246 "msg": "Execution of 10",
1247 "time": "1681113000"
1248 }, {
1249 "price": "200.0000",
1250 "quantity": "10",
1251 "status": "NewStatus",
1252 "msg": "",
1253 "time": "1681113000"
1254 }],
1255 "charge_detail": {
1256 "items": [{
1257 "code": "BROKER_FEES",
1258 "name": "Broker Fees",
1259 "fees": []
1260 }, {
1261 "code": "THIRD_FEES",
1262 "name": "Third-party Fees",
1263 "fees": []
1264 }],
1265 "total_amount": "0",
1266 "currency": "USD"
1267 }
1268 }
1269 "#;
1270
1271 _ = serde_json::from_str::<OrderDetail>(data).unwrap();
1272 }
1273}