use serde::{Deserialize, Serialize};
use crate::client::OkxClient;
use crate::error::Error;
use crate::model::{InstType, NumberString, PositionSide, TradeMode};
use crate::transport::Transport;
const BALANCE: &str = "/api/v5/account/balance";
const POSITIONS: &str = "/api/v5/account/positions";
const POSITION_RISK: &str = "/api/v5/account/account-position-risk";
const ACCOUNT_CONFIG: &str = "/api/v5/account/config";
const BILLS: &str = "/api/v5/account/bills";
const BILLS_ARCHIVE: &str = "/api/v5/account/bills-archive";
const SET_POSITION_MODE: &str = "/api/v5/account/set-position-mode";
const SET_LEVERAGE: &str = "/api/v5/account/set-leverage";
const GET_LEVERAGE: &str = "/api/v5/account/leverage-info";
const MAX_ORDER_SIZE: &str = "/api/v5/account/max-size";
const MAX_AVAILABLE_SIZE: &str = "/api/v5/account/max-avail-size";
const ADJUST_MARGIN: &str = "/api/v5/account/position/margin-balance";
const FEE_RATES: &str = "/api/v5/account/trade-fee";
const ACCOUNT_INSTRUMENTS: &str = "/api/v5/account/instruments";
const MAX_LOAN: &str = "/api/v5/account/max-loan";
const INTEREST_ACCRUED: &str = "/api/v5/account/interest-accrued";
const INTEREST_RATE: &str = "/api/v5/account/interest-rate";
const SET_GREEKS: &str = "/api/v5/account/set-greeks";
const SET_ISOLATED_MODE: &str = "/api/v5/account/set-isolated-mode";
const MAX_WITHDRAWAL: &str = "/api/v5/account/max-withdrawal";
const BORROW_REPAY: &str = "/api/v5/account/borrow-repay";
const BORROW_REPAY_HISTORY: &str = "/api/v5/account/borrow-repay-history";
const INTEREST_LIMITS: &str = "/api/v5/account/interest-limits";
const SIMULATED_MARGIN: &str = "/api/v5/account/simulated_margin";
const GREEKS: &str = "/api/v5/account/greeks";
const POSITIONS_HISTORY: &str = "/api/v5/account/positions-history";
const ACCOUNT_POSITION_TIERS: &str = "/api/v5/account/position-tiers";
const RISK_STATE: &str = "/api/v5/account/risk-state";
const SET_RISK_OFFSET_TYPE: &str = "/api/v5/account/set-riskOffset-type";
const SET_AUTO_LOAN: &str = "/api/v5/account/set-auto-loan";
const SET_ACCOUNT_LEVEL: &str = "/api/v5/account/set-account-level";
const ACTIVATE_OPTION: &str = "/api/v5/account/activate-option";
const POSITION_BUILDER: &str = "/api/v5/account/position-builder";
pub struct Account<'a, T> {
client: &'a OkxClient<T>,
}
impl<'a, T: Transport> Account<'a, T> {
pub(crate) fn new(client: &'a OkxClient<T>) -> Self {
Self { client }
}
pub async fn get_balance(&self, ccy: Option<&str>) -> Result<Vec<AccountBalance>, Error> {
let query = BalanceQuery { ccy };
self.client.get(BALANCE, &query, true).await
}
pub async fn get_positions(
&self,
inst_type: Option<InstType>,
inst_id: Option<&str>,
) -> Result<Vec<Position>, Error> {
let query = PositionsQuery {
inst_type: inst_type.as_ref(),
inst_id,
};
self.client.get(POSITIONS, &query, true).await
}
pub async fn get_position_risk(
&self,
inst_type: Option<InstType>,
) -> Result<Vec<PositionRisk>, Error> {
let query = PositionRiskQuery {
inst_type: inst_type.as_ref(),
};
self.client.get(POSITION_RISK, &query, true).await
}
pub async fn get_account_config(&self) -> Result<Vec<AccountConfig>, Error> {
self.client.get(ACCOUNT_CONFIG, &NoQuery, true).await
}
pub async fn get_account_bills(&self, request: &BillsRequest) -> Result<Vec<AccountBill>, Error> {
self.client.get(BILLS, request, true).await
}
pub async fn get_account_bills_archive(
&self,
request: &BillsArchiveRequest,
) -> Result<Vec<AccountBill>, Error> {
self.client.get(BILLS_ARCHIVE, request, true).await
}
pub async fn set_position_mode(
&self,
pos_mode: &str,
) -> Result<Vec<SetPositionModeResult>, Error> {
let body = SetPositionModeBody { pos_mode };
self.client.post(SET_POSITION_MODE, &body, true).await
}
pub async fn set_leverage(
&self,
request: &SetLeverageRequest,
) -> Result<Vec<LeverageInfo>, Error> {
self.client.post(SET_LEVERAGE, request, true).await
}
pub async fn get_leverage(
&self,
request: &LeverageRequest,
) -> Result<Vec<LeverageInfo>, Error> {
self.client.get(GET_LEVERAGE, request, true).await
}
pub async fn get_max_order_size(
&self,
request: &MaxOrderSizeRequest,
) -> Result<Vec<MaxOrderSize>, Error> {
self.client.get(MAX_ORDER_SIZE, request, true).await
}
pub async fn get_max_avail_size(
&self,
request: &MaxAvailableSizeRequest,
) -> Result<Vec<MaxAvailableSize>, Error> {
self.client.get(MAX_AVAILABLE_SIZE, request, true).await
}
pub async fn adjust_margin(
&self,
request: &AdjustMarginRequest,
) -> Result<Vec<AdjustMarginResult>, Error> {
self.client.post(ADJUST_MARGIN, request, true).await
}
pub async fn get_fee_rates(
&self,
request: &FeeRatesRequest,
) -> Result<Vec<FeeRate>, Error> {
self.client.get(FEE_RATES, request, true).await
}
pub async fn get_account_instruments(
&self,
request: &AccountInstrumentsRequest,
) -> Result<Vec<AccountInstrument>, Error> {
self.client.get(ACCOUNT_INSTRUMENTS, request, true).await
}
pub async fn get_max_loan(&self, request: &MaxLoanRequest) -> Result<Vec<MaxLoan>, Error> {
self.client.get(MAX_LOAN, request, true).await
}
pub async fn get_interest_accrued(
&self,
request: &InterestAccruedRequest,
) -> Result<Vec<InterestAccrued>, Error> {
self.client.get(INTEREST_ACCRUED, request, true).await
}
pub async fn get_interest_rate(
&self,
ccy: Option<&str>,
) -> Result<Vec<InterestRate>, Error> {
let query = BalanceQuery { ccy };
self.client.get(INTEREST_RATE, &query, true).await
}
pub async fn set_greeks(&self, greeks_type: &str) -> Result<Vec<SetGreeksResult>, Error> {
let body = SetGreeksBody { greeks_type };
self.client.post(SET_GREEKS, &body, true).await
}
pub async fn set_isolated_mode(
&self,
iso_mode: &str,
mode_type: &str,
) -> Result<Vec<SetIsolatedModeResult>, Error> {
let body = SetIsolatedModeBody {
iso_mode,
mode_type,
};
self.client.post(SET_ISOLATED_MODE, &body, true).await
}
pub async fn get_max_withdrawal(
&self,
ccy: Option<&str>,
) -> Result<Vec<MaxWithdrawal>, Error> {
let query = BalanceQuery { ccy };
self.client.get(MAX_WITHDRAWAL, &query, true).await
}
pub async fn borrow_repay(
&self,
request: &BorrowRepayRequest,
) -> Result<Vec<BorrowRepayResult>, Error> {
self.client.post(BORROW_REPAY, request, true).await
}
pub async fn get_borrow_repay_history(
&self,
request: &BorrowRepayHistoryRequest,
) -> Result<Vec<BorrowRepayHistory>, Error> {
self.client.get(BORROW_REPAY_HISTORY, request, true).await
}
pub async fn get_interest_limits(
&self,
request: &InterestLimitsRequest,
) -> Result<Vec<InterestLimit>, Error> {
self.client.get(INTEREST_LIMITS, request, true).await
}
pub async fn get_simulated_margin(
&self,
request: &SimulatedMarginRequest,
) -> Result<Vec<SimulatedMargin>, Error> {
self.client.post(SIMULATED_MARGIN, request, true).await
}
pub async fn get_greeks(&self, ccy: Option<&str>) -> Result<Vec<Greek>, Error> {
let query = BalanceQuery { ccy };
self.client.get(GREEKS, &query, true).await
}
pub async fn get_positions_history(
&self,
request: &PositionsHistoryRequest,
) -> Result<Vec<PositionHistory>, Error> {
self.client.get(POSITIONS_HISTORY, request, true).await
}
pub async fn get_account_position_tiers(
&self,
request: &AccountPositionTiersRequest,
) -> Result<Vec<AccountPositionTier>, Error> {
self.client.get(ACCOUNT_POSITION_TIERS, request, true).await
}
pub async fn get_risk_state(&self) -> Result<Vec<RiskState>, Error> {
self.client.get(RISK_STATE, &NoQuery, true).await
}
pub async fn set_risk_offset_type(
&self,
risk_offset_type: &str,
) -> Result<Vec<SetRiskOffsetTypeResult>, Error> {
let body = TypeBody {
value: risk_offset_type,
};
self.client.post(SET_RISK_OFFSET_TYPE, &body, true).await
}
pub async fn set_auto_loan(&self, auto_loan: bool) -> Result<Vec<SetAutoLoanResult>, Error> {
let body = SetAutoLoanBody { auto_loan };
self.client.post(SET_AUTO_LOAN, &body, true).await
}
pub async fn set_account_level(
&self,
acct_lv: &str,
) -> Result<Vec<SetAccountLevelResult>, Error> {
let body = SetAccountLevelBody { acct_lv };
self.client.post(SET_ACCOUNT_LEVEL, &body, true).await
}
pub async fn activate_option(&self) -> Result<Vec<ActivateOptionResult>, Error> {
self.client.post(ACTIVATE_OPTION, &EmptyBody {}, true).await
}
pub async fn position_builder(
&self,
request: &PositionBuilderRequest,
) -> Result<Vec<PositionBuilderResult>, Error> {
self.client.post(POSITION_BUILDER, request, true).await
}
}
#[derive(Serialize)]
struct NoQuery;
#[derive(Serialize)]
struct EmptyBody {}
#[derive(Serialize)]
struct BalanceQuery<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<&'a str>,
}
#[derive(Serialize)]
struct PositionsQuery<'a> {
#[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
inst_type: Option<&'a InstType>,
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<&'a str>,
}
#[derive(Serialize)]
struct PositionRiskQuery<'a> {
#[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
inst_type: Option<&'a InstType>,
}
#[derive(Serialize)]
struct SetPositionModeBody<'a> {
#[serde(rename = "posMode")]
pos_mode: &'a str,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct BillsRequest {
#[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
inst_type: Option<InstType>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(rename = "mgnMode", skip_serializing_if = "Option::is_none")]
mgn_mode: Option<TradeMode>,
#[serde(rename = "ctType", skip_serializing_if = "Option::is_none")]
ct_type: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
bill_type: Option<String>,
#[serde(rename = "subType", skip_serializing_if = "Option::is_none")]
sub_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u32>,
}
impl BillsRequest {
pub fn new() -> Self {
Self::default()
}
pub fn inst_type(mut self, inst_type: InstType) -> Self {
self.inst_type = Some(inst_type);
self
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
pub fn margin_mode(mut self, mgn_mode: TradeMode) -> Self {
self.mgn_mode = Some(mgn_mode);
self
}
pub fn contract_type(mut self, ct_type: impl Into<String>) -> Self {
self.ct_type = Some(ct_type.into());
self
}
pub fn bill_type(mut self, bill_type: impl Into<String>) -> Self {
self.bill_type = Some(bill_type.into());
self
}
pub fn sub_type(mut self, sub_type: impl Into<String>) -> Self {
self.sub_type = Some(sub_type.into());
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct BillsArchiveRequest {
#[serde(flatten)]
base: BillsRequest,
#[serde(skip_serializing_if = "Option::is_none")]
begin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
end: Option<String>,
}
impl BillsArchiveRequest {
pub fn new() -> Self {
Self::default()
}
pub fn filters(mut self, base: BillsRequest) -> Self {
self.base = base;
self
}
pub fn begin(mut self, begin: impl Into<String>) -> Self {
self.begin = Some(begin.into());
self
}
pub fn end(mut self, end: impl Into<String>) -> Self {
self.end = Some(end.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SetLeverageRequest {
lever: String,
#[serde(rename = "mgnMode")]
mgn_mode: TradeMode,
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
pos_side: Option<PositionSide>,
}
impl SetLeverageRequest {
pub fn new(lever: impl Into<String>, mgn_mode: TradeMode) -> Self {
Self {
lever: lever.into(),
mgn_mode,
inst_id: None,
ccy: None,
pos_side: None,
}
}
pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
self.inst_id = Some(inst_id.into());
self
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
pub fn position_side(mut self, pos_side: PositionSide) -> Self {
self.pos_side = Some(pos_side);
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct LeverageRequest {
#[serde(rename = "mgnMode")]
mgn_mode: TradeMode,
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
}
impl LeverageRequest {
pub fn new(mgn_mode: TradeMode) -> Self {
Self {
mgn_mode,
inst_id: None,
ccy: None,
}
}
pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
self.inst_id = Some(inst_id.into());
self
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct MaxOrderSizeRequest {
#[serde(rename = "instId")]
inst_id: String,
#[serde(rename = "tdMode")]
td_mode: TradeMode,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
px: Option<String>,
#[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
trade_quote_ccy: Option<String>,
}
impl MaxOrderSizeRequest {
pub fn new(inst_id: impl Into<String>, td_mode: TradeMode) -> Self {
Self {
inst_id: inst_id.into(),
td_mode,
ccy: None,
px: None,
trade_quote_ccy: None,
}
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
pub fn price(mut self, px: impl Into<String>) -> Self {
self.px = Some(px.into());
self
}
pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
self.trade_quote_ccy = Some(trade_quote_ccy.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct MaxAvailableSizeRequest {
#[serde(rename = "instId")]
inst_id: String,
#[serde(rename = "tdMode")]
td_mode: TradeMode,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
reduce_only: Option<bool>,
#[serde(rename = "unSpotOffset", skip_serializing_if = "Option::is_none")]
un_spot_offset: Option<bool>,
#[serde(rename = "quickMgnType", skip_serializing_if = "Option::is_none")]
quick_mgn_type: Option<String>,
#[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
trade_quote_ccy: Option<String>,
}
impl MaxAvailableSizeRequest {
pub fn new(inst_id: impl Into<String>, td_mode: TradeMode) -> Self {
Self {
inst_id: inst_id.into(),
td_mode,
ccy: None,
reduce_only: None,
un_spot_offset: None,
quick_mgn_type: None,
trade_quote_ccy: None,
}
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
pub fn reduce_only(mut self, reduce_only: bool) -> Self {
self.reduce_only = Some(reduce_only);
self
}
pub fn un_spot_offset(mut self, un_spot_offset: bool) -> Self {
self.un_spot_offset = Some(un_spot_offset);
self
}
pub fn quick_margin_type(mut self, quick_mgn_type: impl Into<String>) -> Self {
self.quick_mgn_type = Some(quick_mgn_type.into());
self
}
pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
self.trade_quote_ccy = Some(trade_quote_ccy.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct AdjustMarginRequest {
#[serde(rename = "instId")]
inst_id: String,
#[serde(rename = "posSide")]
pos_side: PositionSide,
#[serde(rename = "type")]
action: String,
amt: String,
#[serde(rename = "loanTrans", skip_serializing_if = "Option::is_none")]
loan_trans: Option<bool>,
}
impl AdjustMarginRequest {
pub fn new(
inst_id: impl Into<String>,
pos_side: PositionSide,
action: impl Into<String>,
amt: impl Into<String>,
) -> Self {
Self {
inst_id: inst_id.into(),
pos_side,
action: action.into(),
amt: amt.into(),
loan_trans: None,
}
}
pub fn loan_transfer(mut self, loan_trans: bool) -> Self {
self.loan_trans = Some(loan_trans);
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct FeeRatesRequest {
#[serde(rename = "instType")]
inst_type: InstType,
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<String>,
#[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
underlying: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
category: Option<String>,
#[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
inst_family: Option<String>,
}
impl FeeRatesRequest {
pub fn new(inst_type: InstType) -> Self {
Self {
inst_type,
inst_id: None,
underlying: None,
category: None,
inst_family: None,
}
}
pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
self.inst_id = Some(inst_id.into());
self
}
pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
self.underlying = Some(underlying.into());
self
}
pub fn category(mut self, category: impl Into<String>) -> Self {
self.category = Some(category.into());
self
}
pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
self.inst_family = Some(inst_family.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct AccountInstrumentsRequest {
#[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
inst_type: Option<InstType>,
#[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
underlying: Option<String>,
#[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
inst_family: Option<String>,
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<String>,
}
impl AccountInstrumentsRequest {
pub fn new() -> Self {
Self::default()
}
pub fn inst_type(mut self, inst_type: InstType) -> Self {
self.inst_type = Some(inst_type);
self
}
pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
self.underlying = Some(underlying.into());
self
}
pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
self.inst_family = Some(inst_family.into());
self
}
pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
self.inst_id = Some(inst_id.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct MaxLoanRequest {
#[serde(rename = "instId")]
inst_id: String,
#[serde(rename = "mgnMode")]
mgn_mode: TradeMode,
#[serde(rename = "mgnCcy", skip_serializing_if = "Option::is_none")]
mgn_ccy: Option<String>,
#[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
trade_quote_ccy: Option<String>,
}
impl MaxLoanRequest {
pub fn new(inst_id: impl Into<String>, mgn_mode: TradeMode) -> Self {
Self {
inst_id: inst_id.into(),
mgn_mode,
mgn_ccy: None,
trade_quote_ccy: None,
}
}
pub fn margin_currency(mut self, mgn_ccy: impl Into<String>) -> Self {
self.mgn_ccy = Some(mgn_ccy.into());
self
}
pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
self.trade_quote_ccy = Some(trade_quote_ccy.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct InterestAccruedRequest {
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(rename = "mgnMode", skip_serializing_if = "Option::is_none")]
mgn_mode: Option<TradeMode>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u32>,
}
impl InterestAccruedRequest {
pub fn new() -> Self {
Self::default()
}
pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
self.inst_id = Some(inst_id.into());
self
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
pub fn margin_mode(mut self, mgn_mode: TradeMode) -> Self {
self.mgn_mode = Some(mgn_mode);
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Serialize)]
struct SetGreeksBody<'a> {
#[serde(rename = "greeksType")]
greeks_type: &'a str,
}
#[derive(Serialize)]
struct SetIsolatedModeBody<'a> {
#[serde(rename = "isoMode")]
iso_mode: &'a str,
#[serde(rename = "type")]
mode_type: &'a str,
}
#[derive(Debug, Clone, Serialize)]
pub struct BorrowRepayRequest {
ccy: String,
side: String,
amt: String,
#[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
ord_id: Option<String>,
}
impl BorrowRepayRequest {
pub fn new(ccy: impl Into<String>, side: impl Into<String>, amt: impl Into<String>) -> Self {
Self {
ccy: ccy.into(),
side: side.into(),
amt: amt.into(),
ord_id: None,
}
}
pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
self.ord_id = Some(ord_id.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct BorrowRepayHistoryRequest {
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u32>,
}
impl BorrowRepayHistoryRequest {
pub fn new() -> Self {
Self::default()
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct InterestLimitsRequest {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
limit_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
}
impl InterestLimitsRequest {
pub fn new() -> Self {
Self::default()
}
pub fn limit_type(mut self, limit_type: impl Into<String>) -> Self {
self.limit_type = Some(limit_type.into());
self
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SimulatedPosition {
#[serde(rename = "instId")]
inst_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pos: Option<String>,
#[serde(rename = "avgPx", skip_serializing_if = "Option::is_none")]
avg_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
lever: Option<String>,
}
impl SimulatedPosition {
pub fn new(inst_id: impl Into<String>) -> Self {
Self {
inst_id: inst_id.into(),
pos: None,
avg_px: None,
lever: None,
}
}
pub fn position(mut self, pos: impl Into<String>) -> Self {
self.pos = Some(pos.into());
self
}
pub fn average_price(mut self, avg_px: impl Into<String>) -> Self {
self.avg_px = Some(avg_px.into());
self
}
pub fn leverage(mut self, lever: impl Into<String>) -> Self {
self.lever = Some(lever.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SimulatedAsset {
ccy: String,
#[serde(skip_serializing_if = "Option::is_none")]
eq: Option<String>,
}
impl SimulatedAsset {
pub fn new(ccy: impl Into<String>) -> Self {
Self {
ccy: ccy.into(),
eq: None,
}
}
pub fn equity(mut self, eq: impl Into<String>) -> Self {
self.eq = Some(eq.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct SimulatedMarginRequest {
#[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
inst_type: Option<InstType>,
#[serde(rename = "inclRealPos", skip_serializing_if = "Option::is_none")]
include_real_positions: Option<bool>,
#[serde(rename = "spotOffsetType", skip_serializing_if = "Option::is_none")]
spot_offset_type: Option<String>,
#[serde(rename = "simPos", skip_serializing_if = "Option::is_none")]
simulated_positions: Option<Vec<SimulatedPosition>>,
}
impl SimulatedMarginRequest {
pub fn new() -> Self {
Self::default()
}
pub fn inst_type(mut self, inst_type: InstType) -> Self {
self.inst_type = Some(inst_type);
self
}
pub fn include_real_positions(mut self, include_real_positions: bool) -> Self {
self.include_real_positions = Some(include_real_positions);
self
}
pub fn spot_offset_type(mut self, spot_offset_type: impl Into<String>) -> Self {
self.spot_offset_type = Some(spot_offset_type.into());
self
}
pub fn simulated_positions(mut self, simulated_positions: Vec<SimulatedPosition>) -> Self {
self.simulated_positions = Some(simulated_positions);
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct AccountPositionTiersRequest {
#[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
inst_type: Option<InstType>,
#[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
underlying: Option<String>,
#[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
inst_family: Option<String>,
}
impl AccountPositionTiersRequest {
pub fn new() -> Self {
Self::default()
}
pub fn inst_type(mut self, inst_type: InstType) -> Self {
self.inst_type = Some(inst_type);
self
}
pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
self.underlying = Some(underlying.into());
self
}
pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
self.inst_family = Some(inst_family.into());
self
}
}
#[derive(Serialize)]
struct TypeBody<'a> {
#[serde(rename = "type")]
value: &'a str,
}
#[derive(Serialize)]
struct SetAutoLoanBody {
#[serde(rename = "autoLoan")]
auto_loan: bool,
}
#[derive(Serialize)]
struct SetAccountLevelBody<'a> {
#[serde(rename = "acctLv")]
acct_lv: &'a str,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct PositionBuilderRequest {
#[serde(rename = "acctLv", skip_serializing_if = "Option::is_none")]
acct_lv: Option<String>,
#[serde(rename = "inclRealPosAndEq", skip_serializing_if = "Option::is_none")]
include_real_positions_and_equity: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
lever: Option<String>,
#[serde(rename = "greeksType", skip_serializing_if = "Option::is_none")]
greeks_type: Option<String>,
#[serde(rename = "simPos", skip_serializing_if = "Option::is_none")]
simulated_positions: Option<Vec<SimulatedPosition>>,
#[serde(rename = "simAsset", skip_serializing_if = "Option::is_none")]
simulated_assets: Option<Vec<SimulatedAsset>>,
#[serde(rename = "idxVol", skip_serializing_if = "Option::is_none")]
index_volatility: Option<String>,
}
impl PositionBuilderRequest {
pub fn new() -> Self {
Self::default()
}
pub fn account_level(mut self, acct_lv: impl Into<String>) -> Self {
self.acct_lv = Some(acct_lv.into());
self
}
pub fn include_real_positions_and_equity(mut self, include: bool) -> Self {
self.include_real_positions_and_equity = Some(include);
self
}
pub fn leverage(mut self, lever: impl Into<String>) -> Self {
self.lever = Some(lever.into());
self
}
pub fn greeks_type(mut self, greeks_type: impl Into<String>) -> Self {
self.greeks_type = Some(greeks_type.into());
self
}
pub fn simulated_positions(mut self, simulated_positions: Vec<SimulatedPosition>) -> Self {
self.simulated_positions = Some(simulated_positions);
self
}
pub fn simulated_assets(mut self, simulated_assets: Vec<SimulatedAsset>) -> Self {
self.simulated_assets = Some(simulated_assets);
self
}
pub fn index_volatility(mut self, index_volatility: impl Into<String>) -> Self {
self.index_volatility = Some(index_volatility.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct PositionsHistoryRequest {
#[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
inst_type: Option<InstType>,
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<String>,
#[serde(rename = "mgnMode", skip_serializing_if = "Option::is_none")]
mgn_mode: Option<TradeMode>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
close_type: Option<String>,
#[serde(rename = "posId", skip_serializing_if = "Option::is_none")]
pos_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u32>,
}
impl PositionsHistoryRequest {
pub fn new() -> Self {
Self::default()
}
pub fn inst_type(mut self, inst_type: InstType) -> Self {
self.inst_type = Some(inst_type);
self
}
pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
self.inst_id = Some(inst_id.into());
self
}
pub fn margin_mode(mut self, mgn_mode: TradeMode) -> Self {
self.mgn_mode = Some(mgn_mode);
self
}
pub fn close_type(mut self, close_type: impl Into<String>) -> Self {
self.close_type = Some(close_type.into());
self
}
pub fn position_id(mut self, pos_id: impl Into<String>) -> Self {
self.pos_id = Some(pos_id.into());
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AccountBalance {
#[serde(default)]
pub total_eq: NumberString,
#[serde(default)]
pub adj_eq: NumberString,
#[serde(default)]
pub details: Vec<BalanceDetail>,
#[serde(default)]
pub u_time: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct BalanceDetail {
pub ccy: String,
#[serde(default)]
pub eq: NumberString,
#[serde(default)]
pub cash_bal: NumberString,
#[serde(default)]
pub avail_bal: NumberString,
#[serde(default)]
pub frozen_bal: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Position {
pub inst_type: InstType,
pub inst_id: String,
#[serde(default)]
pub pos_id: String,
pub pos_side: PositionSide,
pub mgn_mode: TradeMode,
#[serde(default)]
pub pos: NumberString,
#[serde(default)]
pub avg_px: NumberString,
#[serde(default)]
pub upl: NumberString,
#[serde(default)]
pub lever: NumberString,
#[serde(default)]
pub liq_px: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PositionRisk {
#[serde(default)]
pub adj_eq: NumberString,
#[serde(default)]
pub bal_data: Vec<BalanceDetail>,
#[serde(default)]
pub pos_data: Vec<Position>,
#[serde(default)]
pub ts: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AccountConfig {
#[serde(default)]
pub uid: String,
#[serde(default)]
pub acct_lv: String,
#[serde(default)]
pub pos_mode: String,
#[serde(default)]
pub greeks_type: String,
#[serde(default)]
pub auto_loan: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AccountBill {
#[serde(default)]
pub bill_id: String,
#[serde(default)]
pub inst_type: String,
#[serde(default)]
pub inst_id: String,
#[serde(default)]
pub ccy: String,
#[serde(default)]
pub mgn_mode: String,
#[serde(rename = "type", default)]
pub bill_type: String,
#[serde(default)]
pub sub_type: String,
#[serde(default)]
pub sz: NumberString,
#[serde(default)]
pub bal: NumberString,
#[serde(default)]
pub ts: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SetPositionModeResult {
#[serde(default)]
pub pos_mode: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LeverageInfo {
#[serde(default)]
pub inst_id: String,
pub mgn_mode: TradeMode,
pub pos_side: PositionSide,
#[serde(default)]
pub lever: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct MaxOrderSize {
pub inst_id: String,
#[serde(default)]
pub max_buy: NumberString,
#[serde(default)]
pub max_sell: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct MaxAvailableSize {
pub inst_id: String,
#[serde(default)]
pub avail_buy: NumberString,
#[serde(default)]
pub avail_sell: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct FeeRate {
pub inst_type: InstType,
#[serde(default)]
pub inst_id: String,
#[serde(default)]
pub category: String,
#[serde(default)]
pub maker: NumberString,
#[serde(default)]
pub taker: NumberString,
#[serde(default)]
pub ts: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct MaxWithdrawal {
pub ccy: String,
#[serde(default)]
pub max_wd: NumberString,
#[serde(default)]
pub max_wd_ex: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PositionHistory {
pub inst_type: InstType,
pub inst_id: String,
#[serde(default)]
pub pos_id: String,
pub mgn_mode: TradeMode,
#[serde(rename = "type", default)]
pub close_type: String,
#[serde(default)]
pub realized_pnl: NumberString,
#[serde(default)]
pub c_time: NumberString,
#[serde(default)]
pub u_time: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RiskState {
#[serde(default)]
pub at_risk: String,
#[serde(default)]
pub ts: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AdjustMarginResult {
#[serde(default)]
pub inst_id: String,
#[serde(default)]
pub pos_side: String,
#[serde(default)]
pub amt: NumberString,
#[serde(rename = "type", default)]
pub adjustment_type: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AccountInstrument {
#[serde(default)]
pub inst_type: String,
#[serde(default)]
pub inst_id: String,
#[serde(default)]
pub uly: String,
#[serde(default)]
pub inst_family: String,
#[serde(default)]
pub base_ccy: String,
#[serde(default)]
pub quote_ccy: String,
#[serde(default)]
pub settle_ccy: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct MaxLoan {
#[serde(default)]
pub inst_id: String,
#[serde(default)]
pub mgn_mode: String,
#[serde(default)]
pub mgn_ccy: String,
#[serde(default)]
pub max_loan: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct InterestAccrued {
#[serde(default)]
pub inst_id: String,
#[serde(default)]
pub ccy: String,
#[serde(default)]
pub mgn_mode: String,
#[serde(default)]
pub interest: NumberString,
#[serde(default)]
pub interest_rate: NumberString,
#[serde(default)]
pub liab: NumberString,
#[serde(default)]
pub ts: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct InterestRate {
#[serde(default)]
pub ccy: String,
#[serde(default)]
pub interest_rate: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SetGreeksResult {
#[serde(default)]
pub greeks_type: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SetIsolatedModeResult {
#[serde(default)]
pub iso_mode: String,
#[serde(rename = "type", default)]
pub mode_type: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct BorrowRepayResult {
#[serde(default)]
pub ccy: String,
#[serde(default)]
pub side: String,
#[serde(default)]
pub amt: NumberString,
#[serde(default)]
pub ord_id: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct BorrowRepayHistory {
#[serde(default)]
pub ccy: String,
#[serde(default)]
pub side: String,
#[serde(default)]
pub amt: NumberString,
#[serde(default)]
pub ord_id: String,
#[serde(default)]
pub state: String,
#[serde(default)]
pub ts: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct InterestLimit {
#[serde(default)]
pub ccy: String,
#[serde(default)]
pub rate: NumberString,
#[serde(default)]
pub loan_quota: NumberString,
#[serde(default)]
pub used_loan: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SimulatedMargin {
#[serde(default)]
pub imr: NumberString,
#[serde(default)]
pub mmr: NumberString,
#[serde(default)]
pub mr: NumberString,
#[serde(default)]
pub notional_usd: NumberString,
#[serde(default)]
pub details: Vec<SimulatedMarginDetail>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SimulatedMarginDetail {
#[serde(default)]
pub inst_id: String,
#[serde(default)]
pub pos: NumberString,
#[serde(default)]
pub imr: NumberString,
#[serde(default)]
pub mmr: NumberString,
#[serde(default)]
pub upl: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Greek {
#[serde(default)]
pub ccy: String,
#[serde(rename = "deltaBS", default)]
pub delta_bs: NumberString,
#[serde(rename = "deltaPA", default)]
pub delta_pa: NumberString,
#[serde(rename = "gammaBS", default)]
pub gamma_bs: NumberString,
#[serde(rename = "thetaBS", default)]
pub theta_bs: NumberString,
#[serde(rename = "vegaBS", default)]
pub vega_bs: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AccountPositionTier {
#[serde(default)]
pub inst_type: String,
#[serde(default)]
pub uly: String,
#[serde(default)]
pub inst_family: String,
#[serde(default)]
pub pos_type: String,
#[serde(default)]
pub min_sz: NumberString,
#[serde(default)]
pub max_sz: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SetRiskOffsetTypeResult {
#[serde(rename = "type", default)]
pub risk_offset_type: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SetAutoLoanResult {
#[serde(default)]
pub auto_loan: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SetAccountLevelResult {
#[serde(default)]
pub acct_lv: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ActivateOptionResult {
#[serde(default)]
pub result: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PositionBuilderResult {
#[serde(default)]
pub acct_lv: String,
#[serde(default)]
pub adj_eq: NumberString,
#[serde(default)]
pub imr: NumberString,
#[serde(default)]
pub mmr: NumberString,
#[serde(default)]
pub mr: NumberString,
#[serde(default)]
pub pos_data: Vec<PositionBuilderPosition>,
#[serde(default)]
pub asset_data: Vec<PositionBuilderAsset>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PositionBuilderPosition {
#[serde(default)]
pub inst_type: String,
#[serde(default)]
pub inst_id: String,
#[serde(default)]
pub pos: NumberString,
#[serde(default)]
pub avg_px: NumberString,
#[serde(default)]
pub upl: NumberString,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PositionBuilderAsset {
#[serde(default)]
pub ccy: String,
#[serde(default)]
pub eq: NumberString,
}
#[cfg(test)]
mod tests {
use crate::test_util::MockTransport;
use crate::{Credentials, OkxClient};
fn signed_client(mock: MockTransport) -> OkxClient<MockTransport> {
OkxClient::with_transport(mock)
.credentials(Credentials::new("key", "secret", "pass"))
.build()
}
#[tokio::test]
async fn get_balance_signs_request_and_parses() {
let body = r#"{"code":"0","msg":"","data":[
{"totalEq":"10000","adjEq":"9500","uTime":"1597026383085","details":[
{"ccy":"USDT","eq":"10000","cashBal":"10000","availBal":"9000","frozenBal":"1000"}]}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let balances = client.account().get_balance(None).await.unwrap();
assert_eq!(balances[0].total_eq.as_str(), "10000");
assert_eq!(balances[0].details[0].ccy, "USDT");
assert_eq!(balances[0].details[0].avail_bal.as_str(), "9000");
let req = mock.captured();
assert_eq!(req.method, http::Method::GET);
assert!(req.uri.ends_with("/api/v5/account/balance"));
assert!(req.is_signed(), "authenticated endpoint must be signed");
}
#[tokio::test]
async fn get_positions_passes_filters() {
let body = r#"{"code":"0","msg":"","data":[
{"instType":"SWAP","instId":"BTC-USDT-SWAP","posId":"1","posSide":"long",
"mgnMode":"cross","pos":"1","avgPx":"42000","upl":"5","lever":"10","liqPx":"38000"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let positions = client
.account()
.get_positions(Some(crate::model::InstType::Swap), Some("BTC-USDT-SWAP"))
.await
.unwrap();
assert_eq!(positions[0].inst_id, "BTC-USDT-SWAP");
assert_eq!(positions[0].pos_side, crate::model::PositionSide::Long);
let req = mock.captured();
assert_eq!(req.query(), Some("instType=SWAP&instId=BTC-USDT-SWAP"));
assert!(req.is_signed());
}
#[tokio::test]
async fn missing_credentials_is_configuration_error() {
let mock = MockTransport::new("{}");
let client = OkxClient::with_transport(mock).build();
let err = client.account().get_balance(None).await.unwrap_err();
assert!(matches!(err, crate::Error::Configuration(_)));
}
#[tokio::test]
async fn get_position_risk_signs_and_parses() {
let body = r#"{"code":"0","msg":"","data":[
{"adjEq":"1000","ts":"1597026383085","balData":[{"ccy":"USDT","eq":"1000"}],
"posData":[{"instType":"SWAP","instId":"BTC-USDT-SWAP","posSide":"long","mgnMode":"cross","pos":"1"}]}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let risk = client
.account()
.get_position_risk(Some(crate::model::InstType::Swap))
.await
.unwrap();
assert_eq!(risk[0].adj_eq.as_str(), "1000");
assert_eq!(risk[0].pos_data[0].inst_id, "BTC-USDT-SWAP");
let req = mock.captured();
assert_eq!(req.query(), Some("instType=SWAP"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_account_config_signs_and_parses() {
let body = r#"{"code":"0","msg":"","data":[
{"uid":"1","acctLv":"2","posMode":"net_mode","greeksType":"PA","autoLoan":false}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let config = client.account().get_account_config().await.unwrap();
assert_eq!(config[0].pos_mode, "net_mode");
let req = mock.captured();
assert!(req.uri.ends_with("/api/v5/account/config"));
assert_eq!(req.query(), None);
assert!(req.is_signed());
}
#[tokio::test]
async fn get_account_bills_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"billId":"1","instType":"SPOT","ccy":"USDT","mgnMode":"cash","type":"1","subType":"2",
"sz":"10","bal":"100","ts":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::BillsRequest::new()
.inst_type(crate::model::InstType::Spot)
.currency("USDT")
.bill_type("1")
.limit(1);
let bills = client.account().get_account_bills(&request).await.unwrap();
assert_eq!(bills[0].bill_id, "1");
let req = mock.captured();
assert_eq!(req.query(), Some("instType=SPOT&ccy=USDT&type=1&limit=1"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_account_bills_archive_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"billId":"2","instType":"SPOT","ccy":"USDT","type":"1","sz":"10","bal":"100","ts":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::BillsArchiveRequest::new()
.filters(super::BillsRequest::new().currency("USDT"))
.begin("100")
.end("200");
let bills = client
.account()
.get_account_bills_archive(&request)
.await
.unwrap();
assert_eq!(bills[0].bill_id, "2");
let req = mock.captured();
assert_eq!(req.query(), Some("ccy=USDT&begin=100&end=200"));
assert!(req.is_signed());
}
#[tokio::test]
async fn set_position_mode_posts_body() {
let body = r#"{"code":"0","msg":"","data":[{"posMode":"net_mode"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client.account().set_position_mode("net_mode").await.unwrap();
assert_eq!(result[0].pos_mode, "net_mode");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/set-position-mode"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["posMode"], "net_mode");
assert!(req.is_signed());
}
#[tokio::test]
async fn set_leverage_posts_builder_body() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT-SWAP","mgnMode":"cross","posSide":"long","lever":"10"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::SetLeverageRequest::new("10", crate::model::TradeMode::Cross)
.inst_id("BTC-USDT-SWAP")
.position_side(crate::model::PositionSide::Long);
let result = client.account().set_leverage(&request).await.unwrap();
assert_eq!(result[0].lever.as_str(), "10");
let req = mock.captured();
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["lever"], "10");
assert_eq!(sent["mgnMode"], "cross");
assert_eq!(sent["instId"], "BTC-USDT-SWAP");
assert_eq!(sent["posSide"], "long");
assert!(req.is_signed());
}
#[tokio::test]
async fn get_leverage_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT-SWAP","mgnMode":"cross","posSide":"long","lever":"10"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::LeverageRequest::new(crate::model::TradeMode::Cross)
.inst_id("BTC-USDT-SWAP");
let result = client.account().get_leverage(&request).await.unwrap();
assert_eq!(result[0].mgn_mode, crate::model::TradeMode::Cross);
let req = mock.captured();
assert_eq!(req.query(), Some("mgnMode=cross&instId=BTC-USDT-SWAP"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_max_order_size_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT","maxBuy":"1","maxSell":"2"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::MaxOrderSizeRequest::new("BTC-USDT", crate::model::TradeMode::Cash)
.price("42000");
let result = client.account().get_max_order_size(&request).await.unwrap();
assert_eq!(result[0].max_buy.as_str(), "1");
let req = mock.captured();
assert_eq!(req.query(), Some("instId=BTC-USDT&tdMode=cash&px=42000"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_max_avail_size_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT","availBuy":"1","availSell":"2"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::MaxAvailableSizeRequest::new("BTC-USDT", crate::model::TradeMode::Cash)
.reduce_only(false);
let result = client.account().get_max_avail_size(&request).await.unwrap();
assert_eq!(result[0].avail_sell.as_str(), "2");
let req = mock.captured();
assert_eq!(
req.query(),
Some("instId=BTC-USDT&tdMode=cash&reduceOnly=false")
);
assert!(req.is_signed());
}
#[tokio::test]
async fn get_fee_rates_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instType":"SPOT","instId":"BTC-USDT","category":"1","maker":"-0.0001","taker":"0.001","ts":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::FeeRatesRequest::new(crate::model::InstType::Spot)
.inst_id("BTC-USDT");
let result = client.account().get_fee_rates(&request).await.unwrap();
assert_eq!(result[0].maker.as_str(), "-0.0001");
let req = mock.captured();
assert_eq!(req.query(), Some("instType=SPOT&instId=BTC-USDT"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_max_withdrawal_queries_currency() {
let body = r#"{"code":"0","msg":"","data":[
{"ccy":"USDT","maxWd":"100","maxWdEx":"90"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client.account().get_max_withdrawal(Some("USDT")).await.unwrap();
assert_eq!(result[0].max_wd.as_str(), "100");
let req = mock.captured();
assert_eq!(req.query(), Some("ccy=USDT"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_positions_history_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instType":"SWAP","instId":"BTC-USDT-SWAP","posId":"1","mgnMode":"cross",
"type":"2","realizedPnl":"5","cTime":"1597026383085","uTime":"1597026383999"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::PositionsHistoryRequest::new()
.inst_type(crate::model::InstType::Swap)
.inst_id("BTC-USDT-SWAP")
.limit(1);
let result = client.account().get_positions_history(&request).await.unwrap();
assert_eq!(result[0].realized_pnl.as_str(), "5");
let req = mock.captured();
assert_eq!(
req.query(),
Some("instType=SWAP&instId=BTC-USDT-SWAP&limit=1")
);
assert!(req.is_signed());
}
#[tokio::test]
async fn get_risk_state_signs_and_parses() {
let body = r#"{"code":"0","msg":"","data":[{"atRisk":"false","ts":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client.account().get_risk_state().await.unwrap();
assert_eq!(result[0].at_risk, "false");
let req = mock.captured();
assert!(req.uri.ends_with("/api/v5/account/risk-state"));
assert_eq!(req.query(), None);
assert!(req.is_signed());
}
#[tokio::test]
async fn demo_trading_sets_simulated_header() {
let body = r#"{"code":"0","msg":"","data":[
{"uid":"1","acctLv":"2","posMode":"net_mode","greeksType":"PA","autoLoan":false}]}"#;
let mock = MockTransport::new(body);
let client = OkxClient::with_transport(mock.clone())
.credentials(Credentials::new("key", "secret", "pass"))
.demo_trading(true)
.build();
let config = client.account().get_account_config().await.unwrap();
assert_eq!(config[0].acct_lv, "2");
let req = mock.captured();
assert_eq!(
req.headers
.get("x-simulated-trading")
.and_then(|v| v.to_str().ok()),
Some("1")
);
assert!(req.is_signed());
}
#[tokio::test]
async fn adjust_margin_posts_body() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT-SWAP","posSide":"long","type":"add","amt":"100"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::AdjustMarginRequest::new(
"BTC-USDT-SWAP",
crate::model::PositionSide::Long,
"add",
"100",
)
.loan_transfer(true);
let result = client.account().adjust_margin(&request).await.unwrap();
assert_eq!(result[0].amt.as_str(), "100");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/position/margin-balance"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["instId"], "BTC-USDT-SWAP");
assert_eq!(sent["posSide"], "long");
assert_eq!(sent["type"], "add");
assert_eq!(sent["amt"], "100");
assert_eq!(sent["loanTrans"], true);
assert!(req.is_signed());
}
#[tokio::test]
async fn get_account_instruments_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instType":"SWAP","instId":"BTC-USDT-SWAP","uly":"BTC-USDT","instFamily":"BTC-USDT",
"baseCcy":"BTC","quoteCcy":"USDT","settleCcy":"USDT"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::AccountInstrumentsRequest::new()
.inst_type(crate::model::InstType::Swap)
.inst_id("BTC-USDT-SWAP");
let result = client
.account()
.get_account_instruments(&request)
.await
.unwrap();
assert_eq!(result[0].settle_ccy, "USDT");
let req = mock.captured();
assert_eq!(req.query(), Some("instType=SWAP&instId=BTC-USDT-SWAP"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_max_loan_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT","mgnMode":"cross","mgnCcy":"USDT","maxLoan":"1000"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::MaxLoanRequest::new("BTC-USDT", crate::model::TradeMode::Cross)
.margin_currency("USDT");
let result = client.account().get_max_loan(&request).await.unwrap();
assert_eq!(result[0].max_loan.as_str(), "1000");
let req = mock.captured();
assert_eq!(req.query(), Some("instId=BTC-USDT&mgnMode=cross&mgnCcy=USDT"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_interest_accrued_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instId":"BTC-USDT","ccy":"USDT","mgnMode":"cross","interest":"1",
"interestRate":"0.0001","liab":"100","ts":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::InterestAccruedRequest::new()
.inst_id("BTC-USDT")
.currency("USDT")
.limit(1);
let result = client.account().get_interest_accrued(&request).await.unwrap();
assert_eq!(result[0].interest_rate.as_str(), "0.0001");
let req = mock.captured();
assert_eq!(req.query(), Some("instId=BTC-USDT&ccy=USDT&limit=1"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_interest_rate_queries_currency() {
let body = r#"{"code":"0","msg":"","data":[{"ccy":"USDT","interestRate":"0.0001"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client.account().get_interest_rate(Some("USDT")).await.unwrap();
assert_eq!(result[0].ccy, "USDT");
let req = mock.captured();
assert_eq!(req.query(), Some("ccy=USDT"));
assert!(req.is_signed());
}
#[tokio::test]
async fn set_greeks_posts_body() {
let body = r#"{"code":"0","msg":"","data":[{"greeksType":"PA"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client.account().set_greeks("PA").await.unwrap();
assert_eq!(result[0].greeks_type, "PA");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/set-greeks"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["greeksType"], "PA");
assert!(req.is_signed());
}
#[tokio::test]
async fn set_isolated_mode_posts_body() {
let body = r#"{"code":"0","msg":"","data":[{"isoMode":"automatic","type":"MARGIN"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client
.account()
.set_isolated_mode("automatic", "MARGIN")
.await
.unwrap();
assert_eq!(result[0].iso_mode, "automatic");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/set-isolated-mode"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["isoMode"], "automatic");
assert_eq!(sent["type"], "MARGIN");
assert!(req.is_signed());
}
#[tokio::test]
async fn borrow_repay_posts_body() {
let body = r#"{"code":"0","msg":"","data":[
{"ccy":"USDT","side":"borrow","amt":"100","ordId":"1"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::BorrowRepayRequest::new("USDT", "borrow", "100").order_id("1");
let result = client.account().borrow_repay(&request).await.unwrap();
assert_eq!(result[0].ord_id, "1");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/borrow-repay"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["ccy"], "USDT");
assert_eq!(sent["side"], "borrow");
assert_eq!(sent["amt"], "100");
assert_eq!(sent["ordId"], "1");
assert!(req.is_signed());
}
#[tokio::test]
async fn get_borrow_repay_history_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"ccy":"USDT","side":"borrow","amt":"100","ordId":"1","state":"2","ts":"1597026383085"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::BorrowRepayHistoryRequest::new()
.currency("USDT")
.limit(1);
let result = client
.account()
.get_borrow_repay_history(&request)
.await
.unwrap();
assert_eq!(result[0].state, "2");
let req = mock.captured();
assert_eq!(req.query(), Some("ccy=USDT&limit=1"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_interest_limits_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"ccy":"USDT","rate":"0.0001","loanQuota":"1000","usedLoan":"100"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::InterestLimitsRequest::new()
.limit_type("1")
.currency("USDT");
let result = client.account().get_interest_limits(&request).await.unwrap();
assert_eq!(result[0].loan_quota.as_str(), "1000");
let req = mock.captured();
assert_eq!(req.query(), Some("type=1&ccy=USDT"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_simulated_margin_posts_body_and_omits_unset_fields() {
let body = r#"{"code":"0","msg":"","data":[
{"imr":"10","mmr":"5","mr":"100","notionalUsd":"1000",
"details":[{"instId":"BTC-USDT-SWAP","pos":"1","imr":"10","mmr":"5","upl":"2"}]}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::SimulatedMarginRequest::new()
.inst_type(crate::model::InstType::Swap)
.simulated_positions(vec![
super::SimulatedPosition::new("BTC-USDT-SWAP")
.position("1")
.leverage("10"),
]);
let result = client.account().get_simulated_margin(&request).await.unwrap();
assert_eq!(result[0].details[0].upl.as_str(), "2");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/simulated_margin"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["instType"], "SWAP");
assert_eq!(sent["simPos"][0]["instId"], "BTC-USDT-SWAP");
assert_eq!(sent["simPos"][0]["pos"], "1");
assert_eq!(sent["simPos"][0]["lever"], "10");
assert!(sent.get("inclRealPos").is_none());
assert!(sent["simPos"][0].get("avgPx").is_none());
assert!(req.is_signed());
}
#[tokio::test]
async fn get_greeks_queries_currency() {
let body = r#"{"code":"0","msg":"","data":[
{"ccy":"BTC","deltaBS":"1","deltaPA":"0.9","gammaBS":"0.1","thetaBS":"-0.01","vegaBS":"2"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client.account().get_greeks(Some("BTC")).await.unwrap();
assert_eq!(result[0].delta_pa.as_str(), "0.9");
let req = mock.captured();
assert_eq!(req.query(), Some("ccy=BTC"));
assert!(req.is_signed());
}
#[tokio::test]
async fn get_account_position_tiers_uses_builder_query() {
let body = r#"{"code":"0","msg":"","data":[
{"instType":"OPTION","uly":"BTC-USD","instFamily":"BTC-USD","posType":"1","minSz":"0","maxSz":"100"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::AccountPositionTiersRequest::new()
.inst_type(crate::model::InstType::Option)
.underlying("BTC-USD");
let result = client
.account()
.get_account_position_tiers(&request)
.await
.unwrap();
assert_eq!(result[0].max_sz.as_str(), "100");
let req = mock.captured();
assert_eq!(req.query(), Some("instType=OPTION&uly=BTC-USD"));
assert!(req.is_signed());
}
#[tokio::test]
async fn set_risk_offset_type_posts_body() {
let body = r#"{"code":"0","msg":"","data":[{"type":"1"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client.account().set_risk_offset_type("1").await.unwrap();
assert_eq!(result[0].risk_offset_type, "1");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/set-riskOffset-type"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["type"], "1");
assert!(req.is_signed());
}
#[tokio::test]
async fn set_auto_loan_posts_body() {
let body = r#"{"code":"0","msg":"","data":[{"autoLoan":"true"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client.account().set_auto_loan(true).await.unwrap();
assert_eq!(result[0].auto_loan, "true");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/set-auto-loan"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["autoLoan"], true);
assert!(req.is_signed());
}
#[tokio::test]
async fn set_account_level_posts_body() {
let body = r#"{"code":"0","msg":"","data":[{"acctLv":"2"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client.account().set_account_level("2").await.unwrap();
assert_eq!(result[0].acct_lv, "2");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/set-account-level"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["acctLv"], "2");
assert!(req.is_signed());
}
#[tokio::test]
async fn activate_option_posts_empty_body() {
let body = r#"{"code":"0","msg":"","data":[{"result":"true"}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let result = client.account().activate_option().await.unwrap();
assert_eq!(result[0].result, "true");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/activate-option"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent, serde_json::json!({}));
assert!(req.is_signed());
}
#[tokio::test]
async fn position_builder_posts_body_and_omits_unset_fields() {
let body = r#"{"code":"0","msg":"","data":[
{"acctLv":"2","adjEq":"1000","imr":"10","mmr":"5","mr":"100",
"posData":[{"instType":"SWAP","instId":"BTC-USDT-SWAP","pos":"1","avgPx":"42000","upl":"2"}],
"assetData":[{"ccy":"USDT","eq":"1000"}]}]}"#;
let mock = MockTransport::new(body);
let client = signed_client(mock.clone());
let request = super::PositionBuilderRequest::new()
.account_level("2")
.include_real_positions_and_equity(false)
.simulated_positions(vec![
super::SimulatedPosition::new("BTC-USDT-SWAP").position("1"),
])
.simulated_assets(vec![super::SimulatedAsset::new("USDT").equity("1000")]);
let result = client.account().position_builder(&request).await.unwrap();
assert_eq!(result[0].pos_data[0].inst_id, "BTC-USDT-SWAP");
assert_eq!(result[0].asset_data[0].eq.as_str(), "1000");
let req = mock.captured();
assert_eq!(req.method, http::Method::POST);
assert!(req.uri.ends_with("/api/v5/account/position-builder"));
let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
assert_eq!(sent["acctLv"], "2");
assert_eq!(sent["inclRealPosAndEq"], false);
assert_eq!(sent["simPos"][0]["instId"], "BTC-USDT-SWAP");
assert_eq!(sent["simAsset"][0]["ccy"], "USDT");
assert!(sent.get("lever").is_none());
assert!(sent["simPos"][0].get("avgPx").is_none());
assert!(req.is_signed());
}
}