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 #[serde(with = "serde_utils::decimal_empty_is_0")]
373 pub quantity: Decimal,
374 pub status: OrderStatus,
376 pub msg: String,
378 #[serde(
380 serialize_with = "time::serde::rfc3339::serialize",
381 deserialize_with = "serde_utils::timestamp::deserialize"
382 )]
383 pub time: OffsetDateTime,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct OrderChargeFee {
389 pub code: String,
391 pub name: String,
393 #[serde(with = "serde_utils::decimal_empty_is_0")]
395 pub amount: Decimal,
396 pub currency: String,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct OrderChargeItem {
403 pub code: ChargeCategoryCode,
405 pub name: String,
407 pub fees: Vec<OrderChargeFee>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct OrderChargeDetail {
414 pub total_amount: Decimal,
416 pub currency: String,
418 pub items: Vec<OrderChargeItem>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct OrderDetail {
425 pub order_id: String,
427 pub status: OrderStatus,
429 pub stock_name: String,
431 pub quantity: Decimal,
433 pub executed_quantity: Decimal,
435 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
437 pub price: Option<Decimal>,
438 #[serde(with = "serde_utils::decimal_opt_0_is_none")]
440 pub executed_price: Option<Decimal>,
441 #[serde(
443 serialize_with = "time::serde::rfc3339::serialize",
444 deserialize_with = "serde_utils::timestamp::deserialize"
445 )]
446 pub submitted_at: OffsetDateTime,
447 pub side: OrderSide,
449 pub symbol: String,
451 pub order_type: OrderType,
453 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
455 pub last_done: Option<Decimal>,
456 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
458 pub trigger_price: Option<Decimal>,
459 pub msg: String,
461 pub tag: OrderTag,
463 pub time_in_force: TimeInForceType,
465 #[serde(with = "serde_utils::date_opt")]
467 pub expire_date: Option<Date>,
468 #[serde(
470 deserialize_with = "serde_utils::timestamp_opt::deserialize",
471 serialize_with = "serde_utils::rfc3339_opt::serialize"
472 )]
473 pub updated_at: Option<OffsetDateTime>,
474 #[serde(
476 deserialize_with = "serde_utils::timestamp_opt::deserialize",
477 serialize_with = "serde_utils::rfc3339_opt::serialize"
478 )]
479 pub trigger_at: Option<OffsetDateTime>,
480 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
482 pub trailing_amount: Option<Decimal>,
483 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
485 pub trailing_percent: Option<Decimal>,
486 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
488 pub limit_offset: Option<Decimal>,
489 #[serde(with = "serde_utils::trigger_status")]
491 pub trigger_status: Option<TriggerStatus>,
492 pub currency: String,
494 #[serde(with = "serde_utils::outside_rth")]
496 pub outside_rth: Option<OutsideRTH>,
497 pub remark: String,
499 pub free_status: CommissionFreeStatus,
501 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
503 pub free_amount: Option<Decimal>,
504 #[serde(with = "serde_utils::symbol_opt")]
506 pub free_currency: Option<String>,
507 pub deductions_status: DeductionStatus,
509 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
511 pub deductions_amount: Option<Decimal>,
512 #[serde(with = "serde_utils::symbol_opt")]
514 pub deductions_currency: Option<String>,
515 pub platform_deducted_status: DeductionStatus,
517 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
519 pub platform_deducted_amount: Option<Decimal>,
520 #[serde(with = "serde_utils::symbol_opt")]
522 pub platform_deducted_currency: Option<String>,
523 pub history: Vec<OrderHistoryDetail>,
525 pub charge_detail: OrderChargeDetail,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
531pub struct CashInfo {
532 pub withdraw_cash: Decimal,
534 pub available_cash: Decimal,
536 pub frozen_cash: Decimal,
538 pub settling_cash: Decimal,
540 pub currency: String,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct FrozenTransactionFee {
547 pub currency: String,
549 pub frozen_transaction_fee: Decimal,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct AccountBalance {
556 pub total_cash: Decimal,
558 pub max_finance_amount: Decimal,
560 pub remaining_finance_amount: Decimal,
562 #[serde(with = "serde_utils::risk_level")]
564 pub risk_level: i32,
565 pub margin_call: Decimal,
567 pub currency: String,
569 #[serde(default)]
571 pub cash_infos: Vec<CashInfo>,
572 #[serde(with = "serde_utils::decimal_empty_is_0")]
574 pub net_assets: Decimal,
575 #[serde(with = "serde_utils::decimal_empty_is_0")]
577 pub init_margin: Decimal,
578 #[serde(with = "serde_utils::decimal_empty_is_0")]
580 pub maintenance_margin: Decimal,
581 #[serde(with = "serde_utils::decimal_empty_is_0")]
583 pub buy_power: Decimal,
584 pub frozen_transaction_fees: Vec<FrozenTransactionFee>,
586}
587
588#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, IntoPrimitive)]
590#[repr(i32)]
591pub enum BalanceType {
592 #[num_enum(default)]
594 Unknown = 0,
595 Cash = 1,
597 Stock = 2,
599 Fund = 3,
601}
602
603impl Serialize for BalanceType {
604 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
605 let value: i32 = (*self).into();
606 value.serialize(serializer)
607 }
608}
609
610impl<'de> Deserialize<'de> for BalanceType {
611 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
612 let value = i32::deserialize(deserializer)?;
613 Ok(BalanceType::from(value))
614 }
615}
616
617#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, Serialize)]
619#[repr(i32)]
620pub enum CashFlowDirection {
621 #[num_enum(default)]
623 Unknown,
624 Out = 1,
626 In = 2,
628}
629
630impl<'de> Deserialize<'de> for CashFlowDirection {
631 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
632 let value = i32::deserialize(deserializer)?;
633 Ok(CashFlowDirection::from(value))
634 }
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
639pub struct CashFlow {
640 pub transaction_flow_name: String,
642 pub direction: CashFlowDirection,
644 pub business_type: BalanceType,
646 pub balance: Decimal,
648 pub currency: String,
650 #[serde(
652 serialize_with = "time::serde::rfc3339::serialize",
653 deserialize_with = "serde_utils::timestamp::deserialize"
654 )]
655 pub business_time: OffsetDateTime,
656 #[serde(with = "serde_utils::symbol_opt")]
658 pub symbol: Option<String>,
659 pub description: String,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize)]
665pub struct FundPositionsResponse {
666 #[serde(rename = "list")]
668 pub channels: Vec<FundPositionChannel>,
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct FundPositionChannel {
674 pub account_channel: String,
676
677 #[serde(default, rename = "fund_info")]
679 pub positions: Vec<FundPosition>,
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize)]
684pub struct FundPosition {
685 pub symbol: String,
687 #[serde(with = "serde_utils::decimal_empty_is_0")]
689 pub current_net_asset_value: Decimal,
690 #[serde(
692 serialize_with = "time::serde::rfc3339::serialize",
693 deserialize_with = "serde_utils::timestamp::deserialize"
694 )]
695 pub net_asset_value_day: OffsetDateTime,
696 pub symbol_name: String,
698 pub currency: String,
700 #[serde(with = "serde_utils::decimal_empty_is_0")]
702 pub cost_net_asset_value: Decimal,
703 #[serde(with = "serde_utils::decimal_empty_is_0")]
705 pub holding_units: Decimal,
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize)]
710pub struct StockPositionsResponse {
711 #[serde(rename = "list")]
713 pub channels: Vec<StockPositionChannel>,
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct StockPositionChannel {
719 pub account_channel: String,
721
722 #[serde(default, rename = "stock_info")]
724 pub positions: Vec<StockPosition>,
725}
726
727#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct StockPosition {
730 pub symbol: String,
732 pub symbol_name: String,
734 pub quantity: Decimal,
736 pub available_quantity: Decimal,
738 pub currency: String,
740 #[serde(with = "serde_utils::decimal_empty_is_0")]
743 pub cost_price: Decimal,
744 pub market: Market,
746 #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
748 pub init_quantity: Option<Decimal>,
749}
750
751#[derive(Debug, Clone, Serialize, Deserialize)]
753pub struct MarginRatio {
754 pub im_factor: Decimal,
756 pub mm_factor: Decimal,
758 pub fm_factor: Decimal,
760}
761
762impl_serde_for_enum_string!(
763 OrderType,
764 OrderStatus,
765 OrderSide,
766 TriggerPriceType,
767 OrderTag,
768 TimeInForceType,
769 TriggerStatus,
770 OutsideRTH,
771 CommissionFreeStatus,
772 DeductionStatus,
773 ChargeCategoryCode
774);
775impl_default_for_enum_string!(
776 OrderType,
777 OrderStatus,
778 OrderSide,
779 TriggerPriceType,
780 OrderTag,
781 TimeInForceType,
782 TriggerStatus,
783 OutsideRTH,
784 CommissionFreeStatus,
785 DeductionStatus,
786 ChargeCategoryCode
787);
788
789#[cfg(test)]
790mod tests {
791 use time::macros::datetime;
792
793 use super::*;
794
795 #[test]
796 fn fund_position_response() {
797 let data = r#"
798 {
799 "list": [{
800 "account_channel": "lb",
801 "fund_info": [{
802 "symbol": "HK0000447943",
803 "symbol_name": "高腾亚洲收益基金",
804 "currency": "USD",
805 "holding_units": "5.000",
806 "current_net_asset_value": "0",
807 "cost_net_asset_value": "0.00",
808 "net_asset_value_day": "1649865600"
809 }]
810 }]
811 }
812 "#;
813
814 let resp: FundPositionsResponse = serde_json::from_str(data).unwrap();
815 assert_eq!(resp.channels.len(), 1);
816
817 let channel = &resp.channels[0];
818 assert_eq!(channel.account_channel, "lb");
819 assert_eq!(channel.positions.len(), 1);
820
821 let position = &channel.positions[0];
822 assert_eq!(position.symbol, "HK0000447943");
823 assert_eq!(position.symbol_name, "高腾亚洲收益基金");
824 assert_eq!(position.currency, "USD");
825 assert_eq!(position.current_net_asset_value, decimal!(0i32));
826 assert_eq!(position.cost_net_asset_value, decimal!(0i32));
827 assert_eq!(position.holding_units, decimal!(5i32));
828 assert_eq!(position.net_asset_value_day, datetime!(2022-4-14 0:00 +8));
829 }
830
831 #[test]
832 fn stock_position_response() {
833 let data = r#"
834 {
835 "list": [
836 {
837 "account_channel": "lb",
838 "stock_info": [
839 {
840 "symbol": "700.HK",
841 "symbol_name": "腾讯控股",
842 "currency": "HK",
843 "quantity": "650",
844 "available_quantity": "-450",
845 "cost_price": "457.53",
846 "market": "HK",
847 "init_quantity": "2000"
848 },
849 {
850 "symbol": "9991.HK",
851 "symbol_name": "宝尊电商-SW",
852 "currency": "HK",
853 "quantity": "200",
854 "available_quantity": "0",
855 "cost_price": "32.25",
856 "market": "HK",
857 "init_quantity": ""
858 }
859 ]
860 }
861 ]
862 }
863 "#;
864
865 let resp: StockPositionsResponse = serde_json::from_str(data).unwrap();
866 assert_eq!(resp.channels.len(), 1);
867
868 let channel = &resp.channels[0];
869 assert_eq!(channel.account_channel, "lb");
870 assert_eq!(channel.positions.len(), 2);
871
872 let position = &channel.positions[0];
873 assert_eq!(position.symbol, "700.HK");
874 assert_eq!(position.symbol_name, "腾讯控股");
875 assert_eq!(position.currency, "HK");
876 assert_eq!(position.quantity, decimal!(650));
877 assert_eq!(position.available_quantity, decimal!(-450));
878 assert_eq!(position.cost_price, decimal!(457.53f32));
879 assert_eq!(position.market, Market::HK);
880 assert_eq!(position.init_quantity, Some(decimal!(2000)));
881
882 let position = &channel.positions[0];
883 assert_eq!(position.symbol, "700.HK");
884 assert_eq!(position.symbol_name, "腾讯控股");
885 assert_eq!(position.currency, "HK");
886 assert_eq!(position.quantity, decimal!(650));
887 assert_eq!(position.available_quantity, decimal!(-450));
888 assert_eq!(position.cost_price, decimal!(457.53f32));
889 assert_eq!(position.market, Market::HK);
890
891 let position = &channel.positions[1];
892 assert_eq!(position.symbol, "9991.HK");
893 assert_eq!(position.symbol_name, "宝尊电商-SW");
894 assert_eq!(position.currency, "HK");
895 assert_eq!(position.quantity, decimal!(200));
896 assert_eq!(position.available_quantity, decimal!(0));
897 assert_eq!(position.cost_price, decimal!(32.25f32));
898 assert_eq!(position.init_quantity, None);
899 }
900
901 #[test]
902 fn cash_flow() {
903 let data = r#"
904 {
905 "list": [
906 {
907 "transaction_flow_name": "BuyContract-Stocks",
908 "direction": 1,
909 "balance": "-248.60",
910 "currency": "USD",
911 "business_type": 1,
912 "business_time": "1621507957",
913 "symbol": "AAPL.US",
914 "description": "AAPL"
915 },
916 {
917 "transaction_flow_name": "BuyContract-Stocks",
918 "direction": 1,
919 "balance": "-125.16",
920 "currency": "USD",
921 "business_type": 2,
922 "business_time": "1621504824",
923 "symbol": "AAPL.US",
924 "description": "AAPL"
925 }
926 ]
927 }
928 "#;
929
930 #[derive(Debug, Deserialize)]
931 struct Response {
932 list: Vec<CashFlow>,
933 }
934
935 let resp: Response = serde_json::from_str(data).unwrap();
936 assert_eq!(resp.list.len(), 2);
937
938 let cashflow = &resp.list[0];
939 assert_eq!(cashflow.transaction_flow_name, "BuyContract-Stocks");
940 assert_eq!(cashflow.direction, CashFlowDirection::Out);
941 assert_eq!(cashflow.balance, decimal!(-248.60f32));
942 assert_eq!(cashflow.currency, "USD");
943 assert_eq!(cashflow.business_type, BalanceType::Cash);
944 assert_eq!(cashflow.business_time, datetime!(2021-05-20 18:52:37 +8));
945 assert_eq!(cashflow.symbol.as_deref(), Some("AAPL.US"));
946 assert_eq!(cashflow.description, "AAPL");
947
948 let cashflow = &resp.list[1];
949 assert_eq!(cashflow.transaction_flow_name, "BuyContract-Stocks");
950 assert_eq!(cashflow.direction, CashFlowDirection::Out);
951 assert_eq!(cashflow.balance, decimal!(-125.16f32));
952 assert_eq!(cashflow.currency, "USD");
953 assert_eq!(cashflow.business_type, BalanceType::Stock);
954 assert_eq!(cashflow.business_time, datetime!(2021-05-20 18:00:24 +8));
955 assert_eq!(cashflow.symbol.as_deref(), Some("AAPL.US"));
956 assert_eq!(cashflow.description, "AAPL");
957 }
958
959 #[test]
960 fn account_balance() {
961 let data = r#"
962 {
963 "list": [
964 {
965 "total_cash": "1759070010.72",
966 "max_finance_amount": "977582000",
967 "remaining_finance_amount": "0",
968 "risk_level": "1",
969 "margin_call": "2598051051.50",
970 "currency": "HKD",
971 "cash_infos": [
972 {
973 "withdraw_cash": "97592.30",
974 "available_cash": "195902464.37",
975 "frozen_cash": "11579339.13",
976 "settling_cash": "207288537.81",
977 "currency": "HKD"
978 },
979 {
980 "withdraw_cash": "199893416.74",
981 "available_cash": "199893416.74",
982 "frozen_cash": "28723.76",
983 "settling_cash": "-276806.51",
984 "currency": "USD"
985 }
986 ],
987 "net_assets": "11111.12",
988 "init_margin": "2222.23",
989 "maintenance_margin": "3333.45",
990 "buy_power": "1234.67",
991 "frozen_transaction_fees": [
992 {
993 "currency": "HKD",
994 "frozen_transaction_fee": "123"
995 }
996 ]
997 }
998 ]
999 }"#;
1000
1001 #[derive(Debug, Deserialize)]
1002 struct Response {
1003 list: Vec<AccountBalance>,
1004 }
1005
1006 let resp: Response = serde_json::from_str(data).unwrap();
1007 assert_eq!(resp.list.len(), 1);
1008
1009 let balance = &resp.list[0];
1010 assert_eq!(balance.total_cash, "1759070010.72".parse().unwrap());
1011 assert_eq!(balance.max_finance_amount, "977582000".parse().unwrap());
1012 assert_eq!(balance.remaining_finance_amount, decimal!(0i32));
1013 assert_eq!(balance.risk_level, 1);
1014 assert_eq!(balance.margin_call, "2598051051.50".parse().unwrap());
1015 assert_eq!(balance.currency, "HKD");
1016 assert_eq!(balance.net_assets, "11111.12".parse().unwrap());
1017 assert_eq!(balance.init_margin, "2222.23".parse().unwrap());
1018 assert_eq!(balance.maintenance_margin, "3333.45".parse().unwrap());
1019 assert_eq!(balance.buy_power, "1234.67".parse().unwrap());
1020
1021 assert_eq!(balance.cash_infos.len(), 2);
1022
1023 let cash_info = &balance.cash_infos[0];
1024 assert_eq!(cash_info.withdraw_cash, "97592.30".parse().unwrap());
1025 assert_eq!(cash_info.available_cash, "195902464.37".parse().unwrap());
1026 assert_eq!(cash_info.frozen_cash, "11579339.13".parse().unwrap());
1027 assert_eq!(cash_info.settling_cash, "207288537.81".parse().unwrap());
1028 assert_eq!(cash_info.currency, "HKD");
1029
1030 let cash_info = &balance.cash_infos[1];
1031 assert_eq!(cash_info.withdraw_cash, "199893416.74".parse().unwrap());
1032 assert_eq!(cash_info.available_cash, "199893416.74".parse().unwrap());
1033 assert_eq!(cash_info.frozen_cash, "28723.76".parse().unwrap());
1034 assert_eq!(cash_info.settling_cash, "-276806.51".parse().unwrap());
1035 assert_eq!(cash_info.currency, "USD");
1036
1037 assert_eq!(balance.frozen_transaction_fees.len(), 1);
1038
1039 let frozen_transaction_fee = &balance.frozen_transaction_fees[0];
1040 assert_eq!(frozen_transaction_fee.currency, "HKD");
1041 assert_eq!(
1042 frozen_transaction_fee.frozen_transaction_fee,
1043 "123".parse().unwrap()
1044 );
1045 }
1046
1047 #[test]
1048 fn history_orders() {
1049 let data = r#"
1050 {
1051 "orders": [
1052 {
1053 "currency": "HKD",
1054 "executed_price": "0.000",
1055 "executed_quantity": "0",
1056 "expire_date": "",
1057 "last_done": "",
1058 "limit_offset": "",
1059 "msg": "",
1060 "order_id": "706388312699592704",
1061 "order_type": "ELO",
1062 "outside_rth": "UnknownOutsideRth",
1063 "price": "11.900",
1064 "quantity": "200",
1065 "side": "Buy",
1066 "status": "RejectedStatus",
1067 "stock_name": "Bank of East Asia Ltd/The",
1068 "submitted_at": "1651644897",
1069 "symbol": "23.HK",
1070 "tag": "Normal",
1071 "time_in_force": "Day",
1072 "trailing_amount": "",
1073 "trailing_percent": "",
1074 "trigger_at": "0",
1075 "trigger_price": "",
1076 "trigger_status": "NOT_USED",
1077 "updated_at": "1651644898",
1078 "remark": "abc"
1079 }
1080 ]
1081 }
1082 "#;
1083
1084 #[derive(Deserialize)]
1085 struct Response {
1086 orders: Vec<Order>,
1087 }
1088
1089 let resp: Response = serde_json::from_str(data).unwrap();
1090 assert_eq!(resp.orders.len(), 1);
1091
1092 let order = &resp.orders[0];
1093 assert_eq!(order.currency, "HKD");
1094 assert!(order.executed_price.is_none());
1095 assert_eq!(order.executed_quantity, decimal!(0));
1096 assert!(order.expire_date.is_none());
1097 assert!(order.last_done.is_none());
1098 assert!(order.limit_offset.is_none());
1099 assert_eq!(order.msg, "");
1100 assert_eq!(order.order_id, "706388312699592704");
1101 assert_eq!(order.order_type, OrderType::ELO);
1102 assert!(order.outside_rth.is_none());
1103 assert_eq!(order.price, Some("11.900".parse().unwrap()));
1104 assert_eq!(order.quantity, decimal!(200));
1105 assert_eq!(order.side, OrderSide::Buy);
1106 assert_eq!(order.status, OrderStatus::Rejected);
1107 assert_eq!(order.stock_name, "Bank of East Asia Ltd/The");
1108 assert_eq!(order.submitted_at, datetime!(2022-05-04 14:14:57 +8));
1109 assert_eq!(order.symbol, "23.HK");
1110 assert_eq!(order.tag, OrderTag::Normal);
1111 assert_eq!(order.time_in_force, TimeInForceType::Day);
1112 assert!(order.trailing_amount.is_none());
1113 assert!(order.trailing_percent.is_none());
1114 assert!(order.trigger_at.is_none());
1115 assert!(order.trigger_price.is_none());
1116 assert!(order.trigger_status.is_none());
1117 assert_eq!(order.updated_at, Some(datetime!(2022-05-04 14:14:58 +8)));
1118 assert_eq!(order.remark, "abc");
1119 }
1120
1121 #[test]
1122 fn today_orders() {
1123 let data = r#"
1124 {
1125 "orders": [
1126 {
1127 "currency": "HKD",
1128 "executed_price": "0.000",
1129 "executed_quantity": "0",
1130 "expire_date": "",
1131 "last_done": "",
1132 "limit_offset": "",
1133 "msg": "",
1134 "order_id": "706388312699592704",
1135 "order_type": "ELO",
1136 "outside_rth": "UnknownOutsideRth",
1137 "price": "11.900",
1138 "quantity": "200",
1139 "side": "Buy",
1140 "status": "RejectedStatus",
1141 "stock_name": "Bank of East Asia Ltd/The",
1142 "submitted_at": "1651644897",
1143 "symbol": "23.HK",
1144 "tag": "Normal",
1145 "time_in_force": "Day",
1146 "trailing_amount": "",
1147 "trailing_percent": "",
1148 "trigger_at": "0",
1149 "trigger_price": "",
1150 "trigger_status": "NOT_USED",
1151 "updated_at": "1651644898",
1152 "remark": "abc"
1153 }
1154 ]
1155 }
1156 "#;
1157
1158 #[derive(Deserialize)]
1159 struct Response {
1160 orders: Vec<Order>,
1161 }
1162
1163 let resp: Response = serde_json::from_str(data).unwrap();
1164 assert_eq!(resp.orders.len(), 1);
1165
1166 let order = &resp.orders[0];
1167 assert_eq!(order.currency, "HKD");
1168 assert!(order.executed_price.is_none());
1169 assert_eq!(order.executed_quantity, decimal!(0));
1170 assert!(order.expire_date.is_none());
1171 assert!(order.last_done.is_none());
1172 assert!(order.limit_offset.is_none());
1173 assert_eq!(order.msg, "");
1174 assert_eq!(order.order_id, "706388312699592704");
1175 assert_eq!(order.order_type, OrderType::ELO);
1176 assert!(order.outside_rth.is_none());
1177 assert_eq!(order.price, Some("11.900".parse().unwrap()));
1178 assert_eq!(order.quantity, decimal!(200));
1179 assert_eq!(order.side, OrderSide::Buy);
1180 assert_eq!(order.status, OrderStatus::Rejected);
1181 assert_eq!(order.stock_name, "Bank of East Asia Ltd/The");
1182 assert_eq!(order.submitted_at, datetime!(2022-05-04 14:14:57 +8));
1183 assert_eq!(order.symbol, "23.HK");
1184 assert_eq!(order.tag, OrderTag::Normal);
1185 assert_eq!(order.time_in_force, TimeInForceType::Day);
1186 assert!(order.trailing_amount.is_none());
1187 assert!(order.trailing_percent.is_none());
1188 assert!(order.trigger_at.is_none());
1189 assert!(order.trigger_price.is_none());
1190 assert!(order.trigger_status.is_none());
1191 assert_eq!(order.updated_at, Some(datetime!(2022-05-04 14:14:58 +8)));
1192 assert_eq!(order.remark, "abc");
1193 }
1194
1195 #[test]
1196 fn history_executions() {
1197 let data = r#"
1198 {
1199 "has_more": false,
1200 "trades": [
1201 {
1202 "order_id": "693664675163312128",
1203 "price": "388",
1204 "quantity": "100",
1205 "symbol": "700.HK",
1206 "trade_done_at": "1648611351",
1207 "trade_id": "693664675163312128-1648611351433741210"
1208 }
1209 ]
1210 }
1211 "#;
1212
1213 #[derive(Deserialize)]
1214 struct Response {
1215 trades: Vec<Execution>,
1216 }
1217
1218 let resp: Response = serde_json::from_str(data).unwrap();
1219 assert_eq!(resp.trades.len(), 1);
1220
1221 let execution = &resp.trades[0];
1222 assert_eq!(execution.order_id, "693664675163312128");
1223 assert_eq!(execution.price, "388".parse().unwrap());
1224 assert_eq!(execution.quantity, decimal!(100));
1225 assert_eq!(execution.symbol, "700.HK");
1226 assert_eq!(execution.trade_done_at, datetime!(2022-03-30 11:35:51 +8));
1227 assert_eq!(execution.trade_id, "693664675163312128-1648611351433741210");
1228 }
1229
1230 #[test]
1231 fn order_detail() {
1232 let data = r#"
1233 {
1234 "order_id": "828940451093708800",
1235 "status": "FilledStatus",
1236 "stock_name": "Apple",
1237 "quantity": "10",
1238 "executed_quantity": "10",
1239 "price": "200.000",
1240 "executed_price": "164.660",
1241 "submitted_at": "1680863604",
1242 "side": "Buy",
1243 "symbol": "AAPL.US",
1244 "order_type": "LO",
1245 "last_done": "164.660",
1246 "trigger_price": "0.0000",
1247 "msg": "",
1248 "tag": "Normal",
1249 "time_in_force": "Day",
1250 "expire_date": "2023-04-10",
1251 "updated_at": "1681113000",
1252 "trigger_at": "0",
1253 "trailing_amount": "",
1254 "trailing_percent": "",
1255 "limit_offset": "",
1256 "trigger_status": "NOT_USED",
1257 "outside_rth": "ANY_TIME",
1258 "currency": "USD",
1259 "remark": "1680863603.927165",
1260 "free_status": "None",
1261 "free_amount": "",
1262 "free_currency": "",
1263 "deductions_status": "NONE",
1264 "deductions_amount": "",
1265 "deductions_currency": "",
1266 "platform_deducted_status": "NONE",
1267 "platform_deducted_amount": "",
1268 "platform_deducted_currency": "",
1269 "history": [{
1270 "price": "164.6600",
1271 "quantity": "10",
1272 "status": "FilledStatus",
1273 "msg": "Execution of 10",
1274 "time": "1681113000"
1275 }, {
1276 "price": "200.0000",
1277 "quantity": "10",
1278 "status": "NewStatus",
1279 "msg": "",
1280 "time": "1681113000"
1281 }],
1282 "charge_detail": {
1283 "items": [{
1284 "code": "BROKER_FEES",
1285 "name": "Broker Fees",
1286 "fees": []
1287 }, {
1288 "code": "THIRD_FEES",
1289 "name": "Third-party Fees",
1290 "fees": []
1291 }],
1292 "total_amount": "0",
1293 "currency": "USD"
1294 }
1295 }
1296 "#;
1297
1298 _ = serde_json::from_str::<OrderDetail>(data).unwrap();
1299 }
1300}