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