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