Skip to main content

rust_okx/api/
account.rs

1//! Authenticated account endpoints (`/api/v5/account/*`).
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::OkxClient;
6use crate::error::Error;
7use crate::model::{InstType, NumberString, PositionSide, TradeMode};
8use crate::transport::Transport;
9
10const BALANCE: &str = "/api/v5/account/balance";
11const POSITIONS: &str = "/api/v5/account/positions";
12const POSITION_RISK: &str = "/api/v5/account/account-position-risk";
13const ACCOUNT_CONFIG: &str = "/api/v5/account/config";
14const BILLS: &str = "/api/v5/account/bills";
15const BILLS_ARCHIVE: &str = "/api/v5/account/bills-archive";
16const SET_POSITION_MODE: &str = "/api/v5/account/set-position-mode";
17const SET_LEVERAGE: &str = "/api/v5/account/set-leverage";
18const GET_LEVERAGE: &str = "/api/v5/account/leverage-info";
19const MAX_ORDER_SIZE: &str = "/api/v5/account/max-size";
20const MAX_AVAILABLE_SIZE: &str = "/api/v5/account/max-avail-size";
21const ADJUST_MARGIN: &str = "/api/v5/account/position/margin-balance";
22const FEE_RATES: &str = "/api/v5/account/trade-fee";
23const ACCOUNT_INSTRUMENTS: &str = "/api/v5/account/instruments";
24const MAX_LOAN: &str = "/api/v5/account/max-loan";
25const INTEREST_ACCRUED: &str = "/api/v5/account/interest-accrued";
26const INTEREST_RATE: &str = "/api/v5/account/interest-rate";
27const SET_GREEKS: &str = "/api/v5/account/set-greeks";
28const SET_ISOLATED_MODE: &str = "/api/v5/account/set-isolated-mode";
29const MAX_WITHDRAWAL: &str = "/api/v5/account/max-withdrawal";
30const BORROW_REPAY: &str = "/api/v5/account/borrow-repay";
31const BORROW_REPAY_HISTORY: &str = "/api/v5/account/borrow-repay-history";
32const INTEREST_LIMITS: &str = "/api/v5/account/interest-limits";
33const SIMULATED_MARGIN: &str = "/api/v5/account/simulated_margin";
34const GREEKS: &str = "/api/v5/account/greeks";
35const POSITIONS_HISTORY: &str = "/api/v5/account/positions-history";
36const ACCOUNT_POSITION_TIERS: &str = "/api/v5/account/position-tiers";
37const RISK_STATE: &str = "/api/v5/account/risk-state";
38const SET_RISK_OFFSET_TYPE: &str = "/api/v5/account/set-riskOffset-type";
39const SET_AUTO_LOAN: &str = "/api/v5/account/set-auto-loan";
40const SET_ACCOUNT_LEVEL: &str = "/api/v5/account/set-account-level";
41const ACTIVATE_OPTION: &str = "/api/v5/account/activate-option";
42const POSITION_BUILDER: &str = "/api/v5/account/position-builder";
43
44/// Accessor for the authenticated account endpoints.
45///
46/// Obtain one via [`OkxClient::account`](crate::OkxClient::account). All methods
47/// require credentials; calling them without credentials returns
48/// [`Error::Configuration`].
49pub struct Account<'a, T> {
50    client: &'a OkxClient<T>,
51}
52
53impl<'a, T: Transport> Account<'a, T> {
54    pub(crate) fn new(client: &'a OkxClient<T>) -> Self {
55        Self { client }
56    }
57
58    /// Retrieve the trading-account balance.
59    ///
60    /// `GET /api/v5/account/balance`. Authenticated. Pass `ccy` to filter to one
61    /// or more comma-separated currencies (e.g. `Some("BTC,USDT")`). The result
62    /// is a single [`AccountBalance`].
63    ///
64    /// # Errors
65    ///
66    /// Returns [`Error::Configuration`] if no credentials are set,
67    /// [`Error::Api`] on a non-zero OKX code, or transport/decode errors.
68    pub async fn get_balance(&self, ccy: Option<&str>) -> Result<Vec<AccountBalance>, Error> {
69        let query = BalanceQuery { ccy };
70        self.client.get(BALANCE, &query, true).await
71    }
72
73    /// Retrieve open positions.
74    ///
75    /// `GET /api/v5/account/positions`. Authenticated. Both filters are
76    /// optional; omit them to return all positions.
77    ///
78    /// # Errors
79    ///
80    /// See [`get_balance`](Self::get_balance).
81    pub async fn get_positions(
82        &self,
83        inst_type: Option<InstType>,
84        inst_id: Option<&str>,
85    ) -> Result<Vec<Position>, Error> {
86        let query = PositionsQuery {
87            inst_type: inst_type.as_ref(),
88            inst_id,
89        };
90        self.client.get(POSITIONS, &query, true).await
91    }
92
93    /// Retrieve account position risk.
94    ///
95    /// `GET /api/v5/account/account-position-risk`. Authenticated.
96    ///
97    /// # Errors
98    ///
99    /// See [`get_balance`](Self::get_balance).
100    pub async fn get_position_risk(
101        &self,
102        inst_type: Option<InstType>,
103    ) -> Result<Vec<PositionRisk>, Error> {
104        let query = PositionRiskQuery {
105            inst_type: inst_type.as_ref(),
106        };
107        self.client.get(POSITION_RISK, &query, true).await
108    }
109
110    /// Retrieve account configuration.
111    ///
112    /// `GET /api/v5/account/config`. Authenticated.
113    ///
114    /// # Errors
115    ///
116    /// See [`get_balance`](Self::get_balance).
117    pub async fn get_account_config(&self) -> Result<Vec<AccountConfig>, Error> {
118        self.client.get(ACCOUNT_CONFIG, &NoQuery, true).await
119    }
120
121    /// Retrieve recent account bills.
122    ///
123    /// `GET /api/v5/account/bills`. Authenticated.
124    ///
125    /// # Errors
126    ///
127    /// See [`get_balance`](Self::get_balance).
128    pub async fn get_account_bills(
129        &self,
130        request: &BillsRequest,
131    ) -> Result<Vec<AccountBill>, Error> {
132        self.client.get(BILLS, request, true).await
133    }
134
135    /// Retrieve archived account bills.
136    ///
137    /// `GET /api/v5/account/bills-archive`. Authenticated.
138    ///
139    /// # Errors
140    ///
141    /// See [`get_balance`](Self::get_balance).
142    pub async fn get_account_bills_archive(
143        &self,
144        request: &BillsArchiveRequest,
145    ) -> Result<Vec<AccountBill>, Error> {
146        self.client.get(BILLS_ARCHIVE, request, true).await
147    }
148
149    /// Set the account position mode.
150    ///
151    /// `POST /api/v5/account/set-position-mode`. Authenticated.
152    ///
153    /// # Errors
154    ///
155    /// See [`get_balance`](Self::get_balance).
156    pub async fn set_position_mode(
157        &self,
158        pos_mode: &str,
159    ) -> Result<Vec<SetPositionModeResult>, Error> {
160        let body = SetPositionModeBody { pos_mode };
161        self.client.post(SET_POSITION_MODE, &body, true).await
162    }
163
164    /// Set leverage for an instrument or currency.
165    ///
166    /// `POST /api/v5/account/set-leverage`. Authenticated.
167    ///
168    /// # Errors
169    ///
170    /// See [`get_balance`](Self::get_balance).
171    pub async fn set_leverage(
172        &self,
173        request: &SetLeverageRequest,
174    ) -> Result<Vec<LeverageInfo>, Error> {
175        self.client.post(SET_LEVERAGE, request, true).await
176    }
177
178    /// Retrieve leverage settings.
179    ///
180    /// `GET /api/v5/account/leverage-info`. Authenticated.
181    ///
182    /// # Errors
183    ///
184    /// See [`get_balance`](Self::get_balance).
185    pub async fn get_leverage(
186        &self,
187        request: &LeverageRequest,
188    ) -> Result<Vec<LeverageInfo>, Error> {
189        self.client.get(GET_LEVERAGE, request, true).await
190    }
191
192    /// Retrieve maximum tradable size for an instrument.
193    ///
194    /// `GET /api/v5/account/max-size`. Authenticated.
195    ///
196    /// # Errors
197    ///
198    /// See [`get_balance`](Self::get_balance).
199    pub async fn get_max_order_size(
200        &self,
201        request: &MaxOrderSizeRequest,
202    ) -> Result<Vec<MaxOrderSize>, Error> {
203        self.client.get(MAX_ORDER_SIZE, request, true).await
204    }
205
206    /// Retrieve maximum available size for an instrument.
207    ///
208    /// `GET /api/v5/account/max-avail-size`. Authenticated.
209    ///
210    /// # Errors
211    ///
212    /// See [`get_balance`](Self::get_balance).
213    pub async fn get_max_avail_size(
214        &self,
215        request: &MaxAvailableSizeRequest,
216    ) -> Result<Vec<MaxAvailableSize>, Error> {
217        self.client.get(MAX_AVAILABLE_SIZE, request, true).await
218    }
219
220    /// Increase or decrease margin for a position.
221    ///
222    /// `POST /api/v5/account/position/margin-balance`. Authenticated.
223    ///
224    /// # Errors
225    ///
226    /// See [`get_balance`](Self::get_balance).
227    pub async fn adjust_margin(
228        &self,
229        request: &AdjustMarginRequest,
230    ) -> Result<Vec<AdjustMarginResult>, Error> {
231        self.client.post(ADJUST_MARGIN, request, true).await
232    }
233
234    /// Retrieve trade fee rates.
235    ///
236    /// `GET /api/v5/account/trade-fee`. Authenticated.
237    ///
238    /// # Errors
239    ///
240    /// See [`get_balance`](Self::get_balance).
241    pub async fn get_fee_rates(&self, request: &FeeRatesRequest) -> Result<Vec<FeeRate>, Error> {
242        self.client.get(FEE_RATES, request, true).await
243    }
244
245    /// Retrieve account-available instruments.
246    ///
247    /// `GET /api/v5/account/instruments`. Authenticated.
248    ///
249    /// # Errors
250    ///
251    /// See [`get_balance`](Self::get_balance).
252    pub async fn get_account_instruments(
253        &self,
254        request: &AccountInstrumentsRequest,
255    ) -> Result<Vec<AccountInstrument>, Error> {
256        self.client.get(ACCOUNT_INSTRUMENTS, request, true).await
257    }
258
259    /// Retrieve the maximum loan amount.
260    ///
261    /// `GET /api/v5/account/max-loan`. Authenticated.
262    ///
263    /// # Errors
264    ///
265    /// See [`get_balance`](Self::get_balance).
266    pub async fn get_max_loan(&self, request: &MaxLoanRequest) -> Result<Vec<MaxLoan>, Error> {
267        self.client.get(MAX_LOAN, request, true).await
268    }
269
270    /// Retrieve interest-accrued records.
271    ///
272    /// `GET /api/v5/account/interest-accrued`. Authenticated.
273    ///
274    /// # Errors
275    ///
276    /// See [`get_balance`](Self::get_balance).
277    pub async fn get_interest_accrued(
278        &self,
279        request: &InterestAccruedRequest,
280    ) -> Result<Vec<InterestAccrued>, Error> {
281        self.client.get(INTEREST_ACCRUED, request, true).await
282    }
283
284    /// Retrieve interest rates.
285    ///
286    /// `GET /api/v5/account/interest-rate`. Authenticated.
287    ///
288    /// # Errors
289    ///
290    /// See [`get_balance`](Self::get_balance).
291    pub async fn get_interest_rate(&self, ccy: Option<&str>) -> Result<Vec<InterestRate>, Error> {
292        let query = BalanceQuery { ccy };
293        self.client.get(INTEREST_RATE, &query, true).await
294    }
295
296    /// Set the greeks display type.
297    ///
298    /// `POST /api/v5/account/set-greeks`. Authenticated.
299    ///
300    /// # Errors
301    ///
302    /// See [`get_balance`](Self::get_balance).
303    pub async fn set_greeks(&self, greeks_type: &str) -> Result<Vec<SetGreeksResult>, Error> {
304        let body = SetGreeksBody { greeks_type };
305        self.client.post(SET_GREEKS, &body, true).await
306    }
307
308    /// Set isolated margin transfer mode.
309    ///
310    /// `POST /api/v5/account/set-isolated-mode`. Authenticated.
311    ///
312    /// # Errors
313    ///
314    /// See [`get_balance`](Self::get_balance).
315    pub async fn set_isolated_mode(
316        &self,
317        iso_mode: &str,
318        mode_type: &str,
319    ) -> Result<Vec<SetIsolatedModeResult>, Error> {
320        let body = SetIsolatedModeBody {
321            iso_mode,
322            mode_type,
323        };
324        self.client.post(SET_ISOLATED_MODE, &body, true).await
325    }
326
327    /// Retrieve maximum withdrawal amounts.
328    ///
329    /// `GET /api/v5/account/max-withdrawal`. Authenticated.
330    ///
331    /// # Errors
332    ///
333    /// See [`get_balance`](Self::get_balance).
334    pub async fn get_max_withdrawal(&self, ccy: Option<&str>) -> Result<Vec<MaxWithdrawal>, Error> {
335        let query = BalanceQuery { ccy };
336        self.client.get(MAX_WITHDRAWAL, &query, true).await
337    }
338
339    /// Borrow or repay margin.
340    ///
341    /// `POST /api/v5/account/borrow-repay`. Authenticated.
342    ///
343    /// # Errors
344    ///
345    /// See [`get_balance`](Self::get_balance).
346    pub async fn borrow_repay(
347        &self,
348        request: &BorrowRepayRequest,
349    ) -> Result<Vec<BorrowRepayResult>, Error> {
350        self.client.post(BORROW_REPAY, request, true).await
351    }
352
353    /// Retrieve borrow/repay history.
354    ///
355    /// `GET /api/v5/account/borrow-repay-history`. Authenticated.
356    ///
357    /// # Errors
358    ///
359    /// See [`get_balance`](Self::get_balance).
360    pub async fn get_borrow_repay_history(
361        &self,
362        request: &BorrowRepayHistoryRequest,
363    ) -> Result<Vec<BorrowRepayHistory>, Error> {
364        self.client.get(BORROW_REPAY_HISTORY, request, true).await
365    }
366
367    /// Retrieve borrowing rate and limit information.
368    ///
369    /// `GET /api/v5/account/interest-limits`. Authenticated.
370    ///
371    /// # Errors
372    ///
373    /// See [`get_balance`](Self::get_balance).
374    pub async fn get_interest_limits(
375        &self,
376        request: &InterestLimitsRequest,
377    ) -> Result<Vec<InterestLimit>, Error> {
378        self.client.get(INTEREST_LIMITS, request, true).await
379    }
380
381    /// Calculate simulated margin information.
382    ///
383    /// `POST /api/v5/account/simulated_margin`. Authenticated. This is separate
384    /// from [`OkxClientBuilder::demo_trading`](crate::OkxClientBuilder::demo_trading),
385    /// which only toggles the OKX simulated-trading header.
386    ///
387    /// # Errors
388    ///
389    /// See [`get_balance`](Self::get_balance).
390    pub async fn get_simulated_margin(
391        &self,
392        request: &SimulatedMarginRequest,
393    ) -> Result<Vec<SimulatedMargin>, Error> {
394        self.client.post(SIMULATED_MARGIN, request, true).await
395    }
396
397    /// Retrieve greeks.
398    ///
399    /// `GET /api/v5/account/greeks`. Authenticated.
400    ///
401    /// # Errors
402    ///
403    /// See [`get_balance`](Self::get_balance).
404    pub async fn get_greeks(&self, ccy: Option<&str>) -> Result<Vec<Greek>, Error> {
405        let query = BalanceQuery { ccy };
406        self.client.get(GREEKS, &query, true).await
407    }
408
409    /// Retrieve position history.
410    ///
411    /// `GET /api/v5/account/positions-history`. Authenticated.
412    ///
413    /// # Errors
414    ///
415    /// See [`get_balance`](Self::get_balance).
416    pub async fn get_positions_history(
417        &self,
418        request: &PositionsHistoryRequest,
419    ) -> Result<Vec<PositionHistory>, Error> {
420        self.client.get(POSITIONS_HISTORY, request, true).await
421    }
422
423    /// Retrieve account position tiers.
424    ///
425    /// `GET /api/v5/account/position-tiers`. Authenticated.
426    ///
427    /// # Errors
428    ///
429    /// See [`get_balance`](Self::get_balance).
430    pub async fn get_account_position_tiers(
431        &self,
432        request: &AccountPositionTiersRequest,
433    ) -> Result<Vec<AccountPositionTier>, Error> {
434        self.client.get(ACCOUNT_POSITION_TIERS, request, true).await
435    }
436
437    /// Retrieve the account risk state.
438    ///
439    /// `GET /api/v5/account/risk-state`. Authenticated.
440    ///
441    /// # Errors
442    ///
443    /// See [`get_balance`](Self::get_balance).
444    pub async fn get_risk_state(&self) -> Result<Vec<RiskState>, Error> {
445        self.client.get(RISK_STATE, &NoQuery, true).await
446    }
447
448    /// Set account risk offset type.
449    ///
450    /// `POST /api/v5/account/set-riskOffset-type`. Authenticated.
451    ///
452    /// # Errors
453    ///
454    /// See [`get_balance`](Self::get_balance).
455    pub async fn set_risk_offset_type(
456        &self,
457        risk_offset_type: &str,
458    ) -> Result<Vec<SetRiskOffsetTypeResult>, Error> {
459        let body = TypeBody {
460            value: risk_offset_type,
461        };
462        self.client.post(SET_RISK_OFFSET_TYPE, &body, true).await
463    }
464
465    /// Set account auto-loan mode.
466    ///
467    /// `POST /api/v5/account/set-auto-loan`. Authenticated.
468    ///
469    /// # Errors
470    ///
471    /// See [`get_balance`](Self::get_balance).
472    pub async fn set_auto_loan(&self, auto_loan: bool) -> Result<Vec<SetAutoLoanResult>, Error> {
473        let body = SetAutoLoanBody { auto_loan };
474        self.client.post(SET_AUTO_LOAN, &body, true).await
475    }
476
477    /// Set the account level.
478    ///
479    /// `POST /api/v5/account/set-account-level`. Authenticated.
480    ///
481    /// # Errors
482    ///
483    /// See [`get_balance`](Self::get_balance).
484    pub async fn set_account_level(
485        &self,
486        acct_lv: &str,
487    ) -> Result<Vec<SetAccountLevelResult>, Error> {
488        let body = SetAccountLevelBody { acct_lv };
489        self.client.post(SET_ACCOUNT_LEVEL, &body, true).await
490    }
491
492    /// Activate option trading.
493    ///
494    /// `POST /api/v5/account/activate-option`. Authenticated.
495    ///
496    /// # Errors
497    ///
498    /// See [`get_balance`](Self::get_balance).
499    pub async fn activate_option(&self) -> Result<Vec<ActivateOptionResult>, Error> {
500        self.client.post(ACTIVATE_OPTION, &EmptyBody {}, true).await
501    }
502
503    /// Build simulated positions and equity.
504    ///
505    /// `POST /api/v5/account/position-builder`. Authenticated.
506    ///
507    /// # Errors
508    ///
509    /// See [`get_balance`](Self::get_balance).
510    pub async fn position_builder(
511        &self,
512        request: &PositionBuilderRequest,
513    ) -> Result<Vec<PositionBuilderResult>, Error> {
514        self.client.post(POSITION_BUILDER, request, true).await
515    }
516}
517
518#[derive(Serialize)]
519struct NoQuery;
520
521#[derive(Serialize)]
522struct EmptyBody {}
523
524#[derive(Serialize)]
525struct BalanceQuery<'a> {
526    #[serde(skip_serializing_if = "Option::is_none")]
527    ccy: Option<&'a str>,
528}
529
530#[derive(Serialize)]
531struct PositionsQuery<'a> {
532    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
533    inst_type: Option<&'a InstType>,
534    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
535    inst_id: Option<&'a str>,
536}
537
538#[derive(Serialize)]
539struct PositionRiskQuery<'a> {
540    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
541    inst_type: Option<&'a InstType>,
542}
543
544#[derive(Serialize)]
545struct SetPositionModeBody<'a> {
546    #[serde(rename = "posMode")]
547    pos_mode: &'a str,
548}
549
550/// Query parameters for account bills.
551#[derive(Debug, Clone, Default, Serialize)]
552pub struct BillsRequest {
553    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
554    inst_type: Option<InstType>,
555    #[serde(skip_serializing_if = "Option::is_none")]
556    ccy: Option<String>,
557    #[serde(rename = "mgnMode", skip_serializing_if = "Option::is_none")]
558    mgn_mode: Option<TradeMode>,
559    #[serde(rename = "ctType", skip_serializing_if = "Option::is_none")]
560    ct_type: Option<String>,
561    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
562    bill_type: Option<String>,
563    #[serde(rename = "subType", skip_serializing_if = "Option::is_none")]
564    sub_type: Option<String>,
565    #[serde(skip_serializing_if = "Option::is_none")]
566    after: Option<String>,
567    #[serde(skip_serializing_if = "Option::is_none")]
568    before: Option<String>,
569    #[serde(skip_serializing_if = "Option::is_none")]
570    limit: Option<u32>,
571}
572
573impl BillsRequest {
574    /// Create an empty account-bills query.
575    pub fn new() -> Self {
576        Self::default()
577    }
578
579    /// Set the instrument type filter.
580    pub fn inst_type(mut self, inst_type: InstType) -> Self {
581        self.inst_type = Some(inst_type);
582        self
583    }
584
585    /// Set the currency filter.
586    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
587        self.ccy = Some(ccy.into());
588        self
589    }
590
591    /// Set the margin mode filter.
592    pub fn margin_mode(mut self, mgn_mode: TradeMode) -> Self {
593        self.mgn_mode = Some(mgn_mode);
594        self
595    }
596
597    /// Set the contract type filter.
598    pub fn contract_type(mut self, ct_type: impl Into<String>) -> Self {
599        self.ct_type = Some(ct_type.into());
600        self
601    }
602
603    /// Set the bill type filter.
604    pub fn bill_type(mut self, bill_type: impl Into<String>) -> Self {
605        self.bill_type = Some(bill_type.into());
606        self
607    }
608
609    /// Set the bill subtype filter.
610    pub fn sub_type(mut self, sub_type: impl Into<String>) -> Self {
611        self.sub_type = Some(sub_type.into());
612        self
613    }
614
615    /// Return records after this pagination cursor.
616    pub fn after(mut self, after: impl Into<String>) -> Self {
617        self.after = Some(after.into());
618        self
619    }
620
621    /// Return records before this pagination cursor.
622    pub fn before(mut self, before: impl Into<String>) -> Self {
623        self.before = Some(before.into());
624        self
625    }
626
627    /// Set the maximum number of rows to return.
628    pub fn limit(mut self, limit: u32) -> Self {
629        self.limit = Some(limit);
630        self
631    }
632}
633
634/// Query parameters for archived account bills.
635#[derive(Debug, Clone, Default, Serialize)]
636pub struct BillsArchiveRequest {
637    #[serde(flatten)]
638    base: BillsRequest,
639    #[serde(skip_serializing_if = "Option::is_none")]
640    begin: Option<String>,
641    #[serde(skip_serializing_if = "Option::is_none")]
642    end: Option<String>,
643}
644
645impl BillsArchiveRequest {
646    /// Create an empty archived-bills query.
647    pub fn new() -> Self {
648        Self::default()
649    }
650
651    /// Set the common bills filters.
652    pub fn filters(mut self, base: BillsRequest) -> Self {
653        self.base = base;
654        self
655    }
656
657    /// Set the begin timestamp.
658    pub fn begin(mut self, begin: impl Into<String>) -> Self {
659        self.begin = Some(begin.into());
660        self
661    }
662
663    /// Set the end timestamp.
664    pub fn end(mut self, end: impl Into<String>) -> Self {
665        self.end = Some(end.into());
666        self
667    }
668}
669
670/// Request body for setting leverage.
671#[derive(Debug, Clone, Serialize)]
672pub struct SetLeverageRequest {
673    lever: String,
674    #[serde(rename = "mgnMode")]
675    mgn_mode: TradeMode,
676    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
677    inst_id: Option<String>,
678    #[serde(skip_serializing_if = "Option::is_none")]
679    ccy: Option<String>,
680    #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
681    pos_side: Option<PositionSide>,
682}
683
684impl SetLeverageRequest {
685    /// Create a leverage-setting request.
686    pub fn new(lever: impl Into<String>, mgn_mode: TradeMode) -> Self {
687        Self {
688            lever: lever.into(),
689            mgn_mode,
690            inst_id: None,
691            ccy: None,
692            pos_side: None,
693        }
694    }
695
696    /// Set the instrument ID.
697    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
698        self.inst_id = Some(inst_id.into());
699        self
700    }
701
702    /// Set the currency.
703    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
704        self.ccy = Some(ccy.into());
705        self
706    }
707
708    /// Set the position side.
709    pub fn position_side(mut self, pos_side: PositionSide) -> Self {
710        self.pos_side = Some(pos_side);
711        self
712    }
713}
714
715/// Query parameters for retrieving leverage.
716#[derive(Debug, Clone, Serialize)]
717pub struct LeverageRequest {
718    #[serde(rename = "mgnMode")]
719    mgn_mode: TradeMode,
720    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
721    inst_id: Option<String>,
722    #[serde(skip_serializing_if = "Option::is_none")]
723    ccy: Option<String>,
724}
725
726impl LeverageRequest {
727    /// Create a leverage-info query.
728    pub fn new(mgn_mode: TradeMode) -> Self {
729        Self {
730            mgn_mode,
731            inst_id: None,
732            ccy: None,
733        }
734    }
735
736    /// Set the instrument ID.
737    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
738        self.inst_id = Some(inst_id.into());
739        self
740    }
741
742    /// Set the currency.
743    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
744        self.ccy = Some(ccy.into());
745        self
746    }
747}
748
749/// Query parameters for maximum order size.
750#[derive(Debug, Clone, Serialize)]
751pub struct MaxOrderSizeRequest {
752    #[serde(rename = "instId")]
753    inst_id: String,
754    #[serde(rename = "tdMode")]
755    td_mode: TradeMode,
756    #[serde(skip_serializing_if = "Option::is_none")]
757    ccy: Option<String>,
758    #[serde(skip_serializing_if = "Option::is_none")]
759    px: Option<String>,
760    #[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
761    trade_quote_ccy: Option<String>,
762}
763
764impl MaxOrderSizeRequest {
765    /// Create a maximum-order-size query.
766    pub fn new(inst_id: impl Into<String>, td_mode: TradeMode) -> Self {
767        Self {
768            inst_id: inst_id.into(),
769            td_mode,
770            ccy: None,
771            px: None,
772            trade_quote_ccy: None,
773        }
774    }
775
776    /// Set the currency.
777    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
778        self.ccy = Some(ccy.into());
779        self
780    }
781
782    /// Set the price.
783    pub fn price(mut self, px: impl Into<String>) -> Self {
784        self.px = Some(px.into());
785        self
786    }
787
788    /// Set the trade quote currency.
789    pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
790        self.trade_quote_ccy = Some(trade_quote_ccy.into());
791        self
792    }
793}
794
795/// Query parameters for maximum available size.
796#[derive(Debug, Clone, Serialize)]
797pub struct MaxAvailableSizeRequest {
798    #[serde(rename = "instId")]
799    inst_id: String,
800    #[serde(rename = "tdMode")]
801    td_mode: TradeMode,
802    #[serde(skip_serializing_if = "Option::is_none")]
803    ccy: Option<String>,
804    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
805    reduce_only: Option<bool>,
806    #[serde(rename = "unSpotOffset", skip_serializing_if = "Option::is_none")]
807    un_spot_offset: Option<bool>,
808    #[serde(rename = "quickMgnType", skip_serializing_if = "Option::is_none")]
809    quick_mgn_type: Option<String>,
810    #[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
811    trade_quote_ccy: Option<String>,
812}
813
814impl MaxAvailableSizeRequest {
815    /// Create a maximum-available-size query.
816    pub fn new(inst_id: impl Into<String>, td_mode: TradeMode) -> Self {
817        Self {
818            inst_id: inst_id.into(),
819            td_mode,
820            ccy: None,
821            reduce_only: None,
822            un_spot_offset: None,
823            quick_mgn_type: None,
824            trade_quote_ccy: None,
825        }
826    }
827
828    /// Set the currency.
829    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
830        self.ccy = Some(ccy.into());
831        self
832    }
833
834    /// Set the reduce-only filter.
835    pub fn reduce_only(mut self, reduce_only: bool) -> Self {
836        self.reduce_only = Some(reduce_only);
837        self
838    }
839
840    /// Set the spot offset flag.
841    pub fn un_spot_offset(mut self, un_spot_offset: bool) -> Self {
842        self.un_spot_offset = Some(un_spot_offset);
843        self
844    }
845
846    /// Set the quick margin type.
847    pub fn quick_margin_type(mut self, quick_mgn_type: impl Into<String>) -> Self {
848        self.quick_mgn_type = Some(quick_mgn_type.into());
849        self
850    }
851
852    /// Set the trade quote currency.
853    pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
854        self.trade_quote_ccy = Some(trade_quote_ccy.into());
855        self
856    }
857}
858
859/// Request body for adjusting position margin.
860#[derive(Debug, Clone, Serialize)]
861pub struct AdjustMarginRequest {
862    #[serde(rename = "instId")]
863    inst_id: String,
864    #[serde(rename = "posSide")]
865    pos_side: PositionSide,
866    #[serde(rename = "type")]
867    action: String,
868    amt: String,
869    #[serde(rename = "loanTrans", skip_serializing_if = "Option::is_none")]
870    loan_trans: Option<bool>,
871}
872
873impl AdjustMarginRequest {
874    /// Create a margin-adjustment request.
875    pub fn new(
876        inst_id: impl Into<String>,
877        pos_side: PositionSide,
878        action: impl Into<String>,
879        amt: impl Into<String>,
880    ) -> Self {
881        Self {
882            inst_id: inst_id.into(),
883            pos_side,
884            action: action.into(),
885            amt: amt.into(),
886            loan_trans: None,
887        }
888    }
889
890    /// Set whether to allow loan transfer.
891    pub fn loan_transfer(mut self, loan_trans: bool) -> Self {
892        self.loan_trans = Some(loan_trans);
893        self
894    }
895}
896
897/// Query parameters for trade fee rates.
898#[derive(Debug, Clone, Serialize)]
899pub struct FeeRatesRequest {
900    #[serde(rename = "instType")]
901    inst_type: InstType,
902    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
903    inst_id: Option<String>,
904    #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
905    underlying: Option<String>,
906    #[serde(skip_serializing_if = "Option::is_none")]
907    category: Option<String>,
908    #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
909    inst_family: Option<String>,
910}
911
912impl FeeRatesRequest {
913    /// Create a fee-rates query.
914    pub fn new(inst_type: InstType) -> Self {
915        Self {
916            inst_type,
917            inst_id: None,
918            underlying: None,
919            category: None,
920            inst_family: None,
921        }
922    }
923
924    /// Set the instrument ID.
925    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
926        self.inst_id = Some(inst_id.into());
927        self
928    }
929
930    /// Set the underlying.
931    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
932        self.underlying = Some(underlying.into());
933        self
934    }
935
936    /// Set the fee category.
937    pub fn category(mut self, category: impl Into<String>) -> Self {
938        self.category = Some(category.into());
939        self
940    }
941
942    /// Set the instrument family.
943    pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
944        self.inst_family = Some(inst_family.into());
945        self
946    }
947}
948
949/// Query parameters for account instruments.
950#[derive(Debug, Clone, Default, Serialize)]
951pub struct AccountInstrumentsRequest {
952    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
953    inst_type: Option<InstType>,
954    #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
955    underlying: Option<String>,
956    #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
957    inst_family: Option<String>,
958    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
959    inst_id: Option<String>,
960}
961
962impl AccountInstrumentsRequest {
963    /// Create an empty account-instruments query.
964    pub fn new() -> Self {
965        Self::default()
966    }
967
968    /// Set the instrument type filter.
969    pub fn inst_type(mut self, inst_type: InstType) -> Self {
970        self.inst_type = Some(inst_type);
971        self
972    }
973
974    /// Set the underlying filter.
975    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
976        self.underlying = Some(underlying.into());
977        self
978    }
979
980    /// Set the instrument family filter.
981    pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
982        self.inst_family = Some(inst_family.into());
983        self
984    }
985
986    /// Set the instrument ID filter.
987    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
988        self.inst_id = Some(inst_id.into());
989        self
990    }
991}
992
993/// Query parameters for maximum loan.
994#[derive(Debug, Clone, Serialize)]
995pub struct MaxLoanRequest {
996    #[serde(rename = "instId")]
997    inst_id: String,
998    #[serde(rename = "mgnMode")]
999    mgn_mode: TradeMode,
1000    #[serde(rename = "mgnCcy", skip_serializing_if = "Option::is_none")]
1001    mgn_ccy: Option<String>,
1002    #[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
1003    trade_quote_ccy: Option<String>,
1004}
1005
1006impl MaxLoanRequest {
1007    /// Create a maximum-loan query.
1008    pub fn new(inst_id: impl Into<String>, mgn_mode: TradeMode) -> Self {
1009        Self {
1010            inst_id: inst_id.into(),
1011            mgn_mode,
1012            mgn_ccy: None,
1013            trade_quote_ccy: None,
1014        }
1015    }
1016
1017    /// Set the margin currency.
1018    pub fn margin_currency(mut self, mgn_ccy: impl Into<String>) -> Self {
1019        self.mgn_ccy = Some(mgn_ccy.into());
1020        self
1021    }
1022
1023    /// Set the trade quote currency.
1024    pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
1025        self.trade_quote_ccy = Some(trade_quote_ccy.into());
1026        self
1027    }
1028}
1029
1030/// Query parameters for interest-accrued records.
1031#[derive(Debug, Clone, Default, Serialize)]
1032pub struct InterestAccruedRequest {
1033    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
1034    inst_id: Option<String>,
1035    #[serde(skip_serializing_if = "Option::is_none")]
1036    ccy: Option<String>,
1037    #[serde(rename = "mgnMode", skip_serializing_if = "Option::is_none")]
1038    mgn_mode: Option<TradeMode>,
1039    #[serde(skip_serializing_if = "Option::is_none")]
1040    after: Option<String>,
1041    #[serde(skip_serializing_if = "Option::is_none")]
1042    before: Option<String>,
1043    #[serde(skip_serializing_if = "Option::is_none")]
1044    limit: Option<u32>,
1045}
1046
1047impl InterestAccruedRequest {
1048    /// Create an empty interest-accrued query.
1049    pub fn new() -> Self {
1050        Self::default()
1051    }
1052
1053    /// Set the instrument ID filter.
1054    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
1055        self.inst_id = Some(inst_id.into());
1056        self
1057    }
1058
1059    /// Set the currency filter.
1060    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
1061        self.ccy = Some(ccy.into());
1062        self
1063    }
1064
1065    /// Set the margin mode filter.
1066    pub fn margin_mode(mut self, mgn_mode: TradeMode) -> Self {
1067        self.mgn_mode = Some(mgn_mode);
1068        self
1069    }
1070
1071    /// Return records after this pagination cursor.
1072    pub fn after(mut self, after: impl Into<String>) -> Self {
1073        self.after = Some(after.into());
1074        self
1075    }
1076
1077    /// Return records before this pagination cursor.
1078    pub fn before(mut self, before: impl Into<String>) -> Self {
1079        self.before = Some(before.into());
1080        self
1081    }
1082
1083    /// Set the maximum number of rows to return.
1084    pub fn limit(mut self, limit: u32) -> Self {
1085        self.limit = Some(limit);
1086        self
1087    }
1088}
1089
1090#[derive(Serialize)]
1091struct SetGreeksBody<'a> {
1092    #[serde(rename = "greeksType")]
1093    greeks_type: &'a str,
1094}
1095
1096#[derive(Serialize)]
1097struct SetIsolatedModeBody<'a> {
1098    #[serde(rename = "isoMode")]
1099    iso_mode: &'a str,
1100    #[serde(rename = "type")]
1101    mode_type: &'a str,
1102}
1103
1104/// Request body for borrow/repay.
1105#[derive(Debug, Clone, Serialize)]
1106pub struct BorrowRepayRequest {
1107    ccy: String,
1108    side: String,
1109    amt: String,
1110    #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
1111    ord_id: Option<String>,
1112}
1113
1114impl BorrowRepayRequest {
1115    /// Create a borrow/repay request.
1116    pub fn new(ccy: impl Into<String>, side: impl Into<String>, amt: impl Into<String>) -> Self {
1117        Self {
1118            ccy: ccy.into(),
1119            side: side.into(),
1120            amt: amt.into(),
1121            ord_id: None,
1122        }
1123    }
1124
1125    /// Set the related order ID.
1126    pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
1127        self.ord_id = Some(ord_id.into());
1128        self
1129    }
1130}
1131
1132/// Query parameters for borrow/repay history.
1133#[derive(Debug, Clone, Default, Serialize)]
1134pub struct BorrowRepayHistoryRequest {
1135    #[serde(skip_serializing_if = "Option::is_none")]
1136    ccy: Option<String>,
1137    #[serde(skip_serializing_if = "Option::is_none")]
1138    after: Option<String>,
1139    #[serde(skip_serializing_if = "Option::is_none")]
1140    before: Option<String>,
1141    #[serde(skip_serializing_if = "Option::is_none")]
1142    limit: Option<u32>,
1143}
1144
1145impl BorrowRepayHistoryRequest {
1146    /// Create an empty borrow/repay-history query.
1147    pub fn new() -> Self {
1148        Self::default()
1149    }
1150
1151    /// Set the currency filter.
1152    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
1153        self.ccy = Some(ccy.into());
1154        self
1155    }
1156
1157    /// Return records after this pagination cursor.
1158    pub fn after(mut self, after: impl Into<String>) -> Self {
1159        self.after = Some(after.into());
1160        self
1161    }
1162
1163    /// Return records before this pagination cursor.
1164    pub fn before(mut self, before: impl Into<String>) -> Self {
1165        self.before = Some(before.into());
1166        self
1167    }
1168
1169    /// Set the maximum number of rows to return.
1170    pub fn limit(mut self, limit: u32) -> Self {
1171        self.limit = Some(limit);
1172        self
1173    }
1174}
1175
1176/// Query parameters for interest limits.
1177#[derive(Debug, Clone, Default, Serialize)]
1178pub struct InterestLimitsRequest {
1179    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
1180    limit_type: Option<String>,
1181    #[serde(skip_serializing_if = "Option::is_none")]
1182    ccy: Option<String>,
1183}
1184
1185impl InterestLimitsRequest {
1186    /// Create an empty interest-limits query.
1187    pub fn new() -> Self {
1188        Self::default()
1189    }
1190
1191    /// Set the OKX interest-limit type.
1192    pub fn limit_type(mut self, limit_type: impl Into<String>) -> Self {
1193        self.limit_type = Some(limit_type.into());
1194        self
1195    }
1196
1197    /// Set the currency filter.
1198    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
1199        self.ccy = Some(ccy.into());
1200        self
1201    }
1202}
1203
1204/// A simulated position used by position-builder and simulated-margin requests.
1205#[derive(Debug, Clone, Serialize)]
1206pub struct SimulatedPosition {
1207    #[serde(rename = "instId")]
1208    inst_id: String,
1209    #[serde(skip_serializing_if = "Option::is_none")]
1210    pos: Option<String>,
1211    #[serde(rename = "avgPx", skip_serializing_if = "Option::is_none")]
1212    avg_px: Option<String>,
1213    #[serde(skip_serializing_if = "Option::is_none")]
1214    lever: Option<String>,
1215}
1216
1217impl SimulatedPosition {
1218    /// Create a simulated position for an instrument.
1219    pub fn new(inst_id: impl Into<String>) -> Self {
1220        Self {
1221            inst_id: inst_id.into(),
1222            pos: None,
1223            avg_px: None,
1224            lever: None,
1225        }
1226    }
1227
1228    /// Set the simulated position size.
1229    pub fn position(mut self, pos: impl Into<String>) -> Self {
1230        self.pos = Some(pos.into());
1231        self
1232    }
1233
1234    /// Set the simulated average price.
1235    pub fn average_price(mut self, avg_px: impl Into<String>) -> Self {
1236        self.avg_px = Some(avg_px.into());
1237        self
1238    }
1239
1240    /// Set the simulated leverage.
1241    pub fn leverage(mut self, lever: impl Into<String>) -> Self {
1242        self.lever = Some(lever.into());
1243        self
1244    }
1245}
1246
1247/// A simulated asset used by position-builder requests.
1248#[derive(Debug, Clone, Serialize)]
1249pub struct SimulatedAsset {
1250    ccy: String,
1251    #[serde(skip_serializing_if = "Option::is_none")]
1252    eq: Option<String>,
1253}
1254
1255impl SimulatedAsset {
1256    /// Create a simulated asset for a currency.
1257    pub fn new(ccy: impl Into<String>) -> Self {
1258        Self {
1259            ccy: ccy.into(),
1260            eq: None,
1261        }
1262    }
1263
1264    /// Set the simulated equity.
1265    pub fn equity(mut self, eq: impl Into<String>) -> Self {
1266        self.eq = Some(eq.into());
1267        self
1268    }
1269}
1270
1271/// Request body for simulated margin calculation.
1272#[derive(Debug, Clone, Default, Serialize)]
1273pub struct SimulatedMarginRequest {
1274    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
1275    inst_type: Option<InstType>,
1276    #[serde(rename = "inclRealPos", skip_serializing_if = "Option::is_none")]
1277    include_real_positions: Option<bool>,
1278    #[serde(rename = "spotOffsetType", skip_serializing_if = "Option::is_none")]
1279    spot_offset_type: Option<String>,
1280    #[serde(rename = "simPos", skip_serializing_if = "Option::is_none")]
1281    simulated_positions: Option<Vec<SimulatedPosition>>,
1282}
1283
1284impl SimulatedMarginRequest {
1285    /// Create an empty simulated-margin request.
1286    pub fn new() -> Self {
1287        Self::default()
1288    }
1289
1290    /// Set the instrument type.
1291    pub fn inst_type(mut self, inst_type: InstType) -> Self {
1292        self.inst_type = Some(inst_type);
1293        self
1294    }
1295
1296    /// Set whether real positions and equity are included.
1297    pub fn include_real_positions(mut self, include_real_positions: bool) -> Self {
1298        self.include_real_positions = Some(include_real_positions);
1299        self
1300    }
1301
1302    /// Set the spot offset type.
1303    pub fn spot_offset_type(mut self, spot_offset_type: impl Into<String>) -> Self {
1304        self.spot_offset_type = Some(spot_offset_type.into());
1305        self
1306    }
1307
1308    /// Set simulated positions.
1309    pub fn simulated_positions(mut self, simulated_positions: Vec<SimulatedPosition>) -> Self {
1310        self.simulated_positions = Some(simulated_positions);
1311        self
1312    }
1313}
1314
1315/// Query parameters for account position tiers.
1316#[derive(Debug, Clone, Default, Serialize)]
1317pub struct AccountPositionTiersRequest {
1318    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
1319    inst_type: Option<InstType>,
1320    #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
1321    underlying: Option<String>,
1322    #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
1323    inst_family: Option<String>,
1324}
1325
1326impl AccountPositionTiersRequest {
1327    /// Create an empty account position-tiers query.
1328    pub fn new() -> Self {
1329        Self::default()
1330    }
1331
1332    /// Set the instrument type filter.
1333    pub fn inst_type(mut self, inst_type: InstType) -> Self {
1334        self.inst_type = Some(inst_type);
1335        self
1336    }
1337
1338    /// Set the underlying filter.
1339    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
1340        self.underlying = Some(underlying.into());
1341        self
1342    }
1343
1344    /// Set the instrument family filter.
1345    pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
1346        self.inst_family = Some(inst_family.into());
1347        self
1348    }
1349}
1350
1351#[derive(Serialize)]
1352struct TypeBody<'a> {
1353    #[serde(rename = "type")]
1354    value: &'a str,
1355}
1356
1357#[derive(Serialize)]
1358struct SetAutoLoanBody {
1359    #[serde(rename = "autoLoan")]
1360    auto_loan: bool,
1361}
1362
1363#[derive(Serialize)]
1364struct SetAccountLevelBody<'a> {
1365    #[serde(rename = "acctLv")]
1366    acct_lv: &'a str,
1367}
1368
1369/// Request body for position builder.
1370#[derive(Debug, Clone, Default, Serialize)]
1371pub struct PositionBuilderRequest {
1372    #[serde(rename = "acctLv", skip_serializing_if = "Option::is_none")]
1373    acct_lv: Option<String>,
1374    #[serde(rename = "inclRealPosAndEq", skip_serializing_if = "Option::is_none")]
1375    include_real_positions_and_equity: Option<bool>,
1376    #[serde(skip_serializing_if = "Option::is_none")]
1377    lever: Option<String>,
1378    #[serde(rename = "greeksType", skip_serializing_if = "Option::is_none")]
1379    greeks_type: Option<String>,
1380    #[serde(rename = "simPos", skip_serializing_if = "Option::is_none")]
1381    simulated_positions: Option<Vec<SimulatedPosition>>,
1382    #[serde(rename = "simAsset", skip_serializing_if = "Option::is_none")]
1383    simulated_assets: Option<Vec<SimulatedAsset>>,
1384    #[serde(rename = "idxVol", skip_serializing_if = "Option::is_none")]
1385    index_volatility: Option<String>,
1386}
1387
1388impl PositionBuilderRequest {
1389    /// Create an empty position-builder request.
1390    pub fn new() -> Self {
1391        Self::default()
1392    }
1393
1394    /// Set the account level.
1395    pub fn account_level(mut self, acct_lv: impl Into<String>) -> Self {
1396        self.acct_lv = Some(acct_lv.into());
1397        self
1398    }
1399
1400    /// Set whether real positions and equity are included.
1401    pub fn include_real_positions_and_equity(mut self, include: bool) -> Self {
1402        self.include_real_positions_and_equity = Some(include);
1403        self
1404    }
1405
1406    /// Set leverage.
1407    pub fn leverage(mut self, lever: impl Into<String>) -> Self {
1408        self.lever = Some(lever.into());
1409        self
1410    }
1411
1412    /// Set greeks display type.
1413    pub fn greeks_type(mut self, greeks_type: impl Into<String>) -> Self {
1414        self.greeks_type = Some(greeks_type.into());
1415        self
1416    }
1417
1418    /// Set simulated positions.
1419    pub fn simulated_positions(mut self, simulated_positions: Vec<SimulatedPosition>) -> Self {
1420        self.simulated_positions = Some(simulated_positions);
1421        self
1422    }
1423
1424    /// Set simulated assets.
1425    pub fn simulated_assets(mut self, simulated_assets: Vec<SimulatedAsset>) -> Self {
1426        self.simulated_assets = Some(simulated_assets);
1427        self
1428    }
1429
1430    /// Set index volatility.
1431    pub fn index_volatility(mut self, index_volatility: impl Into<String>) -> Self {
1432        self.index_volatility = Some(index_volatility.into());
1433        self
1434    }
1435}
1436
1437/// Query parameters for position history.
1438#[derive(Debug, Clone, Default, Serialize)]
1439pub struct PositionsHistoryRequest {
1440    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
1441    inst_type: Option<InstType>,
1442    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
1443    inst_id: Option<String>,
1444    #[serde(rename = "mgnMode", skip_serializing_if = "Option::is_none")]
1445    mgn_mode: Option<TradeMode>,
1446    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
1447    close_type: Option<String>,
1448    #[serde(rename = "posId", skip_serializing_if = "Option::is_none")]
1449    pos_id: Option<String>,
1450    #[serde(skip_serializing_if = "Option::is_none")]
1451    after: Option<String>,
1452    #[serde(skip_serializing_if = "Option::is_none")]
1453    before: Option<String>,
1454    #[serde(skip_serializing_if = "Option::is_none")]
1455    limit: Option<u32>,
1456}
1457
1458impl PositionsHistoryRequest {
1459    /// Create an empty position-history query.
1460    pub fn new() -> Self {
1461        Self::default()
1462    }
1463
1464    /// Set the instrument type filter.
1465    pub fn inst_type(mut self, inst_type: InstType) -> Self {
1466        self.inst_type = Some(inst_type);
1467        self
1468    }
1469
1470    /// Set the instrument ID filter.
1471    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
1472        self.inst_id = Some(inst_id.into());
1473        self
1474    }
1475
1476    /// Set the margin mode filter.
1477    pub fn margin_mode(mut self, mgn_mode: TradeMode) -> Self {
1478        self.mgn_mode = Some(mgn_mode);
1479        self
1480    }
1481
1482    /// Set the OKX close type filter.
1483    pub fn close_type(mut self, close_type: impl Into<String>) -> Self {
1484        self.close_type = Some(close_type.into());
1485        self
1486    }
1487
1488    /// Set the position ID filter.
1489    pub fn position_id(mut self, pos_id: impl Into<String>) -> Self {
1490        self.pos_id = Some(pos_id.into());
1491        self
1492    }
1493
1494    /// Return records after this pagination cursor.
1495    pub fn after(mut self, after: impl Into<String>) -> Self {
1496        self.after = Some(after.into());
1497        self
1498    }
1499
1500    /// Return records before this pagination cursor.
1501    pub fn before(mut self, before: impl Into<String>) -> Self {
1502        self.before = Some(before.into());
1503        self
1504    }
1505
1506    /// Set the maximum number of rows to return.
1507    pub fn limit(mut self, limit: u32) -> Self {
1508        self.limit = Some(limit);
1509        self
1510    }
1511}
1512
1513/// The trading-account balance summary.
1514#[derive(Debug, Clone, Deserialize)]
1515#[serde(rename_all = "camelCase")]
1516#[non_exhaustive]
1517pub struct AccountBalance {
1518    /// Total equity in USD.
1519    #[serde(default)]
1520    pub total_eq: NumberString,
1521    /// Adjusted / effective equity in USD.
1522    #[serde(default)]
1523    pub adj_eq: NumberString,
1524    /// Per-currency balance details.
1525    #[serde(default)]
1526    pub details: Vec<BalanceDetail>,
1527    /// Last update time (Unix milliseconds).
1528    #[serde(default)]
1529    pub u_time: NumberString,
1530}
1531
1532/// Balance details for a single currency.
1533#[derive(Debug, Clone, Deserialize)]
1534#[serde(rename_all = "camelCase")]
1535#[non_exhaustive]
1536pub struct BalanceDetail {
1537    /// Currency, e.g. `USDT`.
1538    pub ccy: String,
1539    /// Equity of the currency.
1540    #[serde(default)]
1541    pub eq: NumberString,
1542    /// Cash balance.
1543    #[serde(default)]
1544    pub cash_bal: NumberString,
1545    /// Available balance.
1546    #[serde(default)]
1547    pub avail_bal: NumberString,
1548    /// Frozen balance.
1549    #[serde(default)]
1550    pub frozen_bal: NumberString,
1551}
1552
1553/// An open position.
1554#[derive(Debug, Clone, Deserialize)]
1555#[serde(rename_all = "camelCase")]
1556#[non_exhaustive]
1557pub struct Position {
1558    /// Instrument type.
1559    pub inst_type: InstType,
1560    /// Instrument ID.
1561    pub inst_id: String,
1562    /// Position ID.
1563    #[serde(default)]
1564    pub pos_id: String,
1565    /// Position side.
1566    pub pos_side: PositionSide,
1567    /// Margin mode.
1568    pub mgn_mode: TradeMode,
1569    /// Quantity of positions.
1570    #[serde(default)]
1571    pub pos: NumberString,
1572    /// Average open price.
1573    #[serde(default)]
1574    pub avg_px: NumberString,
1575    /// Unrealized profit and loss.
1576    #[serde(default)]
1577    pub upl: NumberString,
1578    /// Leverage.
1579    #[serde(default)]
1580    pub lever: NumberString,
1581    /// Estimated liquidation price.
1582    #[serde(default)]
1583    pub liq_px: NumberString,
1584}
1585
1586/// Account position-risk snapshot.
1587#[derive(Debug, Clone, Deserialize)]
1588#[serde(rename_all = "camelCase")]
1589#[non_exhaustive]
1590pub struct PositionRisk {
1591    /// Adjusted/effective equity in USD.
1592    #[serde(default)]
1593    pub adj_eq: NumberString,
1594    /// Balance data included in the risk snapshot.
1595    #[serde(default)]
1596    pub bal_data: Vec<BalanceDetail>,
1597    /// Position data included in the risk snapshot.
1598    #[serde(default)]
1599    pub pos_data: Vec<Position>,
1600    /// Timestamp (Unix milliseconds).
1601    #[serde(default)]
1602    pub ts: NumberString,
1603}
1604
1605/// Account configuration.
1606#[derive(Debug, Clone, Deserialize)]
1607#[serde(rename_all = "camelCase")]
1608#[non_exhaustive]
1609pub struct AccountConfig {
1610    /// Account ID.
1611    #[serde(default)]
1612    pub uid: String,
1613    /// Account level.
1614    #[serde(default)]
1615    pub acct_lv: String,
1616    /// Position mode.
1617    #[serde(default)]
1618    pub pos_mode: String,
1619    /// Greeks display type.
1620    #[serde(default)]
1621    pub greeks_type: String,
1622    /// Whether auto-borrow is enabled. OKX returns this as a JSON boolean.
1623    #[serde(default)]
1624    pub auto_loan: bool,
1625}
1626
1627/// Account bill row.
1628#[derive(Debug, Clone, Deserialize)]
1629#[serde(rename_all = "camelCase")]
1630#[non_exhaustive]
1631pub struct AccountBill {
1632    /// Bill ID.
1633    #[serde(default)]
1634    pub bill_id: String,
1635    /// Instrument type.
1636    #[serde(default)]
1637    pub inst_type: String,
1638    /// Instrument ID.
1639    #[serde(default)]
1640    pub inst_id: String,
1641    /// Currency.
1642    #[serde(default)]
1643    pub ccy: String,
1644    /// Margin mode.
1645    #[serde(default)]
1646    pub mgn_mode: String,
1647    /// Bill type.
1648    #[serde(rename = "type", default)]
1649    pub bill_type: String,
1650    /// Bill subtype.
1651    #[serde(default)]
1652    pub sub_type: String,
1653    /// Balance change.
1654    #[serde(default)]
1655    pub sz: NumberString,
1656    /// Balance after the change.
1657    #[serde(default)]
1658    pub bal: NumberString,
1659    /// Timestamp (Unix milliseconds).
1660    #[serde(default)]
1661    pub ts: NumberString,
1662}
1663
1664/// Result of setting position mode.
1665#[derive(Debug, Clone, Deserialize)]
1666#[serde(rename_all = "camelCase")]
1667#[non_exhaustive]
1668pub struct SetPositionModeResult {
1669    /// Position mode.
1670    #[serde(default)]
1671    pub pos_mode: String,
1672}
1673
1674/// Leverage information.
1675#[derive(Debug, Clone, Deserialize)]
1676#[serde(rename_all = "camelCase")]
1677#[non_exhaustive]
1678pub struct LeverageInfo {
1679    /// Instrument ID.
1680    #[serde(default)]
1681    pub inst_id: String,
1682    /// Margin mode.
1683    pub mgn_mode: TradeMode,
1684    /// Position side.
1685    pub pos_side: PositionSide,
1686    /// Leverage.
1687    #[serde(default)]
1688    pub lever: NumberString,
1689}
1690
1691/// Maximum order size information.
1692#[derive(Debug, Clone, Deserialize)]
1693#[serde(rename_all = "camelCase")]
1694#[non_exhaustive]
1695pub struct MaxOrderSize {
1696    /// Instrument ID.
1697    pub inst_id: String,
1698    /// Maximum buy size.
1699    #[serde(default)]
1700    pub max_buy: NumberString,
1701    /// Maximum sell size.
1702    #[serde(default)]
1703    pub max_sell: NumberString,
1704}
1705
1706/// Maximum available size information.
1707#[derive(Debug, Clone, Deserialize)]
1708#[serde(rename_all = "camelCase")]
1709#[non_exhaustive]
1710pub struct MaxAvailableSize {
1711    /// Instrument ID.
1712    pub inst_id: String,
1713    /// Available buy size.
1714    #[serde(default)]
1715    pub avail_buy: NumberString,
1716    /// Available sell size.
1717    #[serde(default)]
1718    pub avail_sell: NumberString,
1719}
1720
1721/// Trade fee-rate information.
1722#[derive(Debug, Clone, Deserialize)]
1723#[serde(rename_all = "camelCase")]
1724#[non_exhaustive]
1725pub struct FeeRate {
1726    /// Instrument type.
1727    pub inst_type: InstType,
1728    /// Instrument ID.
1729    #[serde(default)]
1730    pub inst_id: String,
1731    /// Fee category.
1732    #[serde(default)]
1733    pub category: String,
1734    /// Maker fee rate.
1735    #[serde(default)]
1736    pub maker: NumberString,
1737    /// Taker fee rate.
1738    #[serde(default)]
1739    pub taker: NumberString,
1740    /// Timestamp (Unix milliseconds).
1741    #[serde(default)]
1742    pub ts: NumberString,
1743}
1744
1745/// Maximum withdrawal amount for a currency.
1746#[derive(Debug, Clone, Deserialize)]
1747#[serde(rename_all = "camelCase")]
1748#[non_exhaustive]
1749pub struct MaxWithdrawal {
1750    /// Currency.
1751    pub ccy: String,
1752    /// Maximum withdrawal amount.
1753    #[serde(default)]
1754    pub max_wd: NumberString,
1755    /// Maximum withdrawal amount excluding borrowed amount.
1756    #[serde(default)]
1757    pub max_wd_ex: NumberString,
1758}
1759
1760/// Historical position row.
1761#[derive(Debug, Clone, Deserialize)]
1762#[serde(rename_all = "camelCase")]
1763#[non_exhaustive]
1764pub struct PositionHistory {
1765    /// Instrument type.
1766    pub inst_type: InstType,
1767    /// Instrument ID.
1768    pub inst_id: String,
1769    /// Position ID.
1770    #[serde(default)]
1771    pub pos_id: String,
1772    /// Margin mode.
1773    pub mgn_mode: TradeMode,
1774    /// Close type.
1775    #[serde(rename = "type", default)]
1776    pub close_type: String,
1777    /// Realized PnL.
1778    #[serde(default)]
1779    pub realized_pnl: NumberString,
1780    /// Created time (Unix milliseconds).
1781    #[serde(default)]
1782    pub c_time: NumberString,
1783    /// Updated time (Unix milliseconds).
1784    #[serde(default)]
1785    pub u_time: NumberString,
1786}
1787
1788/// Account risk state.
1789#[derive(Debug, Clone, Deserialize)]
1790#[serde(rename_all = "camelCase")]
1791#[non_exhaustive]
1792pub struct RiskState {
1793    /// Whether the account is currently at risk, as represented by OKX.
1794    #[serde(default)]
1795    pub at_risk: String,
1796    /// Timestamp (Unix milliseconds).
1797    #[serde(default)]
1798    pub ts: NumberString,
1799}
1800
1801/// Result of adding or reducing margin on a position.
1802#[derive(Debug, Clone, Deserialize)]
1803#[serde(rename_all = "camelCase")]
1804#[non_exhaustive]
1805pub struct AdjustMarginResult {
1806    /// Instrument ID.
1807    #[serde(default)]
1808    pub inst_id: String,
1809    /// Position side.
1810    #[serde(default)]
1811    pub pos_side: String,
1812    /// Adjustment amount.
1813    #[serde(default)]
1814    pub amt: NumberString,
1815    /// OKX adjustment type.
1816    #[serde(rename = "type", default)]
1817    pub adjustment_type: String,
1818}
1819
1820/// Account-level instrument configuration.
1821#[derive(Debug, Clone, Deserialize)]
1822#[serde(rename_all = "camelCase")]
1823#[non_exhaustive]
1824pub struct AccountInstrument {
1825    /// Instrument type.
1826    #[serde(default)]
1827    pub inst_type: String,
1828    /// Instrument ID.
1829    #[serde(default)]
1830    pub inst_id: String,
1831    /// Underlying.
1832    #[serde(default)]
1833    pub uly: String,
1834    /// Instrument family.
1835    #[serde(default)]
1836    pub inst_family: String,
1837    /// Base currency.
1838    #[serde(default)]
1839    pub base_ccy: String,
1840    /// Quote currency.
1841    #[serde(default)]
1842    pub quote_ccy: String,
1843    /// Settlement currency.
1844    #[serde(default)]
1845    pub settle_ccy: String,
1846}
1847
1848/// Maximum loan amount available for an instrument or currency.
1849#[derive(Debug, Clone, Deserialize)]
1850#[serde(rename_all = "camelCase")]
1851#[non_exhaustive]
1852pub struct MaxLoan {
1853    /// Instrument ID.
1854    #[serde(default)]
1855    pub inst_id: String,
1856    /// Margin mode.
1857    #[serde(default)]
1858    pub mgn_mode: String,
1859    /// Margin currency.
1860    #[serde(default)]
1861    pub mgn_ccy: String,
1862    /// Maximum loan amount.
1863    #[serde(default)]
1864    pub max_loan: NumberString,
1865}
1866
1867/// Interest accrued by account borrowing.
1868#[derive(Debug, Clone, Deserialize)]
1869#[serde(rename_all = "camelCase")]
1870#[non_exhaustive]
1871pub struct InterestAccrued {
1872    /// Instrument ID.
1873    #[serde(default)]
1874    pub inst_id: String,
1875    /// Currency.
1876    #[serde(default)]
1877    pub ccy: String,
1878    /// Margin mode.
1879    #[serde(default)]
1880    pub mgn_mode: String,
1881    /// Accrued interest.
1882    #[serde(default)]
1883    pub interest: NumberString,
1884    /// Interest rate.
1885    #[serde(default)]
1886    pub interest_rate: NumberString,
1887    /// Liability.
1888    #[serde(default)]
1889    pub liab: NumberString,
1890    /// Timestamp (Unix milliseconds).
1891    #[serde(default)]
1892    pub ts: NumberString,
1893}
1894
1895/// Account borrowing interest rate.
1896#[derive(Debug, Clone, Deserialize)]
1897#[serde(rename_all = "camelCase")]
1898#[non_exhaustive]
1899pub struct InterestRate {
1900    /// Currency.
1901    #[serde(default)]
1902    pub ccy: String,
1903    /// Interest rate.
1904    #[serde(default)]
1905    pub interest_rate: NumberString,
1906}
1907
1908/// Result of updating the greeks display type.
1909#[derive(Debug, Clone, Deserialize)]
1910#[serde(rename_all = "camelCase")]
1911#[non_exhaustive]
1912pub struct SetGreeksResult {
1913    /// Greeks display type.
1914    #[serde(default)]
1915    pub greeks_type: String,
1916}
1917
1918/// Result of updating isolated margin mode.
1919#[derive(Debug, Clone, Deserialize)]
1920#[serde(rename_all = "camelCase")]
1921#[non_exhaustive]
1922pub struct SetIsolatedModeResult {
1923    /// Isolated margin mode.
1924    #[serde(default)]
1925    pub iso_mode: String,
1926    /// OKX isolated-mode scope type.
1927    #[serde(rename = "type", default)]
1928    pub mode_type: String,
1929}
1930
1931/// Result of a borrow/repay request.
1932#[derive(Debug, Clone, Deserialize)]
1933#[serde(rename_all = "camelCase")]
1934#[non_exhaustive]
1935pub struct BorrowRepayResult {
1936    /// Currency.
1937    #[serde(default)]
1938    pub ccy: String,
1939    /// Borrow or repay side.
1940    #[serde(default)]
1941    pub side: String,
1942    /// Requested amount.
1943    #[serde(default)]
1944    pub amt: NumberString,
1945    /// OKX borrow/repay order ID.
1946    #[serde(default)]
1947    pub ord_id: String,
1948}
1949
1950/// Borrow/repay history row.
1951#[derive(Debug, Clone, Deserialize)]
1952#[serde(rename_all = "camelCase")]
1953#[non_exhaustive]
1954pub struct BorrowRepayHistory {
1955    /// Currency.
1956    #[serde(default)]
1957    pub ccy: String,
1958    /// Borrow or repay side.
1959    #[serde(default)]
1960    pub side: String,
1961    /// Amount.
1962    #[serde(default)]
1963    pub amt: NumberString,
1964    /// OKX borrow/repay order ID.
1965    #[serde(default)]
1966    pub ord_id: String,
1967    /// OKX state value.
1968    #[serde(default)]
1969    pub state: String,
1970    /// Timestamp (Unix milliseconds).
1971    #[serde(default)]
1972    pub ts: NumberString,
1973}
1974
1975/// Borrowing interest limit information.
1976#[derive(Debug, Clone, Deserialize)]
1977#[serde(rename_all = "camelCase")]
1978#[non_exhaustive]
1979pub struct InterestLimit {
1980    /// Currency.
1981    #[serde(default)]
1982    pub ccy: String,
1983    /// Interest rate.
1984    #[serde(default)]
1985    pub rate: NumberString,
1986    /// Loan quota.
1987    #[serde(default)]
1988    pub loan_quota: NumberString,
1989    /// Used loan quota.
1990    #[serde(default)]
1991    pub used_loan: NumberString,
1992}
1993
1994/// Simulated margin calculation result.
1995#[derive(Debug, Clone, Deserialize)]
1996#[serde(rename_all = "camelCase")]
1997#[non_exhaustive]
1998pub struct SimulatedMargin {
1999    /// Initial margin requirement.
2000    #[serde(default)]
2001    pub imr: NumberString,
2002    /// Maintenance margin requirement.
2003    #[serde(default)]
2004    pub mmr: NumberString,
2005    /// Margin ratio.
2006    #[serde(default)]
2007    pub mr: NumberString,
2008    /// Notional value in USD.
2009    #[serde(default)]
2010    pub notional_usd: NumberString,
2011    /// Per-instrument details returned by OKX.
2012    #[serde(default)]
2013    pub details: Vec<SimulatedMarginDetail>,
2014}
2015
2016/// Per-instrument detail in a simulated margin response.
2017#[derive(Debug, Clone, Deserialize)]
2018#[serde(rename_all = "camelCase")]
2019#[non_exhaustive]
2020pub struct SimulatedMarginDetail {
2021    /// Instrument ID.
2022    #[serde(default)]
2023    pub inst_id: String,
2024    /// Position size.
2025    #[serde(default)]
2026    pub pos: NumberString,
2027    /// Initial margin requirement.
2028    #[serde(default)]
2029    pub imr: NumberString,
2030    /// Maintenance margin requirement.
2031    #[serde(default)]
2032    pub mmr: NumberString,
2033    /// Unrealized PnL.
2034    #[serde(default)]
2035    pub upl: NumberString,
2036}
2037
2038/// Account greeks row.
2039#[derive(Debug, Clone, Deserialize)]
2040#[serde(rename_all = "camelCase")]
2041#[non_exhaustive]
2042pub struct Greek {
2043    /// Currency.
2044    #[serde(default)]
2045    pub ccy: String,
2046    /// Black-Scholes delta.
2047    #[serde(rename = "deltaBS", default)]
2048    pub delta_bs: NumberString,
2049    /// Portfolio-adjusted delta.
2050    #[serde(rename = "deltaPA", default)]
2051    pub delta_pa: NumberString,
2052    /// Black-Scholes gamma.
2053    #[serde(rename = "gammaBS", default)]
2054    pub gamma_bs: NumberString,
2055    /// Black-Scholes theta.
2056    #[serde(rename = "thetaBS", default)]
2057    pub theta_bs: NumberString,
2058    /// Black-Scholes vega.
2059    #[serde(rename = "vegaBS", default)]
2060    pub vega_bs: NumberString,
2061}
2062
2063/// Account position-tier row.
2064#[derive(Debug, Clone, Deserialize)]
2065#[serde(rename_all = "camelCase")]
2066#[non_exhaustive]
2067pub struct AccountPositionTier {
2068    /// Instrument type.
2069    #[serde(default)]
2070    pub inst_type: String,
2071    /// Underlying.
2072    #[serde(default)]
2073    pub uly: String,
2074    /// Instrument family.
2075    #[serde(default)]
2076    pub inst_family: String,
2077    /// Position type.
2078    #[serde(default)]
2079    pub pos_type: String,
2080    /// Minimum size in the tier.
2081    #[serde(default)]
2082    pub min_sz: NumberString,
2083    /// Maximum size in the tier.
2084    #[serde(default)]
2085    pub max_sz: NumberString,
2086}
2087
2088/// Result of updating risk offset type.
2089#[derive(Debug, Clone, Deserialize)]
2090#[serde(rename_all = "camelCase")]
2091#[non_exhaustive]
2092pub struct SetRiskOffsetTypeResult {
2093    /// OKX risk offset type.
2094    #[serde(rename = "type", default)]
2095    pub risk_offset_type: String,
2096}
2097
2098/// Result of updating auto loan.
2099#[derive(Debug, Clone, Deserialize)]
2100#[serde(rename_all = "camelCase")]
2101#[non_exhaustive]
2102pub struct SetAutoLoanResult {
2103    /// Auto-loan setting as returned by OKX.
2104    #[serde(default)]
2105    pub auto_loan: String,
2106}
2107
2108/// Result of updating account level.
2109#[derive(Debug, Clone, Deserialize)]
2110#[serde(rename_all = "camelCase")]
2111#[non_exhaustive]
2112pub struct SetAccountLevelResult {
2113    /// Account level.
2114    #[serde(default)]
2115    pub acct_lv: String,
2116}
2117
2118/// Result of activating option trading.
2119#[derive(Debug, Clone, Deserialize)]
2120#[serde(rename_all = "camelCase")]
2121#[non_exhaustive]
2122pub struct ActivateOptionResult {
2123    /// OKX result marker, when returned.
2124    #[serde(default)]
2125    pub result: String,
2126}
2127
2128/// Position-builder result.
2129#[derive(Debug, Clone, Deserialize)]
2130#[serde(rename_all = "camelCase")]
2131#[non_exhaustive]
2132pub struct PositionBuilderResult {
2133    /// Account level used for the calculation.
2134    #[serde(default)]
2135    pub acct_lv: String,
2136    /// Adjusted / effective equity.
2137    #[serde(default)]
2138    pub adj_eq: NumberString,
2139    /// Initial margin requirement.
2140    #[serde(default)]
2141    pub imr: NumberString,
2142    /// Maintenance margin requirement.
2143    #[serde(default)]
2144    pub mmr: NumberString,
2145    /// Margin ratio.
2146    #[serde(default)]
2147    pub mr: NumberString,
2148    /// Simulated or real position data.
2149    #[serde(default)]
2150    pub pos_data: Vec<PositionBuilderPosition>,
2151    /// Simulated or real asset data.
2152    #[serde(default)]
2153    pub asset_data: Vec<PositionBuilderAsset>,
2154}
2155
2156/// Position row returned by position builder.
2157#[derive(Debug, Clone, Deserialize)]
2158#[serde(rename_all = "camelCase")]
2159#[non_exhaustive]
2160pub struct PositionBuilderPosition {
2161    /// Instrument type.
2162    #[serde(default)]
2163    pub inst_type: String,
2164    /// Instrument ID.
2165    #[serde(default)]
2166    pub inst_id: String,
2167    /// Position size.
2168    #[serde(default)]
2169    pub pos: NumberString,
2170    /// Average price.
2171    #[serde(default)]
2172    pub avg_px: NumberString,
2173    /// Unrealized PnL.
2174    #[serde(default)]
2175    pub upl: NumberString,
2176}
2177
2178/// Asset row returned by position builder.
2179#[derive(Debug, Clone, Deserialize)]
2180#[serde(rename_all = "camelCase")]
2181#[non_exhaustive]
2182pub struct PositionBuilderAsset {
2183    /// Currency.
2184    #[serde(default)]
2185    pub ccy: String,
2186    /// Equity.
2187    #[serde(default)]
2188    pub eq: NumberString,
2189}
2190
2191#[cfg(test)]
2192mod tests {
2193    use crate::test_util::MockTransport;
2194    use crate::{Credentials, OkxClient};
2195
2196    fn signed_client(mock: MockTransport) -> OkxClient<MockTransport> {
2197        OkxClient::with_transport(mock)
2198            .credentials(Credentials::new("key", "secret", "pass"))
2199            .build()
2200    }
2201
2202    #[tokio::test]
2203    async fn get_balance_signs_request_and_parses() {
2204        let body = r#"{"code":"0","msg":"","data":[
2205            {"totalEq":"10000","adjEq":"9500","uTime":"1597026383085","details":[
2206                {"ccy":"USDT","eq":"10000","cashBal":"10000","availBal":"9000","frozenBal":"1000"}]}]}"#;
2207        let mock = MockTransport::new(body);
2208        let client = signed_client(mock.clone());
2209
2210        let balances = client.account().get_balance(None).await.unwrap();
2211        assert_eq!(balances[0].total_eq.as_str(), "10000");
2212        assert_eq!(balances[0].details[0].ccy, "USDT");
2213        assert_eq!(balances[0].details[0].avail_bal.as_str(), "9000");
2214
2215        let req = mock.captured();
2216        assert_eq!(req.method, http::Method::GET);
2217        assert!(req.uri.ends_with("/api/v5/account/balance"));
2218        assert!(req.is_signed(), "authenticated endpoint must be signed");
2219    }
2220
2221    #[tokio::test]
2222    async fn get_positions_passes_filters() {
2223        let body = r#"{"code":"0","msg":"","data":[
2224            {"instType":"SWAP","instId":"BTC-USDT-SWAP","posId":"1","posSide":"long",
2225             "mgnMode":"cross","pos":"1","avgPx":"42000","upl":"5","lever":"10","liqPx":"38000"}]}"#;
2226        let mock = MockTransport::new(body);
2227        let client = signed_client(mock.clone());
2228
2229        let positions = client
2230            .account()
2231            .get_positions(Some(crate::model::InstType::Swap), Some("BTC-USDT-SWAP"))
2232            .await
2233            .unwrap();
2234        assert_eq!(positions[0].inst_id, "BTC-USDT-SWAP");
2235        assert_eq!(positions[0].pos_side, crate::model::PositionSide::Long);
2236
2237        let req = mock.captured();
2238        assert_eq!(req.query(), Some("instType=SWAP&instId=BTC-USDT-SWAP"));
2239        assert!(req.is_signed());
2240    }
2241
2242    #[tokio::test]
2243    async fn missing_credentials_is_configuration_error() {
2244        let mock = MockTransport::new("{}");
2245        let client = OkxClient::with_transport(mock).build();
2246        let err = client.account().get_balance(None).await.unwrap_err();
2247        assert!(matches!(err, crate::Error::Configuration(_)));
2248    }
2249
2250    #[tokio::test]
2251    async fn get_position_risk_signs_and_parses() {
2252        let body = r#"{"code":"0","msg":"","data":[
2253            {"adjEq":"1000","ts":"1597026383085","balData":[{"ccy":"USDT","eq":"1000"}],
2254             "posData":[{"instType":"SWAP","instId":"BTC-USDT-SWAP","posSide":"long","mgnMode":"cross","pos":"1"}]}]}"#;
2255        let mock = MockTransport::new(body);
2256        let client = signed_client(mock.clone());
2257
2258        let risk = client
2259            .account()
2260            .get_position_risk(Some(crate::model::InstType::Swap))
2261            .await
2262            .unwrap();
2263        assert_eq!(risk[0].adj_eq.as_str(), "1000");
2264        assert_eq!(risk[0].pos_data[0].inst_id, "BTC-USDT-SWAP");
2265
2266        let req = mock.captured();
2267        assert_eq!(req.query(), Some("instType=SWAP"));
2268        assert!(req.is_signed());
2269    }
2270
2271    #[tokio::test]
2272    async fn get_account_config_signs_and_parses() {
2273        let body = r#"{"code":"0","msg":"","data":[
2274            {"uid":"1","acctLv":"2","posMode":"net_mode","greeksType":"PA","autoLoan":false}]}"#;
2275        let mock = MockTransport::new(body);
2276        let client = signed_client(mock.clone());
2277
2278        let config = client.account().get_account_config().await.unwrap();
2279        assert_eq!(config[0].pos_mode, "net_mode");
2280
2281        let req = mock.captured();
2282        assert!(req.uri.ends_with("/api/v5/account/config"));
2283        assert_eq!(req.query(), None);
2284        assert!(req.is_signed());
2285    }
2286
2287    #[tokio::test]
2288    async fn get_account_bills_uses_builder_query() {
2289        let body = r#"{"code":"0","msg":"","data":[
2290            {"billId":"1","instType":"SPOT","ccy":"USDT","mgnMode":"cash","type":"1","subType":"2",
2291             "sz":"10","bal":"100","ts":"1597026383085"}]}"#;
2292        let mock = MockTransport::new(body);
2293        let client = signed_client(mock.clone());
2294        let request = super::BillsRequest::new()
2295            .inst_type(crate::model::InstType::Spot)
2296            .currency("USDT")
2297            .bill_type("1")
2298            .limit(1);
2299
2300        let bills = client.account().get_account_bills(&request).await.unwrap();
2301        assert_eq!(bills[0].bill_id, "1");
2302
2303        let req = mock.captured();
2304        assert_eq!(req.query(), Some("instType=SPOT&ccy=USDT&type=1&limit=1"));
2305        assert!(req.is_signed());
2306    }
2307
2308    #[tokio::test]
2309    async fn get_account_bills_archive_uses_builder_query() {
2310        let body = r#"{"code":"0","msg":"","data":[
2311            {"billId":"2","instType":"SPOT","ccy":"USDT","type":"1","sz":"10","bal":"100","ts":"1597026383085"}]}"#;
2312        let mock = MockTransport::new(body);
2313        let client = signed_client(mock.clone());
2314        let request = super::BillsArchiveRequest::new()
2315            .filters(super::BillsRequest::new().currency("USDT"))
2316            .begin("100")
2317            .end("200");
2318
2319        let bills = client
2320            .account()
2321            .get_account_bills_archive(&request)
2322            .await
2323            .unwrap();
2324        assert_eq!(bills[0].bill_id, "2");
2325
2326        let req = mock.captured();
2327        assert_eq!(req.query(), Some("ccy=USDT&begin=100&end=200"));
2328        assert!(req.is_signed());
2329    }
2330
2331    #[tokio::test]
2332    async fn set_position_mode_posts_body() {
2333        let body = r#"{"code":"0","msg":"","data":[{"posMode":"net_mode"}]}"#;
2334        let mock = MockTransport::new(body);
2335        let client = signed_client(mock.clone());
2336
2337        let result = client
2338            .account()
2339            .set_position_mode("net_mode")
2340            .await
2341            .unwrap();
2342        assert_eq!(result[0].pos_mode, "net_mode");
2343
2344        let req = mock.captured();
2345        assert_eq!(req.method, http::Method::POST);
2346        assert!(req.uri.ends_with("/api/v5/account/set-position-mode"));
2347        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2348        assert_eq!(sent["posMode"], "net_mode");
2349        assert!(req.is_signed());
2350    }
2351
2352    #[tokio::test]
2353    async fn set_leverage_posts_builder_body() {
2354        let body = r#"{"code":"0","msg":"","data":[
2355            {"instId":"BTC-USDT-SWAP","mgnMode":"cross","posSide":"long","lever":"10"}]}"#;
2356        let mock = MockTransport::new(body);
2357        let client = signed_client(mock.clone());
2358        let request = super::SetLeverageRequest::new("10", crate::model::TradeMode::Cross)
2359            .inst_id("BTC-USDT-SWAP")
2360            .position_side(crate::model::PositionSide::Long);
2361
2362        let result = client.account().set_leverage(&request).await.unwrap();
2363        assert_eq!(result[0].lever.as_str(), "10");
2364
2365        let req = mock.captured();
2366        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2367        assert_eq!(sent["lever"], "10");
2368        assert_eq!(sent["mgnMode"], "cross");
2369        assert_eq!(sent["instId"], "BTC-USDT-SWAP");
2370        assert_eq!(sent["posSide"], "long");
2371        assert!(req.is_signed());
2372    }
2373
2374    #[tokio::test]
2375    async fn get_leverage_uses_builder_query() {
2376        let body = r#"{"code":"0","msg":"","data":[
2377            {"instId":"BTC-USDT-SWAP","mgnMode":"cross","posSide":"long","lever":"10"}]}"#;
2378        let mock = MockTransport::new(body);
2379        let client = signed_client(mock.clone());
2380        let request =
2381            super::LeverageRequest::new(crate::model::TradeMode::Cross).inst_id("BTC-USDT-SWAP");
2382
2383        let result = client.account().get_leverage(&request).await.unwrap();
2384        assert_eq!(result[0].mgn_mode, crate::model::TradeMode::Cross);
2385
2386        let req = mock.captured();
2387        assert_eq!(req.query(), Some("mgnMode=cross&instId=BTC-USDT-SWAP"));
2388        assert!(req.is_signed());
2389    }
2390
2391    #[tokio::test]
2392    async fn get_max_order_size_uses_builder_query() {
2393        let body = r#"{"code":"0","msg":"","data":[
2394            {"instId":"BTC-USDT","maxBuy":"1","maxSell":"2"}]}"#;
2395        let mock = MockTransport::new(body);
2396        let client = signed_client(mock.clone());
2397        let request = super::MaxOrderSizeRequest::new("BTC-USDT", crate::model::TradeMode::Cash)
2398            .price("42000");
2399
2400        let result = client.account().get_max_order_size(&request).await.unwrap();
2401        assert_eq!(result[0].max_buy.as_str(), "1");
2402
2403        let req = mock.captured();
2404        assert_eq!(req.query(), Some("instId=BTC-USDT&tdMode=cash&px=42000"));
2405        assert!(req.is_signed());
2406    }
2407
2408    #[tokio::test]
2409    async fn get_max_avail_size_uses_builder_query() {
2410        let body = r#"{"code":"0","msg":"","data":[
2411            {"instId":"BTC-USDT","availBuy":"1","availSell":"2"}]}"#;
2412        let mock = MockTransport::new(body);
2413        let client = signed_client(mock.clone());
2414        let request =
2415            super::MaxAvailableSizeRequest::new("BTC-USDT", crate::model::TradeMode::Cash)
2416                .reduce_only(false);
2417
2418        let result = client.account().get_max_avail_size(&request).await.unwrap();
2419        assert_eq!(result[0].avail_sell.as_str(), "2");
2420
2421        let req = mock.captured();
2422        assert_eq!(
2423            req.query(),
2424            Some("instId=BTC-USDT&tdMode=cash&reduceOnly=false")
2425        );
2426        assert!(req.is_signed());
2427    }
2428
2429    #[tokio::test]
2430    async fn get_fee_rates_uses_builder_query() {
2431        let body = r#"{"code":"0","msg":"","data":[
2432            {"instType":"SPOT","instId":"BTC-USDT","category":"1","maker":"-0.0001","taker":"0.001","ts":"1597026383085"}]}"#;
2433        let mock = MockTransport::new(body);
2434        let client = signed_client(mock.clone());
2435        let request = super::FeeRatesRequest::new(crate::model::InstType::Spot).inst_id("BTC-USDT");
2436
2437        let result = client.account().get_fee_rates(&request).await.unwrap();
2438        assert_eq!(result[0].maker.as_str(), "-0.0001");
2439
2440        let req = mock.captured();
2441        assert_eq!(req.query(), Some("instType=SPOT&instId=BTC-USDT"));
2442        assert!(req.is_signed());
2443    }
2444
2445    #[tokio::test]
2446    async fn get_max_withdrawal_queries_currency() {
2447        let body = r#"{"code":"0","msg":"","data":[
2448            {"ccy":"USDT","maxWd":"100","maxWdEx":"90"}]}"#;
2449        let mock = MockTransport::new(body);
2450        let client = signed_client(mock.clone());
2451
2452        let result = client
2453            .account()
2454            .get_max_withdrawal(Some("USDT"))
2455            .await
2456            .unwrap();
2457        assert_eq!(result[0].max_wd.as_str(), "100");
2458
2459        let req = mock.captured();
2460        assert_eq!(req.query(), Some("ccy=USDT"));
2461        assert!(req.is_signed());
2462    }
2463
2464    #[tokio::test]
2465    async fn get_positions_history_uses_builder_query() {
2466        let body = r#"{"code":"0","msg":"","data":[
2467            {"instType":"SWAP","instId":"BTC-USDT-SWAP","posId":"1","mgnMode":"cross",
2468             "type":"2","realizedPnl":"5","cTime":"1597026383085","uTime":"1597026383999"}]}"#;
2469        let mock = MockTransport::new(body);
2470        let client = signed_client(mock.clone());
2471        let request = super::PositionsHistoryRequest::new()
2472            .inst_type(crate::model::InstType::Swap)
2473            .inst_id("BTC-USDT-SWAP")
2474            .limit(1);
2475
2476        let result = client
2477            .account()
2478            .get_positions_history(&request)
2479            .await
2480            .unwrap();
2481        assert_eq!(result[0].realized_pnl.as_str(), "5");
2482
2483        let req = mock.captured();
2484        assert_eq!(
2485            req.query(),
2486            Some("instType=SWAP&instId=BTC-USDT-SWAP&limit=1")
2487        );
2488        assert!(req.is_signed());
2489    }
2490
2491    #[tokio::test]
2492    async fn get_risk_state_signs_and_parses() {
2493        let body = r#"{"code":"0","msg":"","data":[{"atRisk":"false","ts":"1597026383085"}]}"#;
2494        let mock = MockTransport::new(body);
2495        let client = signed_client(mock.clone());
2496
2497        let result = client.account().get_risk_state().await.unwrap();
2498        assert_eq!(result[0].at_risk, "false");
2499
2500        let req = mock.captured();
2501        assert!(req.uri.ends_with("/api/v5/account/risk-state"));
2502        assert_eq!(req.query(), None);
2503        assert!(req.is_signed());
2504    }
2505
2506    #[tokio::test]
2507    async fn demo_trading_sets_simulated_header() {
2508        let body = r#"{"code":"0","msg":"","data":[
2509            {"uid":"1","acctLv":"2","posMode":"net_mode","greeksType":"PA","autoLoan":false}]}"#;
2510        let mock = MockTransport::new(body);
2511        let client = OkxClient::with_transport(mock.clone())
2512            .credentials(Credentials::new("key", "secret", "pass"))
2513            .demo_trading(true)
2514            .build();
2515
2516        let config = client.account().get_account_config().await.unwrap();
2517        assert_eq!(config[0].acct_lv, "2");
2518
2519        let req = mock.captured();
2520        assert_eq!(
2521            req.headers
2522                .get("x-simulated-trading")
2523                .and_then(|v| v.to_str().ok()),
2524            Some("1")
2525        );
2526        assert!(req.is_signed());
2527    }
2528
2529    #[tokio::test]
2530    async fn adjust_margin_posts_body() {
2531        let body = r#"{"code":"0","msg":"","data":[
2532            {"instId":"BTC-USDT-SWAP","posSide":"long","type":"add","amt":"100"}]}"#;
2533        let mock = MockTransport::new(body);
2534        let client = signed_client(mock.clone());
2535        let request = super::AdjustMarginRequest::new(
2536            "BTC-USDT-SWAP",
2537            crate::model::PositionSide::Long,
2538            "add",
2539            "100",
2540        )
2541        .loan_transfer(true);
2542
2543        let result = client.account().adjust_margin(&request).await.unwrap();
2544        assert_eq!(result[0].amt.as_str(), "100");
2545
2546        let req = mock.captured();
2547        assert_eq!(req.method, http::Method::POST);
2548        assert!(req.uri.ends_with("/api/v5/account/position/margin-balance"));
2549        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2550        assert_eq!(sent["instId"], "BTC-USDT-SWAP");
2551        assert_eq!(sent["posSide"], "long");
2552        assert_eq!(sent["type"], "add");
2553        assert_eq!(sent["amt"], "100");
2554        assert_eq!(sent["loanTrans"], true);
2555        assert!(req.is_signed());
2556    }
2557
2558    #[tokio::test]
2559    async fn get_account_instruments_uses_builder_query() {
2560        let body = r#"{"code":"0","msg":"","data":[
2561            {"instType":"SWAP","instId":"BTC-USDT-SWAP","uly":"BTC-USDT","instFamily":"BTC-USDT",
2562             "baseCcy":"BTC","quoteCcy":"USDT","settleCcy":"USDT"}]}"#;
2563        let mock = MockTransport::new(body);
2564        let client = signed_client(mock.clone());
2565        let request = super::AccountInstrumentsRequest::new()
2566            .inst_type(crate::model::InstType::Swap)
2567            .inst_id("BTC-USDT-SWAP");
2568
2569        let result = client
2570            .account()
2571            .get_account_instruments(&request)
2572            .await
2573            .unwrap();
2574        assert_eq!(result[0].settle_ccy, "USDT");
2575
2576        let req = mock.captured();
2577        assert_eq!(req.query(), Some("instType=SWAP&instId=BTC-USDT-SWAP"));
2578        assert!(req.is_signed());
2579    }
2580
2581    #[tokio::test]
2582    async fn get_max_loan_uses_builder_query() {
2583        let body = r#"{"code":"0","msg":"","data":[
2584            {"instId":"BTC-USDT","mgnMode":"cross","mgnCcy":"USDT","maxLoan":"1000"}]}"#;
2585        let mock = MockTransport::new(body);
2586        let client = signed_client(mock.clone());
2587        let request = super::MaxLoanRequest::new("BTC-USDT", crate::model::TradeMode::Cross)
2588            .margin_currency("USDT");
2589
2590        let result = client.account().get_max_loan(&request).await.unwrap();
2591        assert_eq!(result[0].max_loan.as_str(), "1000");
2592
2593        let req = mock.captured();
2594        assert_eq!(
2595            req.query(),
2596            Some("instId=BTC-USDT&mgnMode=cross&mgnCcy=USDT")
2597        );
2598        assert!(req.is_signed());
2599    }
2600
2601    #[tokio::test]
2602    async fn get_interest_accrued_uses_builder_query() {
2603        let body = r#"{"code":"0","msg":"","data":[
2604            {"instId":"BTC-USDT","ccy":"USDT","mgnMode":"cross","interest":"1",
2605             "interestRate":"0.0001","liab":"100","ts":"1597026383085"}]}"#;
2606        let mock = MockTransport::new(body);
2607        let client = signed_client(mock.clone());
2608        let request = super::InterestAccruedRequest::new()
2609            .inst_id("BTC-USDT")
2610            .currency("USDT")
2611            .limit(1);
2612
2613        let result = client
2614            .account()
2615            .get_interest_accrued(&request)
2616            .await
2617            .unwrap();
2618        assert_eq!(result[0].interest_rate.as_str(), "0.0001");
2619
2620        let req = mock.captured();
2621        assert_eq!(req.query(), Some("instId=BTC-USDT&ccy=USDT&limit=1"));
2622        assert!(req.is_signed());
2623    }
2624
2625    #[tokio::test]
2626    async fn get_interest_rate_queries_currency() {
2627        let body = r#"{"code":"0","msg":"","data":[{"ccy":"USDT","interestRate":"0.0001"}]}"#;
2628        let mock = MockTransport::new(body);
2629        let client = signed_client(mock.clone());
2630
2631        let result = client
2632            .account()
2633            .get_interest_rate(Some("USDT"))
2634            .await
2635            .unwrap();
2636        assert_eq!(result[0].ccy, "USDT");
2637
2638        let req = mock.captured();
2639        assert_eq!(req.query(), Some("ccy=USDT"));
2640        assert!(req.is_signed());
2641    }
2642
2643    #[tokio::test]
2644    async fn set_greeks_posts_body() {
2645        let body = r#"{"code":"0","msg":"","data":[{"greeksType":"PA"}]}"#;
2646        let mock = MockTransport::new(body);
2647        let client = signed_client(mock.clone());
2648
2649        let result = client.account().set_greeks("PA").await.unwrap();
2650        assert_eq!(result[0].greeks_type, "PA");
2651
2652        let req = mock.captured();
2653        assert_eq!(req.method, http::Method::POST);
2654        assert!(req.uri.ends_with("/api/v5/account/set-greeks"));
2655        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2656        assert_eq!(sent["greeksType"], "PA");
2657        assert!(req.is_signed());
2658    }
2659
2660    #[tokio::test]
2661    async fn set_isolated_mode_posts_body() {
2662        let body = r#"{"code":"0","msg":"","data":[{"isoMode":"automatic","type":"MARGIN"}]}"#;
2663        let mock = MockTransport::new(body);
2664        let client = signed_client(mock.clone());
2665
2666        let result = client
2667            .account()
2668            .set_isolated_mode("automatic", "MARGIN")
2669            .await
2670            .unwrap();
2671        assert_eq!(result[0].iso_mode, "automatic");
2672
2673        let req = mock.captured();
2674        assert_eq!(req.method, http::Method::POST);
2675        assert!(req.uri.ends_with("/api/v5/account/set-isolated-mode"));
2676        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2677        assert_eq!(sent["isoMode"], "automatic");
2678        assert_eq!(sent["type"], "MARGIN");
2679        assert!(req.is_signed());
2680    }
2681
2682    #[tokio::test]
2683    async fn borrow_repay_posts_body() {
2684        let body = r#"{"code":"0","msg":"","data":[
2685            {"ccy":"USDT","side":"borrow","amt":"100","ordId":"1"}]}"#;
2686        let mock = MockTransport::new(body);
2687        let client = signed_client(mock.clone());
2688        let request = super::BorrowRepayRequest::new("USDT", "borrow", "100").order_id("1");
2689
2690        let result = client.account().borrow_repay(&request).await.unwrap();
2691        assert_eq!(result[0].ord_id, "1");
2692
2693        let req = mock.captured();
2694        assert_eq!(req.method, http::Method::POST);
2695        assert!(req.uri.ends_with("/api/v5/account/borrow-repay"));
2696        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2697        assert_eq!(sent["ccy"], "USDT");
2698        assert_eq!(sent["side"], "borrow");
2699        assert_eq!(sent["amt"], "100");
2700        assert_eq!(sent["ordId"], "1");
2701        assert!(req.is_signed());
2702    }
2703
2704    #[tokio::test]
2705    async fn get_borrow_repay_history_uses_builder_query() {
2706        let body = r#"{"code":"0","msg":"","data":[
2707            {"ccy":"USDT","side":"borrow","amt":"100","ordId":"1","state":"2","ts":"1597026383085"}]}"#;
2708        let mock = MockTransport::new(body);
2709        let client = signed_client(mock.clone());
2710        let request = super::BorrowRepayHistoryRequest::new()
2711            .currency("USDT")
2712            .limit(1);
2713
2714        let result = client
2715            .account()
2716            .get_borrow_repay_history(&request)
2717            .await
2718            .unwrap();
2719        assert_eq!(result[0].state, "2");
2720
2721        let req = mock.captured();
2722        assert_eq!(req.query(), Some("ccy=USDT&limit=1"));
2723        assert!(req.is_signed());
2724    }
2725
2726    #[tokio::test]
2727    async fn get_interest_limits_uses_builder_query() {
2728        let body = r#"{"code":"0","msg":"","data":[
2729            {"ccy":"USDT","rate":"0.0001","loanQuota":"1000","usedLoan":"100"}]}"#;
2730        let mock = MockTransport::new(body);
2731        let client = signed_client(mock.clone());
2732        let request = super::InterestLimitsRequest::new()
2733            .limit_type("1")
2734            .currency("USDT");
2735
2736        let result = client
2737            .account()
2738            .get_interest_limits(&request)
2739            .await
2740            .unwrap();
2741        assert_eq!(result[0].loan_quota.as_str(), "1000");
2742
2743        let req = mock.captured();
2744        assert_eq!(req.query(), Some("type=1&ccy=USDT"));
2745        assert!(req.is_signed());
2746    }
2747
2748    #[tokio::test]
2749    async fn get_simulated_margin_posts_body_and_omits_unset_fields() {
2750        let body = r#"{"code":"0","msg":"","data":[
2751            {"imr":"10","mmr":"5","mr":"100","notionalUsd":"1000",
2752             "details":[{"instId":"BTC-USDT-SWAP","pos":"1","imr":"10","mmr":"5","upl":"2"}]}]}"#;
2753        let mock = MockTransport::new(body);
2754        let client = signed_client(mock.clone());
2755        let request = super::SimulatedMarginRequest::new()
2756            .inst_type(crate::model::InstType::Swap)
2757            .simulated_positions(vec![
2758                super::SimulatedPosition::new("BTC-USDT-SWAP")
2759                    .position("1")
2760                    .leverage("10"),
2761            ]);
2762
2763        let result = client
2764            .account()
2765            .get_simulated_margin(&request)
2766            .await
2767            .unwrap();
2768        assert_eq!(result[0].details[0].upl.as_str(), "2");
2769
2770        let req = mock.captured();
2771        assert_eq!(req.method, http::Method::POST);
2772        assert!(req.uri.ends_with("/api/v5/account/simulated_margin"));
2773        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2774        assert_eq!(sent["instType"], "SWAP");
2775        assert_eq!(sent["simPos"][0]["instId"], "BTC-USDT-SWAP");
2776        assert_eq!(sent["simPos"][0]["pos"], "1");
2777        assert_eq!(sent["simPos"][0]["lever"], "10");
2778        assert!(sent.get("inclRealPos").is_none());
2779        assert!(sent["simPos"][0].get("avgPx").is_none());
2780        assert!(req.is_signed());
2781    }
2782
2783    #[tokio::test]
2784    async fn get_greeks_queries_currency() {
2785        let body = r#"{"code":"0","msg":"","data":[
2786            {"ccy":"BTC","deltaBS":"1","deltaPA":"0.9","gammaBS":"0.1","thetaBS":"-0.01","vegaBS":"2"}]}"#;
2787        let mock = MockTransport::new(body);
2788        let client = signed_client(mock.clone());
2789
2790        let result = client.account().get_greeks(Some("BTC")).await.unwrap();
2791        assert_eq!(result[0].delta_pa.as_str(), "0.9");
2792
2793        let req = mock.captured();
2794        assert_eq!(req.query(), Some("ccy=BTC"));
2795        assert!(req.is_signed());
2796    }
2797
2798    #[tokio::test]
2799    async fn get_account_position_tiers_uses_builder_query() {
2800        let body = r#"{"code":"0","msg":"","data":[
2801            {"instType":"OPTION","uly":"BTC-USD","instFamily":"BTC-USD","posType":"1","minSz":"0","maxSz":"100"}]}"#;
2802        let mock = MockTransport::new(body);
2803        let client = signed_client(mock.clone());
2804        let request = super::AccountPositionTiersRequest::new()
2805            .inst_type(crate::model::InstType::Option)
2806            .underlying("BTC-USD");
2807
2808        let result = client
2809            .account()
2810            .get_account_position_tiers(&request)
2811            .await
2812            .unwrap();
2813        assert_eq!(result[0].max_sz.as_str(), "100");
2814
2815        let req = mock.captured();
2816        assert_eq!(req.query(), Some("instType=OPTION&uly=BTC-USD"));
2817        assert!(req.is_signed());
2818    }
2819
2820    #[tokio::test]
2821    async fn set_risk_offset_type_posts_body() {
2822        let body = r#"{"code":"0","msg":"","data":[{"type":"1"}]}"#;
2823        let mock = MockTransport::new(body);
2824        let client = signed_client(mock.clone());
2825
2826        let result = client.account().set_risk_offset_type("1").await.unwrap();
2827        assert_eq!(result[0].risk_offset_type, "1");
2828
2829        let req = mock.captured();
2830        assert_eq!(req.method, http::Method::POST);
2831        assert!(req.uri.ends_with("/api/v5/account/set-riskOffset-type"));
2832        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2833        assert_eq!(sent["type"], "1");
2834        assert!(req.is_signed());
2835    }
2836
2837    #[tokio::test]
2838    async fn set_auto_loan_posts_body() {
2839        let body = r#"{"code":"0","msg":"","data":[{"autoLoan":"true"}]}"#;
2840        let mock = MockTransport::new(body);
2841        let client = signed_client(mock.clone());
2842
2843        let result = client.account().set_auto_loan(true).await.unwrap();
2844        assert_eq!(result[0].auto_loan, "true");
2845
2846        let req = mock.captured();
2847        assert_eq!(req.method, http::Method::POST);
2848        assert!(req.uri.ends_with("/api/v5/account/set-auto-loan"));
2849        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2850        assert_eq!(sent["autoLoan"], true);
2851        assert!(req.is_signed());
2852    }
2853
2854    #[tokio::test]
2855    async fn set_account_level_posts_body() {
2856        let body = r#"{"code":"0","msg":"","data":[{"acctLv":"2"}]}"#;
2857        let mock = MockTransport::new(body);
2858        let client = signed_client(mock.clone());
2859
2860        let result = client.account().set_account_level("2").await.unwrap();
2861        assert_eq!(result[0].acct_lv, "2");
2862
2863        let req = mock.captured();
2864        assert_eq!(req.method, http::Method::POST);
2865        assert!(req.uri.ends_with("/api/v5/account/set-account-level"));
2866        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2867        assert_eq!(sent["acctLv"], "2");
2868        assert!(req.is_signed());
2869    }
2870
2871    #[tokio::test]
2872    async fn activate_option_posts_empty_body() {
2873        let body = r#"{"code":"0","msg":"","data":[{"result":"true"}]}"#;
2874        let mock = MockTransport::new(body);
2875        let client = signed_client(mock.clone());
2876
2877        let result = client.account().activate_option().await.unwrap();
2878        assert_eq!(result[0].result, "true");
2879
2880        let req = mock.captured();
2881        assert_eq!(req.method, http::Method::POST);
2882        assert!(req.uri.ends_with("/api/v5/account/activate-option"));
2883        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2884        assert_eq!(sent, serde_json::json!({}));
2885        assert!(req.is_signed());
2886    }
2887
2888    #[tokio::test]
2889    async fn position_builder_posts_body_and_omits_unset_fields() {
2890        let body = r#"{"code":"0","msg":"","data":[
2891            {"acctLv":"2","adjEq":"1000","imr":"10","mmr":"5","mr":"100",
2892             "posData":[{"instType":"SWAP","instId":"BTC-USDT-SWAP","pos":"1","avgPx":"42000","upl":"2"}],
2893             "assetData":[{"ccy":"USDT","eq":"1000"}]}]}"#;
2894        let mock = MockTransport::new(body);
2895        let client = signed_client(mock.clone());
2896        let request = super::PositionBuilderRequest::new()
2897            .account_level("2")
2898            .include_real_positions_and_equity(false)
2899            .simulated_positions(vec![
2900                super::SimulatedPosition::new("BTC-USDT-SWAP").position("1"),
2901            ])
2902            .simulated_assets(vec![super::SimulatedAsset::new("USDT").equity("1000")]);
2903
2904        let result = client.account().position_builder(&request).await.unwrap();
2905        assert_eq!(result[0].pos_data[0].inst_id, "BTC-USDT-SWAP");
2906        assert_eq!(result[0].asset_data[0].eq.as_str(), "1000");
2907
2908        let req = mock.captured();
2909        assert_eq!(req.method, http::Method::POST);
2910        assert!(req.uri.ends_with("/api/v5/account/position-builder"));
2911        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
2912        assert_eq!(sent["acctLv"], "2");
2913        assert_eq!(sent["inclRealPosAndEq"], false);
2914        assert_eq!(sent["simPos"][0]["instId"], "BTC-USDT-SWAP");
2915        assert_eq!(sent["simAsset"][0]["ccy"], "USDT");
2916        assert!(sent.get("lever").is_none());
2917        assert!(sent["simPos"][0].get("avgPx").is_none());
2918        assert!(req.is_signed());
2919    }
2920}