use crate::api::{API_COMMON_PATH, API_MIX_ACCOUNT_PATH, API_MIX_POSITION_PATH};
use crate::client::BitgetClient;
use crate::error::Error;
use reqwest::Method;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
#[derive(Clone)]
pub struct BitgetAccount {
client: BitgetClient,
}
impl BitgetAccount {
pub fn new(client: BitgetClient) -> Self {
Self { client }
}
pub fn from_env() -> Result<Self, Error> {
Ok(Self::new(BitgetClient::from_env()?))
}
pub async fn get_accounts(&self, product_type: &str) -> Result<Vec<Account>, Error> {
let path = format!("{API_MIX_ACCOUNT_PATH}/accounts");
let params = vec![("productType", product_type.to_string())];
self.client
.send_signed_request(Method::GET, &path, ¶ms)
.await
}
pub async fn get_single_account(
&self,
symbol: &str,
product_type: &str,
margin_coin: &str,
) -> Result<Value, Error> {
let path = format!("{API_MIX_ACCOUNT_PATH}/account");
let params = vec![
("marginCoin", margin_coin.to_string()),
("productType", product_type.to_string()),
("symbol", symbol.to_string()),
];
self.client
.send_signed_request(Method::GET, &path, ¶ms)
.await
}
pub async fn get_all_positions(
&self,
product_type: &str,
margin_coin: Option<&str>,
) -> Result<Value, Error> {
let path = format!("{API_MIX_POSITION_PATH}/all-position");
let mut params = vec![("productType", product_type.to_string())];
if let Some(margin_coin) = margin_coin {
params.push(("marginCoin", margin_coin.to_string()));
}
self.client
.send_signed_request(Method::GET, &path, ¶ms)
.await
}
pub async fn get_history_positions(
&self,
request: HistoryPositionRequest,
) -> Result<HistoricalPositionPage, Error> {
let path = format!("{API_MIX_POSITION_PATH}/history-position");
self.client
.send_signed_request(Method::GET, &path, &request.to_params())
.await
}
pub async fn get_bills(
&self,
product_type: &str,
coin: Option<&str>,
business_type: Option<&str>,
) -> Result<Value, Error> {
self.get_account_bills(
AccountBillRequest::new(product_type)
.with_optional_coin(coin)
.with_optional_business_type(business_type),
)
.await
}
pub async fn get_account_bills(&self, request: AccountBillRequest) -> Result<Value, Error> {
let path = format!("{API_MIX_ACCOUNT_PATH}/bill");
self.client
.send_signed_request(Method::GET, &path, &request.to_params())
.await
}
pub async fn set_leverage(
&self,
symbol: &str,
product_type: &str,
margin_coin: &str,
leverage: &str,
) -> Result<Value, Error> {
let path = format!("{API_MIX_ACCOUNT_PATH}/set-leverage");
let request = SetLeverageRequest {
symbol,
product_type,
margin_coin,
leverage,
};
self.client
.send_signed_json_request(Method::POST, &path, &[], &request)
.await
}
pub async fn set_margin_mode(
&self,
symbol: &str,
product_type: &str,
margin_coin: &str,
margin_mode: &str,
) -> Result<Value, Error> {
let path = format!("{API_MIX_ACCOUNT_PATH}/set-margin-mode");
let request = SetMarginModeRequest {
symbol,
product_type,
margin_coin,
margin_mode,
};
self.client
.send_signed_json_request(Method::POST, &path, &[], &request)
.await
}
pub async fn set_position_mode(
&self,
product_type: &str,
pos_mode: &str,
) -> Result<Value, Error> {
let path = format!("{API_MIX_ACCOUNT_PATH}/set-position-mode");
let request = SetPositionModeRequest {
product_type,
pos_mode,
};
self.client
.send_signed_json_request(Method::POST, &path, &[], &request)
.await
}
pub async fn set_position_margin(
&self,
symbol: &str,
product_type: &str,
margin_coin: &str,
amount: &str,
hold_side: &str,
) -> Result<Value, Error> {
let path = format!("{API_MIX_ACCOUNT_PATH}/set-margin");
let request = SetPositionMarginRequest {
symbol,
product_type,
margin_coin,
amount,
hold_side,
};
self.client
.send_signed_json_request(Method::POST, &path, &[], &request)
.await
}
pub async fn set_asset_mode(
&self,
product_type: &str,
asset_mode: &str,
) -> Result<Value, Error> {
let path = format!("{API_MIX_ACCOUNT_PATH}/set-asset-mode");
let request = SetAssetModeRequest {
product_type,
asset_mode,
};
self.client
.send_signed_json_request(Method::POST, &path, &[], &request)
.await
}
pub async fn get_sub_account_assets(
&self,
product_type: &str,
) -> Result<Vec<SubAccountAssets>, Error> {
let path = format!("{API_MIX_ACCOUNT_PATH}/sub-account-assets");
let params = vec![("productType", product_type.to_string())];
self.client
.send_signed_request(Method::GET, &path, ¶ms)
.await
}
pub async fn get_trade_rate(&self, symbol: &str, business_type: &str) -> Result<Value, Error> {
let path = format!("{API_COMMON_PATH}/trade-rate");
let params = vec![
("businessType", business_type.to_string()),
("symbol", symbol.to_string()),
];
self.client
.send_signed_request(Method::GET, &path, ¶ms)
.await
}
pub async fn get_trade_rate_info(
&self,
symbol: &str,
business_type: &str,
) -> Result<TradeRate, Error> {
let path = format!("{API_COMMON_PATH}/trade-rate");
let params = vec![
("businessType", business_type.to_string()),
("symbol", symbol.to_string()),
];
self.client
.send_signed_request(Method::GET, &path, ¶ms)
.await
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountBillRequest {
pub product_type: String,
pub coin: Option<String>,
pub business_type: Option<String>,
pub only_funding: Option<String>,
pub id_less_than: Option<String>,
pub start_time: Option<u64>,
pub end_time: Option<u64>,
pub limit: Option<u32>,
}
impl AccountBillRequest {
pub fn new(product_type: impl Into<String>) -> Self {
Self {
product_type: product_type.into(),
coin: None,
business_type: None,
only_funding: None,
id_less_than: None,
start_time: None,
end_time: None,
limit: None,
}
}
pub fn with_coin(mut self, value: impl Into<String>) -> Self {
self.coin = Some(value.into());
self
}
pub fn with_business_type(mut self, value: impl Into<String>) -> Self {
self.business_type = Some(value.into());
self
}
pub fn with_only_funding(mut self, value: impl Into<String>) -> Self {
self.only_funding = Some(value.into());
self
}
pub fn with_id_less_than(mut self, value: impl Into<String>) -> Self {
self.id_less_than = Some(value.into());
self
}
pub fn with_start_time(mut self, value: u64) -> Self {
self.start_time = Some(value);
self
}
pub fn with_end_time(mut self, value: u64) -> Self {
self.end_time = Some(value);
self
}
pub fn with_limit(mut self, value: u32) -> Self {
self.limit = Some(value);
self
}
fn with_optional_coin(mut self, value: Option<&str>) -> Self {
self.coin = value.map(ToOwned::to_owned);
self
}
fn with_optional_business_type(mut self, value: Option<&str>) -> Self {
self.business_type = value.map(ToOwned::to_owned);
self
}
fn to_params(&self) -> Vec<(&'static str, String)> {
let mut params = vec![("productType", self.product_type.clone())];
push_opt_string(&mut params, "coin", self.coin.clone());
push_opt_string(&mut params, "businessType", self.business_type.clone());
push_opt_string(&mut params, "onlyFunding", self.only_funding.clone());
push_opt_string(&mut params, "idLessThan", self.id_less_than.clone());
push_opt_string(
&mut params,
"startTime",
self.start_time.map(|value| value.to_string()),
);
push_opt_string(
&mut params,
"endTime",
self.end_time.map(|value| value.to_string()),
);
push_opt_string(
&mut params,
"limit",
self.limit.map(|value| value.to_string()),
);
params
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryPositionRequest {
pub product_type: Option<String>,
pub symbol: Option<String>,
pub id_less_than: Option<String>,
pub start_time: Option<u64>,
pub end_time: Option<u64>,
pub limit: Option<u32>,
}
impl HistoryPositionRequest {
pub fn new() -> Self {
Self {
product_type: None,
symbol: None,
id_less_than: None,
start_time: None,
end_time: None,
limit: None,
}
}
pub fn with_product_type(mut self, value: impl Into<String>) -> Self {
self.product_type = Some(value.into());
self
}
pub fn with_symbol(mut self, value: impl Into<String>) -> Self {
self.symbol = Some(value.into());
self
}
pub fn with_id_less_than(mut self, value: impl Into<String>) -> Self {
self.id_less_than = Some(value.into());
self
}
pub fn with_start_time(mut self, value: u64) -> Self {
self.start_time = Some(value);
self
}
pub fn with_end_time(mut self, value: u64) -> Self {
self.end_time = Some(value);
self
}
pub fn with_limit(mut self, value: u32) -> Self {
self.limit = Some(value);
self
}
fn to_params(&self) -> Vec<(&'static str, String)> {
let mut params = Vec::new();
push_opt_string(&mut params, "productType", self.product_type.clone());
push_opt_string(&mut params, "symbol", self.symbol.clone());
push_opt_string(&mut params, "idLessThan", self.id_less_than.clone());
push_opt_string(
&mut params,
"startTime",
self.start_time.map(|value| value.to_string()),
);
push_opt_string(
&mut params,
"endTime",
self.end_time.map(|value| value.to_string()),
);
push_opt_string(
&mut params,
"limit",
self.limit.map(|value| value.to_string()),
);
params
}
}
impl Default for HistoryPositionRequest {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Account {
pub margin_coin: String,
#[serde(default)]
pub locked: String,
#[serde(default)]
pub available: String,
#[serde(default)]
pub account_equity: String,
#[serde(default)]
pub usdt_equity: String,
#[serde(default)]
pub crossed_max_available: Option<String>,
#[serde(default)]
pub isolated_max_available: Option<String>,
#[serde(default)]
pub max_transfer_out: Option<String>,
#[serde(default)]
pub btc_equity: Option<String>,
#[serde(default)]
pub crossed_risk_rate: Option<String>,
#[serde(default, rename = "unrealizedPL", alias = "unrealizedPl")]
pub unrealized_pl: Option<String>,
#[serde(default)]
pub coupon: Option<String>,
#[serde(default, alias = "unionTotalMagin")]
pub union_total_margin: Option<String>,
#[serde(default)]
pub union_available: Option<String>,
#[serde(default)]
pub union_mm: Option<String>,
#[serde(default)]
pub asset_list: Vec<AccountAsset>,
#[serde(default)]
pub isolated_margin: Option<String>,
#[serde(default)]
pub crossed_margin: Option<String>,
#[serde(default, rename = "crossedUnrealizedPL", alias = "crossedUnrealizedPl")]
pub crossed_unrealized_pl: Option<String>,
#[serde(
default,
rename = "isolatedUnrealizedPL",
alias = "isolatedUnrealizedPl"
)]
pub isolated_unrealized_pl: Option<String>,
#[serde(default)]
pub grant: Option<String>,
#[serde(default)]
pub asset_mode: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AccountAsset {
pub coin: String,
#[serde(default)]
pub balance: String,
#[serde(default)]
pub available: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SubAccountAssets {
#[serde(deserialize_with = "deserialize_string_or_number")]
pub user_id: String,
#[serde(default)]
pub asset_list: Vec<Account>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct HistoricalPositionPage {
#[serde(default)]
pub list: Vec<HistoricalPosition>,
#[serde(default)]
pub end_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct HistoricalPosition {
pub position_id: String,
#[serde(default)]
pub margin_coin: String,
#[serde(default)]
pub symbol: String,
#[serde(default)]
pub hold_side: String,
#[serde(default)]
pub open_avg_price: String,
#[serde(default)]
pub close_avg_price: String,
#[serde(default)]
pub margin_mode: String,
#[serde(default)]
pub open_total_pos: String,
#[serde(default)]
pub close_total_pos: String,
#[serde(default)]
pub pnl: String,
#[serde(default)]
pub net_profit: String,
#[serde(default)]
pub total_funding: String,
#[serde(default)]
pub open_fee: String,
#[serde(default)]
pub close_fee: String,
#[serde(default)]
pub pos_mode: Option<String>,
#[serde(default, rename = "ctime", alias = "cTime")]
pub c_time: Option<String>,
#[serde(default, rename = "utime", alias = "uTime")]
pub u_time: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TradeRate {
#[serde(default)]
pub maker_fee_rate: String,
#[serde(default)]
pub taker_fee_rate: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SetLeverageRequest<'a> {
symbol: &'a str,
product_type: &'a str,
margin_coin: &'a str,
leverage: &'a str,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SetMarginModeRequest<'a> {
symbol: &'a str,
product_type: &'a str,
margin_coin: &'a str,
margin_mode: &'a str,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SetPositionModeRequest<'a> {
product_type: &'a str,
pos_mode: &'a str,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SetPositionMarginRequest<'a> {
symbol: &'a str,
product_type: &'a str,
margin_coin: &'a str,
amount: &'a str,
hold_side: &'a str,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SetAssetModeRequest<'a> {
product_type: &'a str,
asset_mode: &'a str,
}
fn push_opt_string(
params: &mut Vec<(&'static str, String)>,
key: &'static str,
value: Option<String>,
) {
if let Some(value) = value {
params.push((key, value));
}
}
fn deserialize_string_or_number<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
match value {
Value::String(value) => Ok(value),
Value::Number(value) => Ok(value.to_string()),
Value::Null => Ok(String::new()),
other => Err(serde::de::Error::custom(format!(
"expected string or number, got {other}"
))),
}
}