Skip to main content

longbridge/trade/
types.rs

1use std::collections::BTreeMap;
2
3use num_enum::{FromPrimitive, IntoPrimitive};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use strum_macros::{Display, EnumString};
7use time::{Date, OffsetDateTime};
8
9use crate::{Market, serde_utils};
10
11/// Order type
12#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
13#[allow(clippy::upper_case_acronyms)]
14pub enum OrderType {
15    /// Unknown
16    Unknown,
17    /// Limit Order
18    #[strum(serialize = "LO")]
19    LO,
20    /// Enhanced Limit Order
21    #[strum(serialize = "ELO")]
22    ELO,
23    /// Market Order
24    #[strum(serialize = "MO")]
25    MO,
26    /// At-auction Order
27    #[strum(serialize = "AO")]
28    AO,
29    /// At-auction Limit Order
30    #[strum(serialize = "ALO")]
31    ALO,
32    /// Odd Lots
33    #[strum(serialize = "ODD")]
34    ODD,
35    /// Limit If Touched
36    #[strum(serialize = "LIT")]
37    LIT,
38    /// Market If Touched
39    #[strum(serialize = "MIT")]
40    MIT,
41    /// Trailing Limit If Touched (Trailing Amount)
42    #[strum(serialize = "TSLPAMT")]
43    TSLPAMT,
44    /// Trailing Limit If Touched (Trailing Percent)
45    #[strum(serialize = "TSLPPCT")]
46    TSLPPCT,
47    /// Trailing Market If Touched (Trailing Amount)
48    #[strum(serialize = "TSMAMT")]
49    TSMAMT,
50    /// Trailing Market If Touched (Trailing Percent)
51    #[strum(serialize = "TSMPCT")]
52    TSMPCT,
53    /// Special Limit Order
54    #[strum(serialize = "SLO")]
55    SLO,
56}
57
58/// Order status
59#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
60pub enum OrderStatus {
61    /// Unknown
62    Unknown,
63    /// Not reported
64    #[strum(serialize = "NotReported")]
65    NotReported,
66    /// Not reported (Replaced Order)
67    #[strum(serialize = "ReplacedNotReported")]
68    ReplacedNotReported,
69    /// Not reported (Protected Order)
70    #[strum(serialize = "ProtectedNotReported")]
71    ProtectedNotReported,
72    /// Not reported (Conditional Order)
73    #[strum(serialize = "VarietiesNotReported")]
74    VarietiesNotReported,
75    /// Filled
76    #[strum(serialize = "FilledStatus")]
77    Filled,
78    /// Wait To New
79    #[strum(serialize = "WaitToNew")]
80    WaitToNew,
81    /// New
82    #[strum(serialize = "NewStatus")]
83    New,
84    /// Wait To Replace
85    #[strum(serialize = "WaitToReplace")]
86    WaitToReplace,
87    /// Pending Replace
88    #[strum(serialize = "PendingReplaceStatus")]
89    PendingReplace,
90    /// Replaced
91    #[strum(serialize = "ReplacedStatus")]
92    Replaced,
93    /// Partial Filled
94    #[strum(serialize = "PartialFilledStatus")]
95    PartialFilled,
96    /// Wait To Cancel
97    #[strum(serialize = "WaitToCancel")]
98    WaitToCancel,
99    /// Pending Cancel
100    #[strum(serialize = "PendingCancelStatus")]
101    PendingCancel,
102    /// Rejected
103    #[strum(serialize = "RejectedStatus")]
104    Rejected,
105    /// Canceled
106    #[strum(serialize = "CanceledStatus")]
107    Canceled,
108    /// Expired
109    #[strum(serialize = "ExpiredStatus")]
110    Expired,
111    /// Partial Withdrawal
112    #[strum(serialize = "PartialWithdrawal")]
113    PartialWithdrawal,
114    /// Monitoring (Time-Conditional Order)
115    #[strum(serialize = "DelayedNotReported")]
116    DelayedNotReported,
117}
118
119/// Execution
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct Execution {
122    /// Order ID
123    pub order_id: String,
124    /// Execution ID
125    pub trade_id: String,
126    /// Security code
127    pub symbol: String,
128    /// Trade done time
129    #[serde(
130        serialize_with = "time::serde::rfc3339::serialize",
131        deserialize_with = "serde_utils::timestamp::deserialize"
132    )]
133    pub trade_done_at: OffsetDateTime,
134    /// Executed quantity
135    pub quantity: Decimal,
136    /// Executed price
137    pub price: Decimal,
138    /// Order side. `Unknown` when the response omits it, as
139    /// `/v1/trade/execution/history` does for Longbridge US accounts.
140    #[serde(default)]
141    pub side: OrderSide,
142}
143
144/// Response for get all executions request
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct AllExecutionsResponse {
147    /// Has more records
148    pub has_more: bool,
149    /// Execution list
150    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
151    pub trades: Vec<Execution>,
152}
153
154/// Order side
155#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
156pub enum OrderSide {
157    /// Unknown
158    Unknown,
159    /// Buy
160    #[strum(serialize = "Buy")]
161    Buy,
162    /// Sell
163    #[strum(serialize = "Sell")]
164    Sell,
165}
166
167/// Order trigger price type
168#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
169pub enum TriggerPriceType {
170    /// Unknown
171    Unknown,
172    /// Limit If Touched
173    #[strum(serialize = "LIT")]
174    LimitIfTouched,
175    /// Market If Touched
176    #[strum(serialize = "MIT")]
177    MarketIfTouched,
178}
179
180/// Order tag
181#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
182pub enum OrderTag {
183    /// Unknown
184    Unknown,
185    /// Normal Order
186    #[strum(serialize = "Normal")]
187    Normal,
188    /// Long term Order
189    #[strum(serialize = "Gtc")]
190    LongTerm,
191    /// Grey Order
192    #[strum(serialize = "Grey")]
193    Grey,
194}
195
196/// Time in force Type
197#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
198pub enum TimeInForceType {
199    /// Unknown
200    Unknown,
201    /// Day Order
202    #[strum(serialize = "Day")]
203    Day,
204    /// Good Til Canceled Order
205    #[strum(serialize = "GTC")]
206    GoodTilCanceled,
207    /// Good Til Date Order
208    #[strum(serialize = "GTD")]
209    GoodTilDate,
210}
211
212/// Trigger status
213#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
214pub enum TriggerStatus {
215    /// Unknown
216    Unknown,
217    /// Deactive
218    #[strum(serialize = "DEACTIVE")]
219    Deactive,
220    /// Active
221    #[strum(serialize = "ACTIVE")]
222    Active,
223    /// Released
224    #[strum(serialize = "RELEASED")]
225    Released,
226}
227
228/// Enable or disable outside regular trading hours
229#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
230pub enum OutsideRTH {
231    /// Unknown
232    Unknown,
233    /// Regular trading hour only
234    #[strum(serialize = "RTH_ONLY")]
235    RTHOnly,
236    /// Any time
237    #[strum(serialize = "ANY_TIME")]
238    AnyTime,
239    /// Overnight
240    #[strum(serialize = "OVERNIGHT")]
241    Overnight,
242    /// Overnight option
243    #[strum(serialize = "OPTION_PRE_MARKET")]
244    OptionPreMarket,
245}
246
247/// Attached order type
248#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
249pub enum AttachedOrderType {
250    /// Unknown
251    Unknown,
252    /// Take profit
253    #[strum(serialize = "PROFIT_TAKER")]
254    ProfitTaker,
255    /// Stop loss
256    #[strum(serialize = "STOP_LOSS")]
257    StopLoss,
258    /// Bracket order
259    #[strum(serialize = "BRACKET")]
260    Bracket,
261}
262
263/// Attached order detail
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct AttachedOrderDetail {
266    /// Attached order ID
267    pub order_id: String,
268    /// Attached order type
269    pub attached_type_display: AttachedOrderType,
270    /// Trigger price
271    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
272    pub trigger_price: Option<Decimal>,
273    /// Quantity
274    pub quantity: Decimal,
275    /// Executed quantity
276    pub executed_qty: Decimal,
277    /// Order status
278    pub status: OrderStatus,
279    /// Last updated time (unix timestamp seconds)
280    #[serde(
281        serialize_with = "time::serde::rfc3339::serialize",
282        deserialize_with = "serde_utils::timestamp::deserialize"
283    )]
284    pub updated_at: OffsetDateTime,
285    /// Whether withdrawn
286    pub withdrawn: bool,
287    /// GTD date
288    #[serde(with = "serde_utils::date_opt")]
289    pub gtd: Option<Date>,
290    /// Time in force
291    pub time_in_force: TimeInForceType,
292    /// Counter order ID
293    pub counter_id: String,
294    /// Trigger status
295    #[serde(with = "serde_utils::trigger_status")]
296    pub trigger_status: Option<TriggerStatus>,
297    /// Executed amount
298    pub executed_amount: Decimal,
299    /// Tag
300    pub tag: OrderTag,
301    /// Submitted time (unix timestamp seconds)
302    #[serde(
303        serialize_with = "time::serde::rfc3339::serialize",
304        deserialize_with = "serde_utils::timestamp::deserialize"
305    )]
306    pub submitted_at: OffsetDateTime,
307    /// Executed price
308    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
309    pub executed_price: Option<Decimal>,
310    /// Force RTH only
311    #[serde(with = "serde_utils::outside_rth")]
312    pub force_only_rth: Option<OutsideRTH>,
313    /// Whether reviewed
314    pub reviewed: bool,
315    /// Order type to submit after trigger
316    pub activate_order_type: OrderType,
317    /// RTH setting for activated order
318    #[serde(with = "serde_utils::outside_rth")]
319    pub activate_rth: Option<OutsideRTH>,
320    /// Submit price (limit price)
321    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
322    pub submit_price: Option<Decimal>,
323    /// Bid/ask depth level of the limit order submitted price
324    #[serde(
325        default,
326        deserialize_with = "serde_utils::int32_opt_null_or_0_is_none::deserialize",
327        serialize_with = "serde_utils::int32_opt_0_is_none::serialize"
328    )]
329    pub limit_depth_level: Option<i32>,
330}
331
332/// Multi-leg strategy
333#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
334pub enum MultiLegStrategy {
335    /// Unknown
336    Unknown,
337    /// Covered call (covered stock)
338    #[strum(to_string = "CoveredCall", serialize = "0")]
339    CoveredCall,
340    /// Covered put (covered stock)
341    #[strum(to_string = "CoveredPut", serialize = "1")]
342    CoveredPut,
343    /// Vertical call spread
344    #[strum(to_string = "VerticalCallSpread", serialize = "2")]
345    VerticalCallSpread,
346    /// Vertical put spread
347    #[strum(to_string = "VerticalPutSpread", serialize = "3")]
348    VerticalPutSpread,
349    /// Collar
350    #[strum(to_string = "Collar", serialize = "4")]
351    Collar,
352    /// Straddle
353    #[strum(to_string = "Straddle", serialize = "5")]
354    Straddle,
355    /// Strangle
356    #[strum(to_string = "Strangle", serialize = "6")]
357    Strangle,
358    /// Calendar call spread
359    #[strum(to_string = "CalendarCallSpread", serialize = "7")]
360    CalendarCallSpread,
361    /// Calendar put spread
362    #[strum(to_string = "CalendarPutSpread", serialize = "8")]
363    CalendarPutSpread,
364}
365
366/// Multi-leg position direction
367#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
368pub enum MultiLegPosition {
369    /// Unknown
370    Unknown,
371    /// Long
372    #[strum(serialize = "LONG")]
373    Long,
374    /// Short
375    #[strum(serialize = "SHORT")]
376    Short,
377}
378
379/// Option contract type
380#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
381pub enum ContractDirection {
382    /// Unknown
383    Unknown,
384    /// Call
385    #[strum(serialize = "C")]
386    Call,
387    /// Put
388    #[strum(serialize = "P")]
389    Put,
390}
391
392/// A leg of a multi-leg combination order
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct MultiLegOrderLeg {
395    /// Option symbol, in `ticker.region` format
396    pub symbol: String,
397    /// Order side
398    pub side: OrderSide,
399    /// Position direction
400    pub position: MultiLegPosition,
401    /// Leg ratio quantity
402    #[serde(with = "serde_utils::decimal_empty_is_0")]
403    pub ratio_quantity: Decimal,
404    /// Strike price
405    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
406    pub strike_price: Option<Decimal>,
407    /// Option expiry date
408    #[serde(default, with = "serde_utils::date_ymd_opt")]
409    pub expire_date: Option<Date>,
410    /// Contract type
411    pub contract_direction: ContractDirection,
412}
413
414/// Multi-leg strategy information
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct MultiLegInfo {
417    /// Multi-leg strategy
418    pub strategy: MultiLegStrategy,
419    /// Strategy name
420    pub strategy_name: String,
421    /// Multi-leg combination ID
422    pub multileg_id: String,
423    /// Multi-leg combination code
424    pub code: String,
425    /// Legs of the combination order
426    #[serde(default)]
427    pub legs: Vec<MultiLegOrderLeg>,
428}
429
430/// Order
431#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct Order {
433    /// Order ID
434    pub order_id: String,
435    /// Order status
436    pub status: OrderStatus,
437    /// Stock name
438    pub stock_name: String,
439    /// Submitted quantity
440    pub quantity: Decimal,
441    /// Executed quantity
442    pub executed_quantity: Decimal,
443    /// Submitted price
444    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
445    pub price: Option<Decimal>,
446    /// Executed price
447    #[serde(with = "serde_utils::decimal_opt_0_is_none")]
448    pub executed_price: Option<Decimal>,
449    /// Submitted time
450    #[serde(
451        serialize_with = "time::serde::rfc3339::serialize",
452        deserialize_with = "serde_utils::timestamp::deserialize"
453    )]
454    pub submitted_at: OffsetDateTime,
455    /// Order side
456    pub side: OrderSide,
457    /// Security code
458    pub symbol: String,
459    /// Order type
460    pub order_type: OrderType,
461    /// Last done
462    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
463    pub last_done: Option<Decimal>,
464    /// `LIT` / `MIT` Order Trigger Price
465    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
466    pub trigger_price: Option<Decimal>,
467    /// Rejected Message or remark
468    pub msg: String,
469    /// Order tag
470    pub tag: OrderTag,
471    /// Time in force type
472    pub time_in_force: TimeInForceType,
473    /// Long term order expire date
474    #[serde(with = "serde_utils::date_opt")]
475    pub expire_date: Option<Date>,
476    /// Last updated time
477    #[serde(
478        deserialize_with = "serde_utils::timestamp_opt::deserialize",
479        serialize_with = "serde_utils::rfc3339_opt::serialize"
480    )]
481    pub updated_at: Option<OffsetDateTime>,
482    /// Conditional order trigger time
483    #[serde(
484        deserialize_with = "serde_utils::timestamp_opt::deserialize",
485        serialize_with = "serde_utils::rfc3339_opt::serialize"
486    )]
487    pub trigger_at: Option<OffsetDateTime>,
488    /// `TSMAMT` / `TSLPAMT` order trailing amount
489    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
490    pub trailing_amount: Option<Decimal>,
491    /// `TSMPCT` / `TSLPPCT` order trailing percent
492    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
493    pub trailing_percent: Option<Decimal>,
494    /// `TSLPAMT` / `TSLPPCT` order limit offset amount
495    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
496    pub limit_offset: Option<Decimal>,
497    /// Conditional order trigger status
498    #[serde(with = "serde_utils::trigger_status")]
499    pub trigger_status: Option<TriggerStatus>,
500    /// Currency
501    pub currency: String,
502    /// Enable or disable outside regular trading hours
503    #[serde(with = "serde_utils::outside_rth")]
504    pub outside_rth: Option<OutsideRTH>,
505    /// Limit depth level
506    #[serde(with = "serde_utils::int32_opt_0_is_none")]
507    pub limit_depth_level: Option<i32>,
508    /// Trigger count
509    #[serde(with = "serde_utils::int32_opt_0_is_none")]
510    pub trigger_count: Option<i32>,
511    /// Monitor price
512    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
513    pub monitor_price: Option<Decimal>,
514    /// Remark
515    pub remark: String,
516    /// Time-condition submit time (`None` when the order is not a
517    /// time-condition order)
518    #[serde(
519        default,
520        deserialize_with = "serde_utils::timestamp_opt_lenient::deserialize",
521        serialize_with = "serde_utils::rfc3339_opt::serialize"
522    )]
523    pub delayed_until: Option<OffsetDateTime>,
524    /// Attached orders
525    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
526    pub attached_orders: Vec<AttachedOrderDetail>,
527    /// Multi-leg strategy information (only present for multi-leg option
528    /// combination orders)
529    #[serde(default)]
530    pub multi_leg: Option<MultiLegInfo>,
531}
532
533/// Commission-free Status
534#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
535pub enum CommissionFreeStatus {
536    /// Unknown
537    Unknown,
538    /// None
539    None,
540    /// Commission-free amount to be calculated
541    Calculated,
542    /// Pending commission-free
543    Pending,
544    /// Commission-free applied
545    Ready,
546}
547
548/// Deduction status
549#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
550pub enum DeductionStatus {
551    /// Unknown
552    Unknown,
553    /// Pending Settlement
554    #[strum(serialize = "NONE")]
555    None,
556    /// Settled with no data
557    #[strum(serialize = "NO_DATA")]
558    NoData,
559    /// Settled and pending distribution
560    #[strum(serialize = "PENDING")]
561    Pending,
562    /// Settled and distributed
563    #[strum(serialize = "DONE")]
564    Done,
565}
566
567/// Charge category code
568#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
569pub enum ChargeCategoryCode {
570    /// Unknown
571    Unknown,
572    /// Broker
573    #[strum(serialize = "BROKER_FEES")]
574    Broker,
575    /// Third
576    #[strum(serialize = "THIRD_FEES")]
577    Third,
578}
579
580/// Order history detail
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct OrderHistoryDetail {
583    /// Executed price for executed orders, submitted price for expired,
584    /// canceled, rejected orders, etc.
585    #[serde(with = "serde_utils::decimal_empty_is_0")]
586    pub price: Decimal,
587    /// Executed quantity for executed orders, remaining quantity for expired,
588    /// canceled, rejected orders, etc.
589    #[serde(with = "serde_utils::decimal_empty_is_0")]
590    pub quantity: Decimal,
591    /// Order status
592    pub status: OrderStatus,
593    /// Execution or error message
594    pub msg: String,
595    /// Occurrence time
596    #[serde(
597        serialize_with = "time::serde::rfc3339::serialize",
598        deserialize_with = "serde_utils::timestamp::deserialize"
599    )]
600    pub time: OffsetDateTime,
601}
602
603/// Order charge fee
604#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct OrderChargeFee {
606    /// Charge code
607    pub code: String,
608    /// Charge name
609    pub name: String,
610    /// Charge amount
611    #[serde(with = "serde_utils::decimal_empty_is_0")]
612    pub amount: Decimal,
613    /// Charge currency
614    pub currency: String,
615}
616
617/// Order charge item
618#[derive(Debug, Clone, Serialize, Deserialize)]
619pub struct OrderChargeItem {
620    /// Charge category code
621    pub code: ChargeCategoryCode,
622    /// Charge category name
623    pub name: String,
624    /// Charge details
625    pub fees: Vec<OrderChargeFee>,
626}
627
628/// Order charge detail
629#[derive(Debug, Clone, Serialize, Deserialize)]
630pub struct OrderChargeDetail {
631    /// Total charges amount
632    pub total_amount: Decimal,
633    /// Settlement currency
634    pub currency: String,
635    /// Order charge items
636    pub items: Vec<OrderChargeItem>,
637}
638
639/// Order detail
640#[derive(Debug, Clone, Serialize, Deserialize)]
641pub struct OrderDetail {
642    /// Order ID
643    pub order_id: String,
644    /// Order status
645    pub status: OrderStatus,
646    /// Stock name
647    pub stock_name: String,
648    /// Submitted quantity
649    pub quantity: Decimal,
650    /// Executed quantity
651    pub executed_quantity: Decimal,
652    /// Submitted price
653    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
654    pub price: Option<Decimal>,
655    /// Executed price
656    #[serde(with = "serde_utils::decimal_opt_0_is_none")]
657    pub executed_price: Option<Decimal>,
658    /// Submitted time
659    #[serde(
660        serialize_with = "time::serde::rfc3339::serialize",
661        deserialize_with = "serde_utils::timestamp::deserialize"
662    )]
663    pub submitted_at: OffsetDateTime,
664    /// Order side
665    pub side: OrderSide,
666    /// Security code
667    pub symbol: String,
668    /// Order type
669    pub order_type: OrderType,
670    /// Last done
671    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
672    pub last_done: Option<Decimal>,
673    /// `LIT` / `MIT` Order Trigger Price
674    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
675    pub trigger_price: Option<Decimal>,
676    /// Rejected Message or remark
677    pub msg: String,
678    /// Order tag
679    pub tag: OrderTag,
680    /// Time in force type
681    pub time_in_force: TimeInForceType,
682    /// Long term order expire date
683    #[serde(with = "serde_utils::date_opt")]
684    pub expire_date: Option<Date>,
685    /// Last updated time
686    #[serde(
687        deserialize_with = "serde_utils::timestamp_opt::deserialize",
688        serialize_with = "serde_utils::rfc3339_opt::serialize"
689    )]
690    pub updated_at: Option<OffsetDateTime>,
691    /// Conditional order trigger time
692    #[serde(
693        deserialize_with = "serde_utils::timestamp_opt::deserialize",
694        serialize_with = "serde_utils::rfc3339_opt::serialize"
695    )]
696    pub trigger_at: Option<OffsetDateTime>,
697    /// `TSMAMT` / `TSLPAMT` order trailing amount
698    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
699    pub trailing_amount: Option<Decimal>,
700    /// `TSMPCT` / `TSLPPCT` order trailing percent
701    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
702    pub trailing_percent: Option<Decimal>,
703    /// `TSLPAMT` / `TSLPPCT` order limit offset amount
704    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
705    pub limit_offset: Option<Decimal>,
706    /// Conditional order trigger status
707    #[serde(with = "serde_utils::trigger_status")]
708    pub trigger_status: Option<TriggerStatus>,
709    /// Currency
710    pub currency: String,
711    /// Enable or disable outside regular trading hours
712    #[serde(with = "serde_utils::outside_rth")]
713    pub outside_rth: Option<OutsideRTH>,
714    /// Limit depth level
715    #[serde(with = "serde_utils::int32_opt_0_is_none")]
716    pub limit_depth_level: Option<i32>,
717    /// Trigger count
718    #[serde(with = "serde_utils::int32_opt_0_is_none")]
719    pub trigger_count: Option<i32>,
720    /// Monitor price
721    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
722    pub monitor_price: Option<Decimal>,
723    /// Remark
724    pub remark: String,
725    /// Time-condition submit time (`None` when the order is not a
726    /// time-condition order)
727    #[serde(
728        default,
729        deserialize_with = "serde_utils::timestamp_opt_lenient::deserialize",
730        serialize_with = "serde_utils::rfc3339_opt::serialize"
731    )]
732    pub delayed_until: Option<OffsetDateTime>,
733    /// Commission-free Status
734    pub free_status: CommissionFreeStatus,
735    /// Commission-free amount
736    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
737    pub free_amount: Option<Decimal>,
738    /// Commission-free currency
739    #[serde(with = "serde_utils::symbol_opt")]
740    pub free_currency: Option<String>,
741    /// Deduction status
742    pub deductions_status: DeductionStatus,
743    /// Deduction amount
744    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
745    pub deductions_amount: Option<Decimal>,
746    /// Deduction currency
747    #[serde(with = "serde_utils::symbol_opt")]
748    pub deductions_currency: Option<String>,
749    /// Platform fee deduction status
750    pub platform_deducted_status: DeductionStatus,
751    /// Platform deduction amount
752    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
753    pub platform_deducted_amount: Option<Decimal>,
754    /// Platform deduction currency
755    #[serde(with = "serde_utils::symbol_opt")]
756    pub platform_deducted_currency: Option<String>,
757    /// Order history details
758    pub history: Vec<OrderHistoryDetail>,
759    /// Order charges
760    pub charge_detail: Option<OrderChargeDetail>,
761    /// Attached orders
762    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
763    pub attached_orders: Vec<AttachedOrderDetail>,
764    /// Multi-leg strategy information (only present for multi-leg option
765    /// combination orders)
766    #[serde(default)]
767    pub multi_leg: Option<MultiLegInfo>,
768}
769
770/// Cash info
771#[derive(Debug, Clone, Serialize, Deserialize)]
772pub struct CashInfo {
773    /// Withdraw cash
774    pub withdraw_cash: Decimal,
775    /// Available cash
776    pub available_cash: Decimal,
777    /// Frozen cash
778    pub frozen_cash: Decimal,
779    /// Cash to be settled
780    pub settling_cash: Decimal,
781    /// Currency
782    pub currency: String,
783}
784
785/// Frozen transaction fee
786#[derive(Debug, Clone, Serialize, Deserialize)]
787pub struct FrozenTransactionFee {
788    /// Currency
789    pub currency: String,
790    /// Frozen transaction fee amount
791    pub frozen_transaction_fee: Decimal,
792}
793
794/// Account balance
795#[derive(Debug, Clone, Serialize, Deserialize)]
796pub struct AccountBalance {
797    /// Total cash
798    pub total_cash: Decimal,
799    /// Maximum financing amount
800    pub max_finance_amount: Decimal,
801    /// Remaining financing amount
802    pub remaining_finance_amount: Decimal,
803    /// Risk control level
804    #[serde(with = "serde_utils::risk_level")]
805    pub risk_level: i32,
806    /// Margin call
807    pub margin_call: Decimal,
808    /// Currency
809    pub currency: String,
810    /// Cash details
811    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
812    pub cash_infos: Vec<CashInfo>,
813    /// Net assets
814    #[serde(with = "serde_utils::decimal_empty_is_0")]
815    pub net_assets: Decimal,
816    /// Initial margin
817    #[serde(with = "serde_utils::decimal_empty_is_0")]
818    pub init_margin: Decimal,
819    /// Maintenance margin
820    #[serde(with = "serde_utils::decimal_empty_is_0")]
821    pub maintenance_margin: Decimal,
822    /// Buy power
823    #[serde(with = "serde_utils::decimal_empty_is_0")]
824    pub buy_power: Decimal,
825    /// Frozen transaction fees
826    pub frozen_transaction_fees: Vec<FrozenTransactionFee>,
827}
828
829/// Balance type
830#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, IntoPrimitive)]
831#[repr(i32)]
832pub enum BalanceType {
833    /// Unknown
834    #[num_enum(default)]
835    Unknown = 0,
836    /// Cash
837    Cash = 1,
838    /// Stock
839    Stock = 2,
840    /// Fund
841    Fund = 3,
842}
843
844impl Serialize for BalanceType {
845    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
846        let value: i32 = (*self).into();
847        value.serialize(serializer)
848    }
849}
850
851impl<'de> Deserialize<'de> for BalanceType {
852    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
853        let value = i32::deserialize(deserializer)?;
854        Ok(BalanceType::from(value))
855    }
856}
857
858/// Cash flow direction
859#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, Serialize)]
860#[repr(i32)]
861pub enum CashFlowDirection {
862    /// Unknown
863    #[num_enum(default)]
864    Unknown,
865    /// Out
866    Out = 1,
867    /// In
868    In = 2,
869}
870
871impl<'de> Deserialize<'de> for CashFlowDirection {
872    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
873        let value = i32::deserialize(deserializer)?;
874        Ok(CashFlowDirection::from(value))
875    }
876}
877
878/// Cash flow
879#[derive(Debug, Clone, Serialize, Deserialize)]
880pub struct CashFlow {
881    /// Cash flow name
882    pub transaction_flow_name: String,
883    /// Outflow direction
884    pub direction: CashFlowDirection,
885    /// Balance type
886    pub business_type: BalanceType,
887    /// Cash amount
888    pub balance: Decimal,
889    /// Cash currency
890    pub currency: String,
891    /// Business time
892    #[serde(
893        serialize_with = "time::serde::rfc3339::serialize",
894        deserialize_with = "serde_utils::timestamp::deserialize"
895    )]
896    pub business_time: OffsetDateTime,
897    /// Associated Stock code information
898    #[serde(with = "serde_utils::symbol_opt")]
899    pub symbol: Option<String>,
900    /// Cash flow description
901    pub description: String,
902}
903
904/// Fund positions response
905#[derive(Debug, Clone, Serialize, Deserialize)]
906pub struct FundPositionsResponse {
907    /// Channels
908    #[serde(rename = "list")]
909    pub channels: Vec<FundPositionChannel>,
910}
911
912/// Fund position channel
913#[derive(Debug, Clone, Serialize, Deserialize)]
914pub struct FundPositionChannel {
915    /// Account type
916    pub account_channel: String,
917
918    /// Fund positions
919    #[serde(
920        default,
921        deserialize_with = "crate::serde_utils::null_as_default",
922        rename = "fund_info"
923    )]
924    pub positions: Vec<FundPosition>,
925}
926
927/// Fund position
928#[derive(Debug, Clone, Serialize, Deserialize)]
929pub struct FundPosition {
930    /// Fund counter id (the ISIN is the last `/`-separated segment)
931    #[serde(alias = "symbol")]
932    pub counter_id: String,
933    /// Current equity
934    #[serde(with = "serde_utils::decimal_empty_is_0")]
935    pub current_net_asset_value: Decimal,
936    /// Current equity time
937    #[serde(
938        serialize_with = "time::serde::rfc3339::serialize",
939        deserialize_with = "serde_utils::timestamp::deserialize"
940    )]
941    pub net_asset_value_day: OffsetDateTime,
942    /// Fund name
943    pub symbol_name: String,
944    /// Currency
945    pub currency: String,
946    /// Net cost
947    #[serde(with = "serde_utils::decimal_empty_is_0")]
948    pub cost_net_asset_value: Decimal,
949    /// Holding units
950    #[serde(with = "serde_utils::decimal_empty_is_0")]
951    pub holding_units: Decimal,
952}
953
954/// Stock positions response
955#[derive(Debug, Clone, Serialize, Deserialize)]
956pub struct StockPositionsResponse {
957    /// Channels
958    #[serde(rename = "list")]
959    pub channels: Vec<StockPositionChannel>,
960}
961
962/// Stock position channel
963#[derive(Debug, Clone, Serialize, Deserialize)]
964pub struct StockPositionChannel {
965    /// Account type
966    pub account_channel: String,
967
968    /// Stock positions
969    #[serde(
970        default,
971        deserialize_with = "crate::serde_utils::null_as_default",
972        rename = "stock_info"
973    )]
974    pub positions: Vec<StockPosition>,
975}
976
977/// Stock position
978#[derive(Debug, Clone, Serialize, Deserialize)]
979pub struct StockPosition {
980    /// Stock code
981    pub symbol: String,
982    /// Stock name
983    pub symbol_name: String,
984    /// The number of holdings
985    pub quantity: Decimal,
986    /// Available quantity
987    pub available_quantity: Decimal,
988    /// Currency
989    pub currency: String,
990    /// Cost Price(According to the client's choice of average purchase or
991    /// diluted cost)
992    #[serde(with = "serde_utils::decimal_empty_is_0")]
993    pub cost_price: Decimal,
994    /// Market
995    pub market: Market,
996    /// Initial position before market opening
997    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
998    pub init_quantity: Option<Decimal>,
999}
1000
1001/// Margin ratio
1002#[derive(Debug, Clone, Serialize, Deserialize)]
1003pub struct MarginRatio {
1004    /// Initial margin ratio
1005    pub im_factor: Decimal,
1006    /// Maintain the initial margin ratio
1007    pub mm_factor: Decimal,
1008    /// Forced close-out margin ratio
1009    pub fm_factor: Decimal,
1010}
1011
1012impl_serde_for_enum_string!(
1013    OrderType,
1014    OrderStatus,
1015    OrderSide,
1016    TriggerPriceType,
1017    OrderTag,
1018    TimeInForceType,
1019    TriggerStatus,
1020    OutsideRTH,
1021    CommissionFreeStatus,
1022    DeductionStatus,
1023    ChargeCategoryCode
1024);
1025impl_serde_for_enum_string!(AttachedOrderType);
1026impl_default_for_enum_string!(AttachedOrderType);
1027impl_serde_for_enum_string!(MultiLegStrategy, MultiLegPosition, ContractDirection);
1028impl_default_for_enum_string!(MultiLegStrategy, MultiLegPosition, ContractDirection);
1029impl_default_for_enum_string!(
1030    OrderType,
1031    OrderStatus,
1032    OrderSide,
1033    TriggerPriceType,
1034    OrderTag,
1035    TimeInForceType,
1036    TriggerStatus,
1037    OutsideRTH,
1038    CommissionFreeStatus,
1039    DeductionStatus,
1040    ChargeCategoryCode
1041);
1042
1043// ── US-market types
1044// ───────────────────────────────────────────────────────────
1045
1046/// Request for [`crate::TradeContext::us_query_orders`], modelled after
1047/// [`crate::GetHistoryOrdersOptions`] for HK/CN orders.
1048///
1049/// `query_type`: 0 = all (includes Rejected), 1 = pending, 2 = history (filled
1050/// only). Default 0 matches what the app shows as "past orders".
1051///
1052/// `symbol` accepts a user-facing symbol e.g. `"AAPL.US"` or `"DOGEUSD.BKKT"`
1053/// and is sent to the server as-is.
1054#[derive(Debug, Clone, Default)]
1055pub struct GetUSHistoryOrders {
1056    /// Optional symbol filter, e.g. `"AAPL.US"`.
1057    pub symbol: Option<String>,
1058    /// Direction filter. [`crate::OrderSide::Unknown`] = all (default).
1059    pub side: OrderSide,
1060    /// Start timestamp (seconds). Defaults to 90 days ago.
1061    pub start_at: i64,
1062    /// End timestamp (seconds). Defaults to now.
1063    pub end_at: i64,
1064    /// 0 = all, 1 = pending, 2 = history (filled only). Default 0.
1065    pub query_type: i32,
1066    /// Page number, 1-based. Default 1.
1067    pub page: i32,
1068    /// Page size. Default 20.
1069    pub limit: i32,
1070}
1071
1072/// Alias kept for backward compatibility.
1073pub type QueryUSOrdersOptions = GetUSHistoryOrders;
1074
1075/// Internal JSON body sent to POST /v1/us/orders/query.
1076#[derive(Debug, Clone, Serialize)]
1077pub(crate) struct USQueryOrdersBody {
1078    pub account_channel: String,
1079    pub action: i32,
1080    pub start_at: f64,
1081    pub end_at: f64,
1082    pub symbols: Vec<String>,
1083    pub security_types: Vec<String>,
1084    pub query_type: i32,
1085    pub page: i32,
1086    pub limit: i32,
1087    pub query_version: f64,
1088}
1089
1090/// Response for [`crate::TradeContext::us_query_orders`].
1091#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1092pub struct QueryUSOrdersResponse {
1093    /// Order list (raw JSON for forward compatibility).
1094    /// Order ID field is `id` (not `order_id`).
1095    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1096    pub orders: Vec<serde_json::Value>,
1097    /// Total number of orders matching the query.
1098    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1099    pub total_count: i32,
1100}
1101
1102/// One order state-transition entry within [`USOrderDetail`].
1103#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1104pub struct USOrderHistory {
1105    /// Execution type code
1106    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1107    pub exec_type: i32,
1108    /// Order status at this transition
1109    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1110    pub status: String,
1111    /// Price at this transition
1112    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1113    pub price: String,
1114    /// Quantity at this transition
1115    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1116    pub qty: String,
1117    /// Transition time (Unix-second string)
1118    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1119    pub time: String,
1120    /// Message / note
1121    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1122    pub msg: String,
1123    /// Whether the action was manual
1124    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1125    pub is_manually: bool,
1126    /// Counterparty ID
1127    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1128    pub opp_party_id: String,
1129    /// Trade match ID
1130    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1131    pub trd_match_id: String,
1132    /// Operator identifier
1133    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1134    pub operator: String,
1135    /// Entrust channel of the operation
1136    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1137    pub op_entrust_way: String,
1138    /// Cancel/reject response-to code
1139    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1140    pub cxl_rej_response_to: i32,
1141    /// Withdrawal reason
1142    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1143    pub withdrawal_reason: String,
1144    /// Counterparty name
1145    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1146    pub opp_name: String,
1147    /// Execution ID
1148    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1149    pub exec_id: String,
1150}
1151
1152/// Action-button state for an order.
1153#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1154pub struct USButtonControl {
1155    /// Whether the withdraw/cancel action is available
1156    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1157    pub withdraw: i32,
1158    /// Whether the replace/modify action is available
1159    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1160    pub replace: i32,
1161    /// Actions blocked with an exception reason
1162    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1163    pub exceptionable: Vec<String>,
1164}
1165
1166/// One fee category within [`USChargeDetail`].
1167#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1168pub struct USChargeItem {
1169    /// Fee category code
1170    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1171    pub code: i32,
1172    /// Fee category name
1173    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1174    pub name: String,
1175    /// Individual fee amounts in this category
1176    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1177    pub fees: Vec<String>,
1178}
1179
1180/// Fee breakdown for an order.
1181#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1182pub struct USChargeDetail {
1183    /// Fee currency
1184    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1185    pub currency: String,
1186    /// Total fee amount
1187    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1188    pub total_amount: String,
1189    /// Per-category fee breakdown
1190    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1191    pub items: Vec<USChargeItem>,
1192}
1193
1194/// One bracket/conditional sub-order attached to a main order.
1195#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1196pub struct USAttachedOrder {
1197    /// Attached-order type (display code)
1198    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1199    pub attached_type_display: i32,
1200    /// Executed quantity
1201    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1202    pub executed_qty: String,
1203    /// Order quantity
1204    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1205    pub quantity: String,
1206    /// Order status
1207    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1208    pub status: String,
1209    /// Trigger price
1210    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1211    pub trigger_price: String,
1212    /// Order ID
1213    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1214    pub order_id: String,
1215    /// Good-till date
1216    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1217    pub gtd: String,
1218    /// Time-in-force code
1219    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1220    pub time_in_force: i32,
1221    /// Order tag code
1222    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1223    pub tag: i32,
1224    /// Order type to activate on trigger
1225    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1226    pub activate_order_type: String,
1227    /// Whether to activate outside regular trading hours
1228    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1229    pub activate_rth: i32,
1230    /// Submitted price
1231    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1232    pub submit_price: String,
1233    /// User-facing trading symbol (e.g. `"NKE.US"`)
1234    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1235    pub symbol: String,
1236    /// Whether the order has been withdrawn
1237    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1238    pub withdrawn: bool,
1239}
1240
1241/// Full typed order object within [`USOrderDetailResponse`].
1242/// `submitted_at` and `done_at` are raw unix-second strings.
1243/// `order_histories` is nested inside this object, not at the response top
1244/// level.
1245#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1246pub struct USOrderDetail {
1247    /// Order ID
1248    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1249    pub id: String,
1250    /// Internal account ID
1251    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1252    pub aaid: String,
1253    /// Account channel
1254    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1255    pub account_channel: String,
1256    /// Order action code (buy/sell)
1257    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1258    pub action: i32,
1259    /// User-facing trading symbol (e.g. `"NKE.US"`)
1260    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1261    pub symbol: String,
1262    /// User-facing underlying symbol (options only)
1263    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1264    pub underlying_symbol: String,
1265    /// Security type
1266    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1267    pub security_type: String,
1268    /// Security name
1269    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1270    pub name: String,
1271    /// Order currency
1272    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1273    pub currency: String,
1274    /// Trade currency
1275    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1276    pub trade_currency: String,
1277    /// Order type
1278    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1279    pub order_type: String,
1280    /// Order status
1281    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1282    pub status: String,
1283    /// Order price
1284    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1285    pub price: String,
1286    /// Order quantity
1287    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1288    pub quantity: String,
1289    /// Executed quantity
1290    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1291    pub executed_qty: String,
1292    /// Executed price
1293    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1294    pub executed_price: String,
1295    /// Executed amount
1296    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1297    pub executed_amount: String,
1298    /// Operate direction
1299    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1300    pub operate_direction: String,
1301    /// Time-in-force code
1302    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1303    pub time_in_force: i32,
1304    /// Good-till date
1305    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1306    pub gtd: String,
1307    /// Order tag code
1308    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1309    pub tag: i32,
1310    /// Message / note
1311    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1312    pub msg: String,
1313    /// Whether the order is restricted to regular trading hours
1314    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1315    pub force_only_rth: i32,
1316    /// Submission time (Unix-second string)
1317    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1318    pub submitted_at: String,
1319    /// Completion time (Unix-second string)
1320    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1321    pub done_at: String,
1322    /// Trigger price
1323    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1324    pub trigger_price: String,
1325    /// Trigger time (Unix-second string)
1326    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1327    pub trigger_at: String,
1328    /// Trigger status code
1329    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1330    pub trigger_status: i32,
1331    /// Trigger exchange
1332    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1333    pub trigger_exchange: String,
1334    /// Last-done price at trigger
1335    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1336    pub trigger_last_done: String,
1337    /// Number of times triggered
1338    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1339    pub trigger_count: i32,
1340    /// Trailing amount (trailing-stop orders)
1341    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1342    pub tailing_amount: String,
1343    /// Trailing percent (trailing-stop orders)
1344    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1345    pub tailing_percent: String,
1346    /// Limit offset (trailing-limit orders)
1347    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1348    pub limit_offset: String,
1349    /// Order-book depth level for limit orders
1350    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1351    pub limit_depth_level: i32,
1352    /// Market price at reference
1353    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1354    pub market_price: String,
1355    /// Submitted amount
1356    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1357    pub submitted_amount: String,
1358    /// Estimated fee
1359    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1360    pub estimated_fee: String,
1361    /// Commission-free status code
1362    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1363    pub free_status: i32,
1364    /// Commission-free amount
1365    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1366    pub free_amount: String,
1367    /// Commission-free currency
1368    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1369    pub free_currency: String,
1370    /// Deduction status code
1371    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1372    pub deductions_status: i32,
1373    /// Deduction amount
1374    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1375    pub deductions_amount: String,
1376    /// Deduction currency
1377    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1378    pub deductions_currency: String,
1379    /// Platform deduction status code
1380    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1381    pub platform_deductions_status: i32,
1382    /// Platform deduction amount
1383    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1384    pub platform_deductions_amount: String,
1385    /// Platform deduction currency
1386    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1387    pub platform_deductions_currency: String,
1388    /// Display account
1389    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1390    pub display_account: String,
1391    /// Settlement account
1392    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1393    pub settlement_account: String,
1394    /// Settlement channel
1395    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1396    pub settlement_channel: String,
1397    /// Customer name
1398    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1399    pub customer_name: String,
1400    /// Account holder real name
1401    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1402    pub real_name: String,
1403    /// Account holder English name
1404    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1405    pub en_name: String,
1406    /// Joint-account holder real name
1407    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1408    pub joint_real_name: String,
1409    /// Joint-account holder English name
1410    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1411    pub joint_en_name: String,
1412    /// Organization ID
1413    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1414    pub org_id: String,
1415    /// Broker-to-client assigned number (HK)
1416    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1417    pub bcan: String,
1418    /// Entrust channel code
1419    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1420    pub op_entrust_way: i32,
1421    /// Entrust channel name
1422    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1423    pub op_entrust_way_name: String,
1424    /// Remark
1425    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1426    pub remark: String,
1427    /// Notice text
1428    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1429    pub notice: String,
1430    /// Short-sell type code
1431    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1432    pub short_sell_type: i32,
1433    /// Strategy (ploy) type
1434    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1435    pub ploy_type: String,
1436    /// Strategy (ploy) ID
1437    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1438    pub ploy_id: String,
1439    /// Strategy (ploy) status
1440    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1441    pub ploy_status: String,
1442    /// Trend direction code
1443    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1444    pub trend: i32,
1445    /// Withdrawal reason
1446    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1447    pub withdrawal_reason: String,
1448    /// Order type to activate on trigger
1449    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1450    pub activate_order_type: String,
1451    /// Whether to activate outside regular trading hours
1452    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1453    pub activate_rth: i32,
1454    /// Submitted price
1455    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1456    pub submit_price: String,
1457    /// Contract direction (options)
1458    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1459    pub contract_direction: String,
1460    /// Strike price (options)
1461    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1462    pub strike_price: String,
1463    /// Contract size (options)
1464    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1465    pub contract_size: String,
1466    /// Monitor price (conditional orders)
1467    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1468    pub monitor_price: String,
1469    /// Available action buttons
1470    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1471    pub button_control: USButtonControl,
1472    /// Fee breakdown (absent when not applicable)
1473    pub charge_detail: Option<USChargeDetail>,
1474    /// Attached bracket/conditional sub-orders
1475    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1476    pub attached_orders: Vec<USAttachedOrder>,
1477    /// Order state-transition history
1478    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1479    pub order_histories: Vec<USOrderHistory>,
1480}
1481
1482/// Response for [`crate::TradeContext::us_order_detail`].
1483/// Path: `GET /v1/us/orders/{order_id}`
1484#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1485pub struct USOrderDetailResponse {
1486    /// Full typed order object; None only on error.
1487    pub order: Option<USOrderDetail>,
1488    /// Active bracket/conditional sub-order, or None.
1489    pub current_attached_order: Option<USOrderDetail>,
1490    /// Server response timestamp (milliseconds string).
1491    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1492    pub current_millisecond: String,
1493}
1494
1495/// One cash currency entry in [`USAssetOverview`].
1496#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1497pub struct USCashEntry {
1498    /// Currency
1499    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1500    pub currency: String,
1501    /// Cash frozen for pending buys
1502    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1503    pub frozen_buy_cash: String,
1504    /// Outstanding (unsettled) amount
1505    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1506    pub outstanding: String,
1507    /// Settled cash
1508    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1509    pub settled_cash: String,
1510    /// Total amount
1511    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1512    pub total_amount: String,
1513    /// Total cash
1514    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1515    pub total_cash: String,
1516}
1517
1518/// One cryptocurrency holding in [`USAssetOverview`].
1519#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1520pub struct USCryptoEntry {
1521    /// Asset type (e.g. `"CRYPTO"`)
1522    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1523    pub asset_type: String,
1524    /// Trading-pair code (e.g. `"BTCUSD"`); the user-facing symbol is
1525    /// `"<symbol>.<market>"` (e.g. `"BTCUSD.BKKT"`)
1526    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1527    pub symbol: String,
1528    /// Holding quantity
1529    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1530    pub quantity: String,
1531    /// Settlement currency
1532    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1533    pub currency: String,
1534    /// Average cost price; empty when the server has none
1535    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1536    pub average_cost: String,
1537    /// Market code (e.g. `"BKKT"`)
1538    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1539    pub market: String,
1540    /// Trade status
1541    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1542    pub trade_status: String,
1543    /// Previous close price
1544    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1545    pub prev_close: String,
1546    /// Latest price
1547    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1548    pub last_done: String,
1549    /// Market price
1550    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1551    pub market_price: String,
1552    /// Pre-market close price
1553    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1554    pub pretrade_close: String,
1555    /// Today's investment value
1556    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1557    pub stock_invest_of_today: String,
1558    /// Today's profit/loss
1559    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1560    pub today_pl: String,
1561    /// Today's investment value (pre-market)
1562    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1563    pub pretrade_stock_invest_of_today: String,
1564    /// Today's profit/loss (pre-market)
1565    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1566    pub pretrade_today_pl: String,
1567    /// Overnight-session latest price
1568    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1569    pub night_last_done: String,
1570    /// Overnight-session previous close
1571    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1572    pub night_prev_close: String,
1573    /// Position side (long/short)
1574    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1575    pub position_side: String,
1576    /// Position open time (Unix-second string; `"0"` when unknown)
1577    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1578    pub open_position_time: String,
1579    /// Asset name (e.g. `"Bitcoin"`)
1580    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1581    pub name: String,
1582    /// Industry classification ID
1583    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1584    pub industry_counter_id: String,
1585    /// Industry name
1586    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1587    pub industry_name: String,
1588}
1589
1590/// One stock/equity position in [`USAssetOverview`].
1591#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1592pub struct USStockEntry {
1593    /// Ticker code returned by the API (e.g. `"AAPL"`). See `full_symbol` for
1594    /// the qualified form.
1595    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1596    pub symbol: String,
1597    /// Qualified user-facing symbol (e.g. `"AAPL.US"`)
1598    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1599    pub full_symbol: String,
1600    /// Asset type
1601    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1602    pub asset_type: String,
1603    /// Holding quantity
1604    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1605    pub quantity: String,
1606    /// Currency
1607    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1608    pub currency: String,
1609    /// Average cost price
1610    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1611    pub average_cost: String,
1612    /// Market code
1613    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1614    pub market: String,
1615    /// Trade status
1616    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1617    pub trade_status: String,
1618    /// Previous close price
1619    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1620    pub prev_close: String,
1621    /// Latest price
1622    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1623    pub last_done: String,
1624    /// Market price
1625    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1626    pub market_price: String,
1627    /// Pre-market close price
1628    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1629    pub pretrade_close: String,
1630    /// Today's stock investment value
1631    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1632    pub stock_invest_of_today: String,
1633    /// Today's profit/loss
1634    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1635    pub today_pl: String,
1636    /// Today's stock investment value (pre-market)
1637    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1638    pub pretrade_stock_invest_of_today: String,
1639    /// Today's profit/loss (pre-market)
1640    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1641    pub pretrade_today_pl: String,
1642    /// Overnight-session latest price
1643    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1644    pub night_last_done: String,
1645    /// Overnight-session previous close
1646    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1647    pub night_prev_close: String,
1648    /// Position side (long/short)
1649    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1650    pub position_side: String,
1651    /// Position open time (Unix-second string)
1652    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1653    pub open_position_time: String,
1654    /// Security name
1655    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1656    pub name: String,
1657    /// Industry classification ID
1658    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1659    pub industry_counter_id: String,
1660    /// Industry name
1661    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1662    pub industry_name: String,
1663}
1664
1665/// Response for [`crate::TradeContext::us_asset_overview`].
1666/// Field names match the actual API response from `GET /v1/us/assets/overview`.
1667#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1668pub struct USAssetOverview {
1669    /// Account type
1670    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1671    pub account_type: String,
1672    /// Account snapshot timestamp (Unix-second string → OffsetDateTime).
1673    #[serde(default, with = "crate::serde_utils::timestamp_opt")]
1674    pub asset_timestamp: Option<time::OffsetDateTime>,
1675    /// Cash buying power (top-level convenience field).
1676    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1677    pub cash_buy_power: String,
1678    /// Overnight buying power
1679    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1680    pub overnight_buy_power: String,
1681    /// Account currency
1682    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1683    pub currency: String,
1684    /// Cash balances per currency
1685    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1686    pub cash_list: Vec<USCashEntry>,
1687    /// Stock/equity positions
1688    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1689    pub stock_list: Vec<USStockEntry>,
1690    /// Option positions (raw JSON)
1691    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1692    pub option_list: Vec<serde_json::Value>,
1693    /// Cryptocurrency holdings
1694    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1695    pub crypto_list: Vec<USCryptoEntry>,
1696    /// Multi-leg option strategies (raw JSON)
1697    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1698    pub multi_leg: serde_json::Value,
1699}
1700
1701/// One time-period metric in a [`USRealizedPLEntry`].
1702#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1703pub struct USRealizedPLMetric {
1704    /// Realized P&L amount
1705    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1706    pub amount: String,
1707    /// Period code (server-defined; 2 = current month observed in testing).
1708    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1709    pub period: i32,
1710    /// Realized P&L rate
1711    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1712    pub rate: String,
1713}
1714
1715/// One asset-category entry in [`USRealizedPL`].
1716/// `category`: 0 = all, 1 = stock, 2 = option, 3 = crypto (server-defined).
1717#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1718pub struct USRealizedPLEntry {
1719    /// Asset category (0 = all, 1 = stock, 2 = option, 3 = crypto)
1720    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1721    pub category: i32,
1722    /// Currency
1723    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1724    pub currency: String,
1725    /// Per-period realized P&L metrics
1726    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1727    pub metrics: Vec<USRealizedPLMetric>,
1728}
1729
1730/// Request for [`crate::TradeContext::us_realized_pl`], modelled after
1731/// [`crate::GetUSHistoryOrders`].
1732#[derive(Debug, Clone, Default)]
1733pub struct GetUSRealizedPLOptions {
1734    /// Currency, e.g. `"USD"`. Defaults to `"USD"` if empty.
1735    pub currency: String,
1736    /// Asset category filter: `""` = all, `"STOCK"`, `"OPTION"`, `"CRYPTO"`.
1737    pub category: String,
1738}
1739
1740/// Response for [`crate::TradeContext::us_realized_pl`].
1741/// Field name matches the actual API response from `GET
1742/// /v1/us/assets/pl/realized`.
1743#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1744pub struct USRealizedPL {
1745    /// Per-asset-category realized P&L entries
1746    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1747    pub realized_pl_list: Vec<USRealizedPLEntry>,
1748}
1749
1750/// Request for [`crate::TradeContext::us_available_holding`].
1751#[derive(Debug, Clone, Default)]
1752pub struct GetUSAvailableHoldingOptions {
1753    /// Security symbol, e.g. `"AAPL.US"`, an option symbol such as
1754    /// `"AAPL261120C345000.US"`, or a crypto pair such as `"BTCUSD.BKKT"`.
1755    pub symbol: String,
1756    /// Holding type: `"STOCK"`, `"OPTION"` or `"CRYPTO"`. Defaults to
1757    /// `"STOCK"` if empty.
1758    pub equity_type: String,
1759    /// Position side: `"LONG"` or `"SHORT"`. Defaults to `"LONG"` if empty.
1760    pub position_side: String,
1761}
1762
1763/// Response for [`crate::TradeContext::us_available_holding`].
1764#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1765pub struct USAvailableHolding {
1766    /// Quantity available to sell (long) or buy to cover (short); negative for
1767    /// a short position
1768    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1769    pub available_qty: String,
1770}
1771
1772/// Buying power in [`USCashAndBuyPower`].
1773#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1774pub struct USBuyLimitInfo {
1775    /// Currency
1776    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1777    pub currency: String,
1778    /// Cash buying power
1779    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1780    pub cash_buy_power: String,
1781    /// Overnight buying power
1782    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1783    pub overnight_buy_power: String,
1784    /// Day-trade buying power
1785    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1786    pub day_trade_buy_power: String,
1787    /// Instant buying power
1788    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1789    pub instant_buy_power: String,
1790    /// Intraday margin buying power
1791    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1792    pub intraday_margin_buy_power: String,
1793    /// Option buying power
1794    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1795    pub option_buy_power: String,
1796    /// Crypto buying power
1797    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1798    pub crypto_buy_power: String,
1799    /// Margin maintenance excess used for buying power
1800    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1801    pub mme: String,
1802}
1803
1804/// One cash currency entry in [`USCashAndBuyPower`].
1805#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1806pub struct USCashDetail {
1807    /// Currency
1808    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1809    pub currency: String,
1810    /// Total cash
1811    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1812    pub total_amount: String,
1813    /// Available cash
1814    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1815    pub available_cash: String,
1816    /// Withdrawable cash
1817    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1818    pub withdraw_cash: String,
1819    /// Cash frozen for pending orders
1820    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1821    pub frozen_cash: String,
1822    /// Settled cash
1823    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1824    pub settled_cash: String,
1825    /// Outstanding (unsettled) amount
1826    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1827    pub outstanding: String,
1828    /// Amount held for external use (e.g. IPO or fund subscriptions)
1829    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1830    pub external_amount: String,
1831}
1832
1833/// Margin account status in [`USCashAndBuyPower`].
1834#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1835pub struct USMarginStatus {
1836    /// Currency
1837    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1838    pub currency: String,
1839    /// Net liquidation value
1840    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1841    pub nlv: String,
1842    /// Margin equity
1843    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1844    pub margin_equity: String,
1845    /// Margin maintenance excess
1846    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1847    pub margin_maintenance_excess: String,
1848    /// Maintenance margin requirement
1849    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1850    pub mmr: String,
1851    /// Regulation T excess
1852    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1853    pub reg_t_excess: String,
1854    /// Special memorandum account
1855    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1856    pub sma: String,
1857}
1858
1859/// Response for [`crate::TradeContext::us_cash_and_buy_power`].
1860/// Field names match `GET /v1/us/assets/cash-and-buy-power`.
1861#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1862pub struct USCashAndBuyPower {
1863    /// Buying power
1864    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1865    pub buy_limit_info: USBuyLimitInfo,
1866    /// Cash per currency
1867    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1868    pub cash_list: Vec<USCashDetail>,
1869    /// Margin account status
1870    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1871    pub margin_status: USMarginStatus,
1872}
1873
1874/// Request for [`crate::TradeContext::us_transaction_history`].
1875#[derive(Debug, Clone)]
1876pub struct GetUSTransactionHistoryOptions {
1877    /// Start time (inclusive)
1878    pub start_at: OffsetDateTime,
1879    /// End time (inclusive)
1880    pub end_at: OffsetDateTime,
1881    /// Page number, starting at 1. Defaults to 1 if 0.
1882    pub page: i32,
1883    /// Page size. Defaults to 20 if 0.
1884    pub size: i32,
1885    /// Transaction types to include; empty means all. Values:
1886    /// `"TT_DEPOSIT_AND_WITHDRAWAL"`, `"TT_STOCKS_AND_ETFS_TRADING"`,
1887    /// `"TT_OPTIONS_TRADING"`, `"TT_CRYPTO_TRADING"`, `"TT_CORPORATE_ACTION"`,
1888    /// `"TT_TRANSFER_IN_AND_OUT"`, `"TT_CRYPTO_TRANSFER"`,
1889    /// `"TT_MARGIN_FINANCING"`, `"TT_OTHER"`.
1890    pub types: Vec<String>,
1891    /// Currency filter, e.g. `"USD"`; empty means all.
1892    pub currency: String,
1893}
1894
1895impl GetUSTransactionHistoryOptions {
1896    /// Create a request for transactions between `start_at` and `end_at`.
1897    pub fn new(start_at: OffsetDateTime, end_at: OffsetDateTime) -> Self {
1898        Self {
1899            start_at,
1900            end_at,
1901            page: 1,
1902            size: 20,
1903            types: Vec::new(),
1904            currency: String::new(),
1905        }
1906    }
1907}
1908
1909/// One record in [`USTransactionHistory`].
1910#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1911pub struct USTransaction {
1912    /// Display title
1913    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1914    pub title: String,
1915    /// Business code
1916    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1917    pub biz_code: String,
1918    /// Business code name
1919    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1920    pub biz_code_name: String,
1921    /// Amount; negative for outflows
1922    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1923    pub amount: String,
1924    /// Currency
1925    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1926    pub currency: String,
1927    /// Security symbol, if the record relates to one (e.g. `"AAPL.US"`)
1928    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1929    pub symbol: String,
1930    /// Market code
1931    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1932    pub market: String,
1933    /// Holding type
1934    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1935    pub equity_type: String,
1936    /// Position side
1937    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1938    pub position_side: String,
1939    /// Quantity
1940    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1941    pub quantity: String,
1942    /// Quantity description
1943    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1944    pub quantity_description: String,
1945    /// Reference number
1946    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1947    pub ref_no: String,
1948    /// Remark
1949    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1950    pub remark: String,
1951    /// Creation time
1952    #[serde(
1953        default,
1954        deserialize_with = "crate::serde_utils::timestamp_opt_lenient::deserialize",
1955        serialize_with = "crate::serde_utils::rfc3339_opt::serialize"
1956    )]
1957    pub create_at: Option<OffsetDateTime>,
1958    /// Completion time
1959    #[serde(
1960        default,
1961        deserialize_with = "crate::serde_utils::timestamp_opt_lenient::deserialize",
1962        serialize_with = "crate::serde_utils::rfc3339_opt::serialize"
1963    )]
1964    pub done_at: Option<OffsetDateTime>,
1965}
1966
1967/// Response for [`crate::TradeContext::us_transaction_history`].
1968/// Field names match `POST /v1/us/assets/transaction-history`.
1969#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1970pub struct USTransactionHistory {
1971    /// Current page number
1972    #[serde(default, with = "crate::serde_utils::int64_str")]
1973    pub current_page: i64,
1974    /// Records on this page. The API returns no total count: a page shorter
1975    /// than the requested size is the last one.
1976    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1977    pub items: Vec<USTransaction>,
1978}
1979
1980/// One exchange rate in [`USExchangeRates`].
1981#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1982pub struct USExchangeRate {
1983    /// Account channel the rates apply to, e.g. `"us_lb_apex"` (outer
1984    /// `universal_exchanges` key)
1985    pub universal_key: String,
1986    /// Rate type, e.g. `"am"` or `"reference"` (inner `spec_exchanges` key)
1987    pub spec_key: String,
1988    /// Base currency
1989    pub base_currency: String,
1990    /// Other (quote) currency
1991    pub other_currency: String,
1992    /// Bid rate
1993    pub bid_rate: String,
1994    /// Offer rate
1995    pub offer_rate: String,
1996    /// Average rate
1997    pub average_rate: String,
1998}
1999
2000/// Response for [`crate::TradeContext::us_exchange_rates`].
2001///
2002/// The upstream response is a two-level map
2003/// (`universal_exchanges.<key>.spec_exchanges.<key>.exchanges[]`); it is
2004/// flattened into a single list, sorted by the two upstream map keys, with
2005/// the keys kept on each entry as [`USExchangeRate::universal_key`] and
2006/// [`USExchangeRate::spec_key`].
2007#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2008#[serde(from = "RawUSExchangeRates")]
2009pub struct USExchangeRates {
2010    /// Exchange rates
2011    pub exchanges: Vec<USExchangeRate>,
2012}
2013
2014#[derive(Deserialize, Default)]
2015struct RawUSExchangeRate {
2016    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2017    base_currency: String,
2018    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2019    other_currency: String,
2020    #[serde(default, with = "crate::serde_utils::value_as_opt_string")]
2021    bid_rate: Option<String>,
2022    #[serde(default, with = "crate::serde_utils::value_as_opt_string")]
2023    offer_rate: Option<String>,
2024    #[serde(default, with = "crate::serde_utils::value_as_opt_string")]
2025    average_rate: Option<String>,
2026}
2027
2028#[derive(Deserialize, Default)]
2029struct RawUSSpecExchanges {
2030    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2031    exchanges: Vec<RawUSExchangeRate>,
2032}
2033
2034#[derive(Deserialize, Default)]
2035struct RawUSUniversalExchanges {
2036    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2037    spec_exchanges: BTreeMap<String, RawUSSpecExchanges>,
2038}
2039
2040#[derive(Deserialize, Default)]
2041struct RawUSExchangeRates {
2042    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2043    universal_exchanges: BTreeMap<String, RawUSUniversalExchanges>,
2044    /// Already-flattened form, so that the serialized [`USExchangeRates`]
2045    /// deserializes back.
2046    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2047    exchanges: Vec<USExchangeRate>,
2048}
2049
2050impl From<RawUSExchangeRates> for USExchangeRates {
2051    fn from(raw: RawUSExchangeRates) -> Self {
2052        let mut exchanges = raw.exchanges;
2053        for (universal_key, universal) in raw.universal_exchanges {
2054            for (spec_key, spec) in universal.spec_exchanges {
2055                exchanges.extend(spec.exchanges.into_iter().map(|rate| USExchangeRate {
2056                    universal_key: universal_key.clone(),
2057                    spec_key: spec_key.clone(),
2058                    base_currency: rate.base_currency,
2059                    other_currency: rate.other_currency,
2060                    bid_rate: rate.bid_rate.unwrap_or_default(),
2061                    offer_rate: rate.offer_rate.unwrap_or_default(),
2062                    average_rate: rate.average_rate.unwrap_or_default(),
2063                }));
2064            }
2065        }
2066        Self { exchanges }
2067    }
2068}
2069
2070#[cfg(test)]
2071mod tests {
2072    use time::macros::datetime;
2073
2074    use super::*;
2075
2076    #[test]
2077    fn execution_without_side_defaults_to_unknown() {
2078        // `/v1/trade/execution/history` on a Longbridge US account returns
2079        // trades with no `side`; they must still deserialize.
2080        let data = r#"{
2081            "order_id": "1000000000000000001",
2082            "trade_id": "1000000000000000001-1",
2083            "symbol": "ETHUSD.BKKT",
2084            "trade_done_at": "1790203596",
2085            "quantity": "0.168",
2086            "price": "2690.34165646"
2087        }"#;
2088        let execution: Execution = serde_json::from_str(data).expect("execution without side");
2089        assert_eq!(
2090            execution.side,
2091            OrderSide::Unknown,
2092            "missing side defaults to Unknown"
2093        );
2094        assert_eq!(execution.symbol, "ETHUSD.BKKT", "symbol is kept");
2095    }
2096
2097    #[test]
2098    fn us_exchange_rates_response() {
2099        let data = r#"{
2100            "universal_exchanges": {
2101                "2": {"spec_exchanges": {"USD": {"exchanges": [
2102                    {"base_currency": "USD", "other_currency": "HKD", "bid_rate": 7.8, "offer_rate": "7.85", "average_rate": null}
2103                ]}}},
2104                "1": {"spec_exchanges": {
2105                    "b": {"exchanges": [{"base_currency": "USD", "other_currency": "CNH", "bid_rate": "7.1"}]},
2106                    "a": {"exchanges": null}
2107                }}
2108            }
2109        }"#;
2110        let resp: USExchangeRates = serde_json::from_str(data).unwrap();
2111        assert_eq!(resp.exchanges.len(), 2);
2112        assert_eq!(resp.exchanges[0].universal_key, "1");
2113        assert_eq!(resp.exchanges[0].spec_key, "b");
2114        assert_eq!(resp.exchanges[0].other_currency, "CNH");
2115        assert_eq!(resp.exchanges[0].offer_rate, "");
2116        assert_eq!(resp.exchanges[1].universal_key, "2");
2117        assert_eq!(resp.exchanges[1].spec_key, "USD");
2118        assert_eq!(resp.exchanges[1].bid_rate, "7.8");
2119        assert_eq!(resp.exchanges[1].offer_rate, "7.85");
2120        assert_eq!(resp.exchanges[1].average_rate, "");
2121
2122        let json = serde_json::to_string(&resp).unwrap();
2123        let back: USExchangeRates = serde_json::from_str(&json).unwrap();
2124        assert_eq!(back.exchanges.len(), 2);
2125        assert_eq!(back.exchanges[1].bid_rate, "7.8");
2126    }
2127
2128    #[test]
2129    fn us_cash_and_buy_power_response() {
2130        let data = r#"{
2131            "buy_limit_info": {"currency": "USD", "cash_buy_power": "100.5", "overnight_buy_power": "90"},
2132            "cash_list": [{"currency": "USD", "total_amount": "120", "available_cash": "100.5", "withdraw_cash": "80", "frozen_cash": null}],
2133            "margin_status": {"currency": "USD", "nlv": "1200"}
2134        }"#;
2135        let resp: USCashAndBuyPower = serde_json::from_str(data).unwrap();
2136        assert_eq!(resp.buy_limit_info.cash_buy_power, "100.5");
2137        assert_eq!(resp.cash_list[0].withdraw_cash, "80");
2138        assert_eq!(resp.cash_list[0].frozen_cash, "");
2139        assert_eq!(resp.margin_status.nlv, "1200");
2140    }
2141
2142    #[test]
2143    fn us_transaction_history_response() {
2144        let data = r#"{
2145            "current_page": "2",
2146            "items": [
2147                {"title": "Buy", "amount": "-100", "currency": "USD", "symbol": "AAPL.US", "create_at": "1792600000", "done_at": 1792600060},
2148                {"title": "Buy", "amount": "-10", "currency": "USD", "symbol": "BTCUSD.BKKT", "counter_id": "VA/BKKT/BTCUSD", "done_at": "0"}
2149            ]
2150        }"#;
2151        let resp: USTransactionHistory = serde_json::from_str(data).unwrap();
2152        assert_eq!(resp.current_page, 2);
2153        assert_eq!(resp.items[0].symbol, "AAPL.US");
2154        assert_eq!(
2155            resp.items[0].create_at.unwrap().unix_timestamp(),
2156            1792600000
2157        );
2158        assert_eq!(resp.items[0].done_at.unwrap().unix_timestamp(), 1792600060);
2159        assert_eq!(resp.items[1].symbol, "BTCUSD.BKKT");
2160        assert!(resp.items[1].done_at.is_none());
2161    }
2162
2163    /// Shapes (field names, JSON types, map keys) as returned by US prod.
2164    #[test]
2165    fn us_asset_responses_as_returned_by_prod() {
2166        let data = r#"{
2167            "current_page": 1,
2168            "items": [{
2169                "amount": "0.5", "biz_code": "LVAVNSCR", "biz_code_name": "Cryptocurrency Deposit",
2170                "counter_id": "VA/BKKT/BTCUSD", "create_at": "1791478565", "currency": "",
2171                "done_at": "1791478565", "equity_type": "CRYPTO", "market": "BKKT",
2172                "position_side": "LONG", "quantity": "0.5", "quantity_description": "0.5",
2173                "ref_no": "R1", "remark": "", "symbol": "BTCUSD.BKKT",
2174                "title": "Cryptocurrency Deposit: BTCUSD"
2175            }]
2176        }"#;
2177        let resp: USTransactionHistory = serde_json::from_str(data).unwrap();
2178        assert_eq!(resp.current_page, 1);
2179        assert_eq!(resp.items[0].symbol, "BTCUSD.BKKT");
2180        assert_eq!(resp.items[0].equity_type, "CRYPTO");
2181        assert_eq!(resp.items[0].done_at.unwrap().unix_timestamp(), 1791478565);
2182
2183        let data = r#"{"universal_exchanges": {"us_lb_apex": {"spec_exchanges": {
2184            "reference": {"exchanges": [{"average_rate": 1, "base_currency": "USD", "bid_rate": 1, "offer_rate": 1, "other_currency": "USD"}]},
2185            "am": {"exchanges": [{"average_rate": 1, "base_currency": "USD", "bid_rate": 1, "offer_rate": 1, "other_currency": "USD"}]}
2186        }}}}"#;
2187        let resp: USExchangeRates = serde_json::from_str(data).unwrap();
2188        let keys: Vec<_> = resp
2189            .exchanges
2190            .iter()
2191            .map(|e| (e.universal_key.as_str(), e.spec_key.as_str()))
2192            .collect();
2193        assert_eq!(keys, [("us_lb_apex", "am"), ("us_lb_apex", "reference")]);
2194        assert_eq!(resp.exchanges[0].average_rate, "1");
2195
2196        let data = r#"{
2197            "account_type": "1", "asset_timestamp": "1792661597", "currency": "USD",
2198            "stock_list": [{"symbol": "MULL", "full_symbol": "MULL.US", "counter_id": "ETF/US/MULL", "quantity": "10", "market": "US"}],
2199            "crypto_list": [{
2200                "asset_type": "CRYPTO", "average_cost": "", "counter_id": "VA/BKKT/ETHUSD",
2201                "currency": "USD", "last_done": "2500", "market": "BKKT", "market_price": "2500",
2202                "name": "Ethereum", "open_position_time": "0", "position_side": "LONG",
2203                "quantity": "0.1", "symbol": "ETHUSD", "today_pl": "", "trade_status": "105"
2204            }],
2205            "option_list": [], "multi_leg": {"holdings": []}
2206        }"#;
2207        let resp: USAssetOverview = serde_json::from_str(data).unwrap();
2208        assert_eq!(resp.asset_timestamp.unwrap().unix_timestamp(), 1792661597);
2209        assert_eq!(resp.stock_list[0].full_symbol, "MULL.US");
2210        let crypto = &resp.crypto_list[0];
2211        assert_eq!(crypto.symbol, "ETHUSD");
2212        assert_eq!(crypto.market, "BKKT");
2213        assert_eq!(crypto.quantity, "0.1");
2214        assert_eq!(crypto.name, "Ethereum");
2215        assert_eq!(crypto.market_price, "2500");
2216    }
2217
2218    #[test]
2219    fn fund_position_response() {
2220        let data = r#"
2221        {
2222            "list": [{
2223                "account_channel": "lb",
2224                "fund_info": [{
2225                    "counter_id": "UT/FD/HK0000447943",
2226                    "symbol_name": "高腾亚洲收益基金",
2227                    "currency": "USD",
2228                    "holding_units": "5.000",
2229                    "current_net_asset_value": "0",
2230                    "cost_net_asset_value": "0.00",
2231                    "net_asset_value_day": "1649865600"
2232                }]
2233            }]
2234        }
2235        "#;
2236
2237        let resp: FundPositionsResponse = serde_json::from_str(data).unwrap();
2238        assert_eq!(resp.channels.len(), 1);
2239
2240        let channel = &resp.channels[0];
2241        assert_eq!(channel.account_channel, "lb");
2242        assert_eq!(channel.positions.len(), 1);
2243
2244        let position = &channel.positions[0];
2245        assert_eq!(position.counter_id, "UT/FD/HK0000447943");
2246        assert_eq!(position.symbol_name, "高腾亚洲收益基金");
2247        assert_eq!(position.currency, "USD");
2248        assert_eq!(position.current_net_asset_value, decimal!(0i32));
2249        assert_eq!(position.cost_net_asset_value, decimal!(0i32));
2250        assert_eq!(position.holding_units, decimal!(5i32));
2251        assert_eq!(position.net_asset_value_day, datetime!(2022-4-14 0:00 +8));
2252
2253        // Transition compatibility: the legacy `symbol` key (which carried a
2254        // bare ISIN) still deserializes into `counter_id` via the serde alias.
2255        let legacy = r#"{"list":[{"account_channel":"lb","fund_info":[{"symbol":"HK0000447943","symbol_name":"x","currency":"USD","holding_units":"0","current_net_asset_value":"0","cost_net_asset_value":"0","net_asset_value_day":"1649865600"}]}]}"#;
2256        let resp: FundPositionsResponse = serde_json::from_str(legacy).unwrap();
2257        assert_eq!(resp.channels[0].positions[0].counter_id, "HK0000447943");
2258    }
2259
2260    #[test]
2261    fn stock_position_response() {
2262        let data = r#"
2263        {
2264            "list": [
2265              {
2266                "account_channel": "lb",
2267                "stock_info": [
2268                  {
2269                    "symbol": "700.HK",
2270                    "symbol_name": "腾讯控股",
2271                    "currency": "HK",
2272                    "quantity": "650",
2273                    "available_quantity": "-450",
2274                    "cost_price": "457.53",
2275                    "market": "HK",
2276                    "init_quantity": "2000"
2277                  },
2278                  {
2279                    "symbol": "9991.HK",
2280                    "symbol_name": "宝尊电商-SW",
2281                    "currency": "HK",
2282                    "quantity": "200",
2283                    "available_quantity": "0",
2284                    "cost_price": "32.25",
2285                    "market": "HK",
2286                    "init_quantity": ""
2287                  }
2288                ]
2289              }
2290            ]
2291          }
2292        "#;
2293
2294        let resp: StockPositionsResponse = serde_json::from_str(data).unwrap();
2295        assert_eq!(resp.channels.len(), 1);
2296
2297        let channel = &resp.channels[0];
2298        assert_eq!(channel.account_channel, "lb");
2299        assert_eq!(channel.positions.len(), 2);
2300
2301        let position = &channel.positions[0];
2302        assert_eq!(position.symbol, "700.HK");
2303        assert_eq!(position.symbol_name, "腾讯控股");
2304        assert_eq!(position.currency, "HK");
2305        assert_eq!(position.quantity, decimal!(650));
2306        assert_eq!(position.available_quantity, decimal!(-450));
2307        assert_eq!(position.cost_price, decimal!(457.53f32));
2308        assert_eq!(position.market, Market::HK);
2309        assert_eq!(position.init_quantity, Some(decimal!(2000)));
2310
2311        let position = &channel.positions[0];
2312        assert_eq!(position.symbol, "700.HK");
2313        assert_eq!(position.symbol_name, "腾讯控股");
2314        assert_eq!(position.currency, "HK");
2315        assert_eq!(position.quantity, decimal!(650));
2316        assert_eq!(position.available_quantity, decimal!(-450));
2317        assert_eq!(position.cost_price, decimal!(457.53f32));
2318        assert_eq!(position.market, Market::HK);
2319
2320        let position = &channel.positions[1];
2321        assert_eq!(position.symbol, "9991.HK");
2322        assert_eq!(position.symbol_name, "宝尊电商-SW");
2323        assert_eq!(position.currency, "HK");
2324        assert_eq!(position.quantity, decimal!(200));
2325        assert_eq!(position.available_quantity, decimal!(0));
2326        assert_eq!(position.cost_price, decimal!(32.25f32));
2327        assert_eq!(position.init_quantity, None);
2328    }
2329
2330    #[test]
2331    fn cash_flow() {
2332        let data = r#"
2333        {
2334            "list": [
2335              {
2336                "transaction_flow_name": "BuyContract-Stocks",
2337                "direction": 1,
2338                "balance": "-248.60",
2339                "currency": "USD",
2340                "business_type": 1,
2341                "business_time": "1621507957",
2342                "symbol": "AAPL.US",
2343                "description": "AAPL"
2344              },
2345              {
2346                "transaction_flow_name": "BuyContract-Stocks",
2347                "direction": 1,
2348                "balance": "-125.16",
2349                "currency": "USD",
2350                "business_type": 2,
2351                "business_time": "1621504824",
2352                "symbol": "AAPL.US",
2353                "description": "AAPL"
2354              }
2355            ]
2356          }
2357          "#;
2358
2359        #[derive(Debug, Deserialize)]
2360        struct Response {
2361            list: Vec<CashFlow>,
2362        }
2363
2364        let resp: Response = serde_json::from_str(data).unwrap();
2365        assert_eq!(resp.list.len(), 2);
2366
2367        let cashflow = &resp.list[0];
2368        assert_eq!(cashflow.transaction_flow_name, "BuyContract-Stocks");
2369        assert_eq!(cashflow.direction, CashFlowDirection::Out);
2370        assert_eq!(cashflow.balance, decimal!(-248.60f32));
2371        assert_eq!(cashflow.currency, "USD");
2372        assert_eq!(cashflow.business_type, BalanceType::Cash);
2373        assert_eq!(cashflow.business_time, datetime!(2021-05-20 18:52:37 +8));
2374        assert_eq!(cashflow.symbol.as_deref(), Some("AAPL.US"));
2375        assert_eq!(cashflow.description, "AAPL");
2376
2377        let cashflow = &resp.list[1];
2378        assert_eq!(cashflow.transaction_flow_name, "BuyContract-Stocks");
2379        assert_eq!(cashflow.direction, CashFlowDirection::Out);
2380        assert_eq!(cashflow.balance, decimal!(-125.16f32));
2381        assert_eq!(cashflow.currency, "USD");
2382        assert_eq!(cashflow.business_type, BalanceType::Stock);
2383        assert_eq!(cashflow.business_time, datetime!(2021-05-20 18:00:24 +8));
2384        assert_eq!(cashflow.symbol.as_deref(), Some("AAPL.US"));
2385        assert_eq!(cashflow.description, "AAPL");
2386    }
2387
2388    #[test]
2389    fn account_balance() {
2390        let data = r#"
2391        {
2392            "list": [
2393              {
2394                "total_cash": "1759070010.72",
2395                "max_finance_amount": "977582000",
2396                "remaining_finance_amount": "0",
2397                "risk_level": "1",
2398                "margin_call": "2598051051.50",
2399                "currency": "HKD",
2400                "cash_infos": [
2401                  {
2402                    "withdraw_cash": "97592.30",
2403                    "available_cash": "195902464.37",
2404                    "frozen_cash": "11579339.13",
2405                    "settling_cash": "207288537.81",
2406                    "currency": "HKD"
2407                  },
2408                  {
2409                    "withdraw_cash": "199893416.74",
2410                    "available_cash": "199893416.74",
2411                    "frozen_cash": "28723.76",
2412                    "settling_cash": "-276806.51",
2413                    "currency": "USD"
2414                  }
2415                ],
2416                "net_assets": "11111.12",
2417                "init_margin": "2222.23",
2418                "maintenance_margin": "3333.45",
2419                "buy_power": "1234.67",
2420                "frozen_transaction_fees": [
2421                    {
2422                        "currency": "HKD",
2423                        "frozen_transaction_fee": "123"
2424                    }
2425                ]
2426              }
2427            ]
2428          }"#;
2429
2430        #[derive(Debug, Deserialize)]
2431        struct Response {
2432            list: Vec<AccountBalance>,
2433        }
2434
2435        let resp: Response = serde_json::from_str(data).unwrap();
2436        assert_eq!(resp.list.len(), 1);
2437
2438        let balance = &resp.list[0];
2439        assert_eq!(balance.total_cash, "1759070010.72".parse().unwrap());
2440        assert_eq!(balance.max_finance_amount, "977582000".parse().unwrap());
2441        assert_eq!(balance.remaining_finance_amount, decimal!(0i32));
2442        assert_eq!(balance.risk_level, 1);
2443        assert_eq!(balance.margin_call, "2598051051.50".parse().unwrap());
2444        assert_eq!(balance.currency, "HKD");
2445        assert_eq!(balance.net_assets, "11111.12".parse().unwrap());
2446        assert_eq!(balance.init_margin, "2222.23".parse().unwrap());
2447        assert_eq!(balance.maintenance_margin, "3333.45".parse().unwrap());
2448        assert_eq!(balance.buy_power, "1234.67".parse().unwrap());
2449
2450        assert_eq!(balance.cash_infos.len(), 2);
2451
2452        let cash_info = &balance.cash_infos[0];
2453        assert_eq!(cash_info.withdraw_cash, "97592.30".parse().unwrap());
2454        assert_eq!(cash_info.available_cash, "195902464.37".parse().unwrap());
2455        assert_eq!(cash_info.frozen_cash, "11579339.13".parse().unwrap());
2456        assert_eq!(cash_info.settling_cash, "207288537.81".parse().unwrap());
2457        assert_eq!(cash_info.currency, "HKD");
2458
2459        let cash_info = &balance.cash_infos[1];
2460        assert_eq!(cash_info.withdraw_cash, "199893416.74".parse().unwrap());
2461        assert_eq!(cash_info.available_cash, "199893416.74".parse().unwrap());
2462        assert_eq!(cash_info.frozen_cash, "28723.76".parse().unwrap());
2463        assert_eq!(cash_info.settling_cash, "-276806.51".parse().unwrap());
2464        assert_eq!(cash_info.currency, "USD");
2465
2466        assert_eq!(balance.frozen_transaction_fees.len(), 1);
2467
2468        let frozen_transaction_fee = &balance.frozen_transaction_fees[0];
2469        assert_eq!(frozen_transaction_fee.currency, "HKD");
2470        assert_eq!(
2471            frozen_transaction_fee.frozen_transaction_fee,
2472            "123".parse().unwrap()
2473        );
2474    }
2475
2476    #[test]
2477    fn history_orders() {
2478        let data = r#"
2479        {
2480            "orders": [
2481              {
2482                "currency": "HKD",
2483                "executed_price": "0.000",
2484                "executed_quantity": "0",
2485                "expire_date": "",
2486                "last_done": "",
2487                "limit_offset": "",
2488                "msg": "",
2489                "order_id": "706388312699592704",
2490                "order_type": "ELO",
2491                "outside_rth": "UnknownOutsideRth",
2492                "price": "11.900",
2493                "quantity": "200",
2494                "side": "Buy",
2495                "status": "RejectedStatus",
2496                "stock_name": "Bank of East Asia Ltd/The",
2497                "submitted_at": "1651644897",
2498                "symbol": "23.HK",
2499                "tag": "Normal",
2500                "time_in_force": "Day",
2501                "trailing_amount": "",
2502                "trailing_percent": "",
2503                "trigger_at": "0",
2504                "trigger_price": "",
2505                "trigger_status": "NOT_USED",
2506                "updated_at": "1651644898",
2507                "limit_depth_level": 0,
2508                "trigger_count": 0,
2509                "monitor_price": "",
2510                "remark": "abc",
2511                "delayed_until": "1767225600"
2512              }
2513            ]
2514          }
2515        "#;
2516
2517        #[derive(Deserialize)]
2518        struct Response {
2519            orders: Vec<Order>,
2520        }
2521
2522        let resp: Response = serde_json::from_str(data).unwrap();
2523        assert_eq!(resp.orders.len(), 1);
2524
2525        let order = &resp.orders[0];
2526        assert_eq!(order.currency, "HKD");
2527        assert!(order.executed_price.is_none());
2528        assert_eq!(order.executed_quantity, decimal!(0));
2529        assert!(order.expire_date.is_none());
2530        assert!(order.last_done.is_none());
2531        assert!(order.limit_offset.is_none());
2532        assert_eq!(order.msg, "");
2533        assert_eq!(order.order_id, "706388312699592704");
2534        assert_eq!(order.order_type, OrderType::ELO);
2535        assert!(order.outside_rth.is_none());
2536        assert_eq!(order.price, Some("11.900".parse().unwrap()));
2537        assert_eq!(order.quantity, decimal!(200));
2538        assert_eq!(order.side, OrderSide::Buy);
2539        assert_eq!(order.status, OrderStatus::Rejected);
2540        assert_eq!(order.stock_name, "Bank of East Asia Ltd/The");
2541        assert_eq!(order.submitted_at, datetime!(2022-05-04 14:14:57 +8));
2542        assert_eq!(order.symbol, "23.HK");
2543        assert_eq!(order.tag, OrderTag::Normal);
2544        assert_eq!(order.time_in_force, TimeInForceType::Day);
2545        assert!(order.trailing_amount.is_none());
2546        assert!(order.trailing_percent.is_none());
2547        assert!(order.trigger_at.is_none());
2548        assert!(order.trigger_price.is_none());
2549        assert!(order.trigger_status.is_none());
2550        assert_eq!(order.updated_at, Some(datetime!(2022-05-04 14:14:58 +8)));
2551        assert_eq!(order.remark, "abc");
2552        assert_eq!(order.delayed_until, Some(datetime!(2026-01-01 00:00 UTC)));
2553    }
2554
2555    #[test]
2556    fn today_orders() {
2557        let data = r#"
2558        {
2559            "orders": [
2560              {
2561                "currency": "HKD",
2562                "executed_price": "0.000",
2563                "executed_quantity": "0",
2564                "expire_date": "",
2565                "last_done": "",
2566                "limit_offset": "",
2567                "msg": "",
2568                "order_id": "706388312699592704",
2569                "order_type": "ELO",
2570                "outside_rth": "UnknownOutsideRth",
2571                "price": "11.900",
2572                "quantity": "200",
2573                "side": "Buy",
2574                "status": "RejectedStatus",
2575                "stock_name": "Bank of East Asia Ltd/The",
2576                "submitted_at": "1651644897",
2577                "symbol": "23.HK",
2578                "tag": "Normal",
2579                "time_in_force": "Day",
2580                "trailing_amount": "",
2581                "trailing_percent": "",
2582                "trigger_at": "0",
2583                "trigger_price": "",
2584                "trigger_status": "NOT_USED",
2585                "updated_at": "1651644898",
2586                "limit_depth_level": 0,
2587                "trigger_count": 0,
2588                "monitor_price": "",
2589                "remark": "abc"
2590              }
2591            ]
2592          }
2593        "#;
2594
2595        #[derive(Deserialize)]
2596        struct Response {
2597            orders: Vec<Order>,
2598        }
2599
2600        let resp: Response = serde_json::from_str(data).unwrap();
2601        assert_eq!(resp.orders.len(), 1);
2602
2603        let order = &resp.orders[0];
2604        assert_eq!(order.currency, "HKD");
2605        assert!(order.executed_price.is_none());
2606        assert_eq!(order.executed_quantity, decimal!(0));
2607        assert!(order.expire_date.is_none());
2608        assert!(order.last_done.is_none());
2609        assert!(order.limit_offset.is_none());
2610        assert_eq!(order.msg, "");
2611        assert_eq!(order.order_id, "706388312699592704");
2612        assert_eq!(order.order_type, OrderType::ELO);
2613        assert!(order.outside_rth.is_none());
2614        assert_eq!(order.price, Some("11.900".parse().unwrap()));
2615        assert_eq!(order.quantity, decimal!(200));
2616        assert_eq!(order.side, OrderSide::Buy);
2617        assert_eq!(order.status, OrderStatus::Rejected);
2618        assert_eq!(order.stock_name, "Bank of East Asia Ltd/The");
2619        assert_eq!(order.submitted_at, datetime!(2022-05-04 14:14:57 +8));
2620        assert_eq!(order.symbol, "23.HK");
2621        assert_eq!(order.tag, OrderTag::Normal);
2622        assert_eq!(order.time_in_force, TimeInForceType::Day);
2623        assert!(order.trailing_amount.is_none());
2624        assert!(order.trailing_percent.is_none());
2625        assert!(order.trigger_at.is_none());
2626        assert!(order.trigger_price.is_none());
2627        assert!(order.trigger_status.is_none());
2628        assert_eq!(order.updated_at, Some(datetime!(2022-05-04 14:14:58 +8)));
2629        assert_eq!(order.remark, "abc");
2630        assert!(order.delayed_until.is_none());
2631    }
2632
2633    #[test]
2634    fn history_executions() {
2635        let data = r#"
2636        {
2637            "has_more": false,
2638            "trades": [
2639              {
2640                "order_id": "693664675163312128",
2641                "price": "388",
2642                "quantity": "100",
2643                "symbol": "700.HK",
2644                "trade_done_at": "1648611351",
2645                "trade_id": "693664675163312128-1648611351433741210",
2646                "side": "Buy"
2647              }
2648            ]
2649          }
2650        "#;
2651
2652        #[derive(Deserialize)]
2653        struct Response {
2654            trades: Vec<Execution>,
2655        }
2656
2657        let resp: Response = serde_json::from_str(data).unwrap();
2658        assert_eq!(resp.trades.len(), 1);
2659
2660        let execution = &resp.trades[0];
2661        assert_eq!(execution.order_id, "693664675163312128");
2662        assert_eq!(execution.price, "388".parse().unwrap());
2663        assert_eq!(execution.quantity, decimal!(100));
2664        assert_eq!(execution.symbol, "700.HK");
2665        assert_eq!(execution.trade_done_at, datetime!(2022-03-30 11:35:51 +8));
2666        assert_eq!(execution.trade_id, "693664675163312128-1648611351433741210");
2667        assert_eq!(execution.side, OrderSide::Buy);
2668    }
2669
2670    #[test]
2671    fn order_detail() {
2672        let data = r#"
2673        {
2674            "order_id": "828940451093708800",
2675            "status": "FilledStatus",
2676            "stock_name": "Apple",
2677            "quantity": "10",
2678            "executed_quantity": "10",
2679            "price": "200.000",
2680            "executed_price": "164.660",
2681            "submitted_at": "1680863604",
2682            "side": "Buy",
2683            "symbol": "AAPL.US",
2684            "order_type": "LO",
2685            "last_done": "164.660",
2686            "trigger_price": "0.0000",
2687            "msg": "",
2688            "tag": "Normal",
2689            "time_in_force": "Day",
2690            "expire_date": "2023-04-10",
2691            "updated_at": "1681113000",
2692            "trigger_at": "0",
2693            "trailing_amount": "",
2694            "trailing_percent": "",
2695            "limit_offset": "",
2696            "trigger_status": "NOT_USED",
2697            "outside_rth": "ANY_TIME",
2698            "currency": "USD",
2699            "limit_depth_level": 0,
2700            "trigger_count": 0,
2701            "monitor_price": "",
2702            "remark": "1680863603.927165",
2703            "delayed_until": "0",
2704            "free_status": "None",
2705            "free_amount": "",
2706            "free_currency": "",
2707            "deductions_status": "NONE",
2708            "deductions_amount": "",
2709            "deductions_currency": "",
2710            "platform_deducted_status": "NONE",
2711            "platform_deducted_amount": "",
2712            "platform_deducted_currency": "",
2713            "history": [{
2714                "price": "164.6600",
2715                "quantity": "10",
2716                "status": "FilledStatus",
2717                "msg": "Execution of 10",
2718                "time": "1681113000"
2719            }, {
2720                "price": "200.0000",
2721                "quantity": "10",
2722                "status": "NewStatus",
2723                "msg": "",
2724                "time": "1681113000"
2725            }],
2726            "charge_detail": {
2727                "items": [{
2728                    "code": "BROKER_FEES",
2729                    "name": "Broker Fees",
2730                    "fees": []
2731                }, {
2732                    "code": "THIRD_FEES",
2733                    "name": "Third-party Fees",
2734                    "fees": []
2735                }],
2736                "total_amount": "0",
2737                "currency": "USD"
2738            }
2739        }
2740        "#;
2741
2742        let detail = serde_json::from_str::<OrderDetail>(data).unwrap();
2743        assert!(
2744            detail.delayed_until.is_none(),
2745            "\"0\" means no time condition"
2746        );
2747
2748        let mut value: serde_json::Value = serde_json::from_str(data).unwrap();
2749        value["status"] = "DelayedNotReported".into();
2750        value["delayed_until"] = "1767225600".into();
2751        let detail = serde_json::from_value::<OrderDetail>(value).unwrap();
2752        assert_eq!(detail.status, OrderStatus::DelayedNotReported);
2753        assert_eq!(detail.delayed_until, Some(datetime!(2026-01-01 00:00 UTC)));
2754
2755        // The spec marks delayed_until nullable.
2756        let mut value: serde_json::Value = serde_json::from_str(data).unwrap();
2757        value["delayed_until"] = serde_json::Value::Null;
2758        let detail = serde_json::from_value::<OrderDetail>(value).unwrap();
2759        assert!(detail.delayed_until.is_none());
2760
2761        // A malformed value in this new field must not fail the whole response.
2762        for (raw, expected) in [
2763            (
2764                serde_json::json!(1767225600),
2765                Some(datetime!(2026-01-01 00:00 UTC)),
2766            ),
2767            (serde_json::json!("abc"), None),
2768            (serde_json::json!("99999999999999"), None),
2769            (serde_json::json!(""), None),
2770            (serde_json::json!("-5"), None),
2771        ] {
2772            let mut value: serde_json::Value = serde_json::from_str(data).unwrap();
2773            value["delayed_until"] = raw.clone();
2774            let detail = serde_json::from_value::<OrderDetail>(value).unwrap();
2775            assert_eq!(detail.delayed_until, expected, "{raw}");
2776        }
2777    }
2778
2779    #[test]
2780    fn attached_order_limit_depth_level() {
2781        let data = r#"
2782        {
2783            "order_id": "706388312699592705",
2784            "attached_type_display": "STOP_LOSS",
2785            "trigger_price": "10.500",
2786            "quantity": "200",
2787            "executed_qty": "0",
2788            "status": "NewStatus",
2789            "updated_at": "1651644898",
2790            "withdrawn": false,
2791            "gtd": "",
2792            "time_in_force": "Day",
2793            "counter_id": "",
2794            "trigger_status": "NOT_USED",
2795            "executed_amount": "0",
2796            "tag": "Normal",
2797            "submitted_at": "1651644897",
2798            "executed_price": "0.000",
2799            "force_only_rth": "RTH_ONLY",
2800            "reviewed": false,
2801            "activate_order_type": "MIT",
2802            "activate_rth": "RTH_ONLY",
2803            "submit_price": "",
2804            "limit_depth_level": 0
2805        }
2806        "#;
2807        for (raw, expected) in [
2808            (serde_json::json!(0), None),
2809            (serde_json::json!(3), Some(3)),
2810            (serde_json::json!(-2), Some(-2)),
2811            (serde_json::json!("3"), Some(3)),
2812            (serde_json::json!("abc"), None),
2813            (serde_json::Value::Null, None),
2814        ] {
2815            let mut value: serde_json::Value = serde_json::from_str(data).unwrap();
2816            value["limit_depth_level"] = raw.clone();
2817            let detail = serde_json::from_value::<AttachedOrderDetail>(value).unwrap();
2818            assert_eq!(detail.limit_depth_level, expected, "{raw}");
2819        }
2820
2821        // Responses that predate the field still deserialize.
2822        let mut value: serde_json::Value = serde_json::from_str(data).unwrap();
2823        value.as_object_mut().unwrap().remove("limit_depth_level");
2824        let detail = serde_json::from_value::<AttachedOrderDetail>(value).unwrap();
2825        assert!(detail.limit_depth_level.is_none());
2826    }
2827}