use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use serde_json::{json, Value};
use crate::core::{
HttpClient, Credentials,
ExchangeId, ExchangeType, AccountType, Symbol,
ExchangeError, ExchangeResult,
Price, Quantity, Kline, Ticker, OrderBook,
Order, OrderSide, OrderType, Balance, AccountInfo,
Position, FundingRate, MarginType,
OrderRequest, CancelRequest, CancelScope,
BalanceQuery, PositionQuery, PositionModification,
OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
TimeInForce, AmendRequest,
UserTrade, UserTradeFilter,
MarketDataCapabilities, TradingCapabilities, AccountCapabilities,
};
use crate::core::types::SymbolInfo;
use crate::core::traits::{
ExchangeIdentity, MarketData, Trading, Account, Positions,
};
use crate::core::{CancelAll, AmendOrder, BatchOrders, AccountTransfers, CustodialFunds, SubAccounts};
use crate::core::types::{ConnectorStats, CancelAllResponse, OrderResult};
use crate::core::types::{
TransferRequest, TransferHistoryFilter, TransferResponse,
DepositAddress, WithdrawRequest, WithdrawResponse, FundsRecord, FundsHistoryFilter, FundsRecordType,
SubAccountOperation, SubAccountResult, SubAccount,
};
use crate::core::types::{RateLimitCapabilities, LimitModel, RestLimitPool, WsLimits, OrderbookCapabilities, WsBookChannel};
use crate::core::utils::{RuntimeLimiter, RateLimitMonitor, RateLimitPressure};
use crate::core::utils::PrecisionCache;
use super::endpoints::{BingxUrls, BingxEndpoint, format_symbol, map_kline_interval};
use super::auth::BingxAuth;
use super::parser::BingxParser;
static BINGX_RATE_POOLS: &[RestLimitPool] = &[RestLimitPool {
name: "default",
max_budget: 500,
window_seconds: 10,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}];
static BINGX_RATE_CAPS: RateLimitCapabilities = RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: BINGX_RATE_POOLS,
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: None,
max_subs_per_conn: Some(200),
max_msg_per_sec: None,
max_streams_per_conn: None,
},
};
pub struct BingxConnector {
http: HttpClient,
auth: Option<BingxAuth>,
urls: BingxUrls,
testnet: bool,
limiter: Arc<Mutex<RuntimeLimiter>>,
monitor: Arc<Mutex<RateLimitMonitor>>,
precision: PrecisionCache,
}
impl BingxConnector {
pub async fn new(credentials: Option<Credentials>, testnet: bool) -> ExchangeResult<Self> {
let urls = BingxUrls::MAINNET;
let http = HttpClient::new(30_000)?;
let auth = credentials
.as_ref()
.map(BingxAuth::new)
.transpose()?;
let limiter = Arc::new(Mutex::new(RuntimeLimiter::from_caps(&BINGX_RATE_CAPS)));
let monitor = Arc::new(Mutex::new(RateLimitMonitor::new("BingX")));
Ok(Self {
http,
auth,
urls,
testnet,
limiter,
monitor,
precision: PrecisionCache::new(),
})
}
pub async fn public(testnet: bool) -> ExchangeResult<Self> {
Self::new(None, testnet).await
}
async fn rate_limit_wait(&self, weight: u32, essential: bool) -> bool {
loop {
let wait_time = {
let mut limiter = self.limiter.lock()
.expect("rate limiter mutex poisoned");
let pressure = self.monitor.lock()
.expect("rate monitor mutex poisoned")
.check(&mut limiter);
if pressure >= RateLimitPressure::Cutoff && !essential {
return false;
}
if limiter.try_acquire("default", weight) {
return true;
}
limiter.time_until_ready("default", weight)
};
if wait_time > Duration::ZERO {
tokio::time::sleep(wait_time).await;
}
}
}
async fn get(
&self,
endpoint: BingxEndpoint,
mut params: HashMap<String, String>,
account_type: AccountType,
) -> ExchangeResult<Value> {
if !self.rate_limit_wait(1, false).await {
return Err(ExchangeError::RateLimitExceeded {
retry_after: None,
message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
});
}
let base_url = self.urls.rest_url(account_type);
let path = endpoint.path();
let headers = if endpoint.requires_auth() {
let auth = self.auth.as_ref()
.ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
auth.sign_request(&mut params)
} else {
HashMap::new()
};
let query = if params.is_empty() {
String::new()
} else {
let qs: Vec<String> = params.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
format!("?{}", qs.join("&"))
};
let url = format!("{}{}{}", base_url, path, query);
let response = self.http.get_with_headers(&url, &HashMap::new(), &headers).await?;
Ok(response)
}
async fn post(
&self,
endpoint: BingxEndpoint,
mut params: HashMap<String, String>,
account_type: AccountType,
) -> ExchangeResult<Value> {
self.rate_limit_wait(1, true).await;
let base_url = self.urls.rest_url(account_type);
let path = endpoint.path();
let auth = self.auth.as_ref()
.ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
let headers = auth.sign_request(&mut params);
let query = params.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
let url = format!("{}{}?{}", base_url, path, query);
let response = self.http.post(&url, &json!({}), &headers).await?;
Ok(response)
}
async fn delete(
&self,
endpoint: BingxEndpoint,
mut params: HashMap<String, String>,
account_type: AccountType,
) -> ExchangeResult<Value> {
self.rate_limit_wait(1, true).await;
let base_url = self.urls.rest_url(account_type);
let path = endpoint.path();
let auth = self.auth.as_ref()
.ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
let headers = auth.sign_request(&mut params);
let query = params.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
let url = format!("{}{}?{}", base_url, path, query);
let response = self.http.delete(&url, &HashMap::new(), &headers).await?;
Ok(response)
}
fn check_response(&self, response: &Value) -> ExchangeResult<()> {
let code = response.get("code").and_then(|c| c.as_i64()).unwrap_or(0);
if code != 0 {
let msg = response.get("msg")
.and_then(|m| m.as_str())
.unwrap_or("Unknown error");
return Err(ExchangeError::Api {
code: code as i32,
message: msg.to_string(),
});
}
Ok(())
}
fn build_placed_order(
order_id: String,
client_order_id: Option<String>,
symbol: &Symbol,
side: OrderSide,
order_type: OrderType,
price: Option<Price>,
quantity: Quantity,
) -> Order {
Order {
id: order_id,
client_order_id,
symbol: symbol.to_string(),
side,
order_type,
status: crate::core::OrderStatus::New,
price,
stop_price: None,
quantity,
filled_quantity: 0.0,
average_price: None,
commission: None,
commission_asset: None,
created_at: crate::core::timestamp_millis() as i64,
updated_at: None,
time_in_force: TimeInForce::Gtc,
}
}
}
impl ExchangeIdentity for BingxConnector {
fn exchange_id(&self) -> ExchangeId {
ExchangeId::BingX
}
fn metrics(&self) -> ConnectorStats {
let (http_requests, http_errors, last_latency_ms) = self.http.stats();
let (rate_used, rate_max) = if let Ok(mut limiter) = self.limiter.lock() {
limiter.primary_stats()
} else {
(0, 0)
};
ConnectorStats {
http_requests,
http_errors,
last_latency_ms,
rate_used,
rate_max,
rate_groups: Vec::new(),
ws_ping_rtt_ms: 0,
}
}
fn is_testnet(&self) -> bool {
self.testnet
}
fn supported_account_types(&self) -> Vec<AccountType> {
vec![
AccountType::Spot,
AccountType::FuturesCross,
AccountType::FuturesIsolated,
]
}
fn exchange_type(&self) -> ExchangeType {
ExchangeType::Cex
}
fn rate_limit_capabilities(&self) -> RateLimitCapabilities {
BINGX_RATE_CAPS
}
fn orderbook_capabilities(&self, account_type: AccountType) -> OrderbookCapabilities {
static SPOT_CHANNELS: &[WsBookChannel] = &[
WsBookChannel::snapshot("@depth5", 5, 1000),
WsBookChannel::snapshot("@depth10", 10, 1000),
WsBookChannel::snapshot("@depth20", 20, 1000),
WsBookChannel::snapshot("@depth50", 50, 1000),
WsBookChannel::snapshot("@depth100", 100, 1000),
];
static FUTURES_CHANNELS: &[WsBookChannel] = &[
WsBookChannel::snapshot("@depth5", 5, 100),
WsBookChannel::snapshot("@depth10", 10, 100),
WsBookChannel::snapshot("@depth20", 20, 100),
WsBookChannel::snapshot("@depth50", 50, 100),
WsBookChannel::snapshot("@depth100", 100, 100),
];
match account_type {
AccountType::FuturesCross | AccountType::FuturesIsolated => OrderbookCapabilities {
ws_depths: &[5, 10, 20, 50, 100],
ws_default_depth: Some(20),
rest_max_depth: Some(1000),
rest_depth_values: &[5, 10, 20, 50, 100, 500, 1000],
supports_snapshot: true,
supports_delta: false,
update_speeds_ms: &[100, 200, 500, 1000],
default_speed_ms: Some(100),
ws_channels: FUTURES_CHANNELS,
checksum: None,
has_sequence: true,
has_prev_sequence: false,
supports_aggregation: false,
aggregation_levels: &[],
},
_ => OrderbookCapabilities {
ws_depths: &[5, 10, 20, 50, 100],
ws_default_depth: Some(20),
rest_max_depth: Some(1000),
rest_depth_values: &[5, 10, 20, 50, 100, 500, 1000],
supports_snapshot: true,
supports_delta: false,
update_speeds_ms: &[1000],
default_speed_ms: Some(1000),
ws_channels: SPOT_CHANNELS,
checksum: None,
has_sequence: true,
has_prev_sequence: false,
supports_aggregation: true,
aggregation_levels: &[],
},
}
}
}
#[async_trait]
impl MarketData for BingxConnector {
async fn get_price(
&self,
symbol: Symbol,
account_type: AccountType,
) -> ExchangeResult<Price> {
let ticker = self.get_ticker(symbol, account_type).await?;
Ok(ticker.last_price)
}
async fn get_orderbook(
&self,
symbol: Symbol,
depth: Option<u16>,
account_type: AccountType,
) -> ExchangeResult<OrderBook> {
let endpoint = match account_type {
AccountType::Spot | AccountType::Margin => BingxEndpoint::SpotDepth,
_ => BingxEndpoint::SwapDepth,
};
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&symbol.base, &symbol.quote, account_type));
if let Some(d) = depth {
params.insert("limit".to_string(), d.to_string());
}
let response = self.get(endpoint, params, account_type).await?;
BingxParser::parse_orderbook(&response)
}
async fn get_klines(
&self,
symbol: Symbol,
interval: &str,
limit: Option<u16>,
account_type: AccountType,
end_time: Option<i64>,
) -> ExchangeResult<Vec<Kline>> {
let endpoint = match account_type {
AccountType::Spot | AccountType::Margin => BingxEndpoint::SpotKlines,
_ => BingxEndpoint::SwapKlines,
};
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&symbol.base, &symbol.quote, account_type));
params.insert("interval".to_string(), map_kline_interval(interval).to_string());
if let Some(l) = limit {
params.insert("limit".to_string(), l.min(1440).to_string());
}
if let Some(et) = end_time {
params.insert("endTime".to_string(), et.to_string());
}
let response = self.get(endpoint, params, account_type).await?;
BingxParser::parse_klines(&response)
}
async fn get_ticker(
&self,
symbol: Symbol,
account_type: AccountType,
) -> ExchangeResult<Ticker> {
let endpoint = match account_type {
AccountType::Spot | AccountType::Margin => BingxEndpoint::SpotTickerBookTicker,
_ => BingxEndpoint::SwapTicker,
};
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&symbol.base, &symbol.quote, account_type));
let response = self.get(endpoint, params, account_type).await?;
BingxParser::parse_ticker(&response)
}
async fn ping(&self) -> ExchangeResult<()> {
let response = self.get(BingxEndpoint::SpotSymbols, HashMap::new(), AccountType::Spot).await?;
if response.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) == 0 {
Ok(())
} else {
Err(ExchangeError::Network("Ping failed".to_string()))
}
}
async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let info = match account_type {
AccountType::Spot | AccountType::Margin => {
let response = self.get(BingxEndpoint::SpotSymbols, HashMap::new(), AccountType::Spot).await?;
BingxParser::parse_spot_exchange_info(&response, account_type)?
}
_ => {
let response = self.get(BingxEndpoint::SwapContracts, HashMap::new(), AccountType::FuturesCross).await?;
BingxParser::parse_swap_exchange_info(&response, account_type)?
}
};
self.precision.load_from_symbols(&info);
Ok(info)
}
fn market_data_capabilities(&self, _account_type: AccountType) -> MarketDataCapabilities {
MarketDataCapabilities {
has_ping: true,
has_price: true,
has_ticker: true,
has_orderbook: true,
has_klines: true,
has_exchange_info: true,
has_recent_trades: false,
supported_intervals: &[
"1m", "3m", "5m", "15m", "30m",
"1h", "2h", "4h", "6h", "8h", "12h",
"1d", "3d", "1w", "1M",
],
max_kline_limit: Some(1440),
has_ws_klines: true,
has_ws_trades: true,
has_ws_orderbook: true,
has_ws_ticker: true,
}
}
}
#[async_trait]
impl Trading for BingxConnector {
async fn place_order(&self, req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
let symbol = req.symbol.clone();
let side = req.side;
let quantity = req.quantity;
let account_type = req.account_type;
let is_futures = matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated);
let endpoint = if is_futures { BingxEndpoint::SwapOrder } else { BingxEndpoint::SpotOrder };
let formatted_symbol = format_symbol(&symbol.base, &symbol.quote, account_type);
let side_str = match side { OrderSide::Buy => "BUY", OrderSide::Sell => "SELL" };
let mut params = HashMap::new();
params.insert("symbol".to_string(), formatted_symbol.clone());
params.insert("side".to_string(), side_str.to_string());
match req.order_type {
OrderType::Market => {
params.insert("type".to_string(), "MARKET".to_string());
if is_futures {
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
} else {
match side {
OrderSide::Buy => {
params.insert("quoteOrderQty".to_string(), self.precision.qty(&formatted_symbol, quantity));
}
OrderSide::Sell => {
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
}
}
}
}
OrderType::Limit { price } => {
params.insert("type".to_string(), "LIMIT".to_string());
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
params.insert("price".to_string(), self.precision.price(&formatted_symbol, price));
if is_futures {
params.insert("timeInForce".to_string(), "GTC".to_string());
}
}
OrderType::PostOnly { price } => {
if !is_futures {
return Err(ExchangeError::UnsupportedOperation(
"PostOnly is not documented for BingX Spot".to_string()
));
}
params.insert("type".to_string(), "LIMIT".to_string());
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
params.insert("price".to_string(), self.precision.price(&formatted_symbol, price));
params.insert("timeInForce".to_string(), "PostOnly".to_string());
}
OrderType::Ioc { price } => {
params.insert("type".to_string(), "LIMIT".to_string());
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
if let Some(p) = price {
params.insert("price".to_string(), self.precision.price(&formatted_symbol, p));
}
params.insert("timeInForce".to_string(), "IOC".to_string());
}
OrderType::Fok { price } => {
params.insert("type".to_string(), "LIMIT".to_string());
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
params.insert("price".to_string(), self.precision.price(&formatted_symbol, price));
params.insert("timeInForce".to_string(), "FOK".to_string());
}
OrderType::StopMarket { stop_price } => {
if !is_futures {
return Err(ExchangeError::UnsupportedOperation(
"StopMarket is only supported for BingX Swap (futures)".to_string()
));
}
params.insert("type".to_string(), "STOP_MARKET".to_string());
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
params.insert("stopPrice".to_string(), self.precision.price(&formatted_symbol, stop_price));
}
OrderType::StopLimit { stop_price, limit_price } => {
if !is_futures {
return Err(ExchangeError::UnsupportedOperation(
"StopLimit is only supported for BingX Swap (futures)".to_string()
));
}
params.insert("type".to_string(), "STOP".to_string());
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
params.insert("price".to_string(), self.precision.price(&formatted_symbol, limit_price));
params.insert("stopPrice".to_string(), self.precision.price(&formatted_symbol, stop_price));
params.insert("timeInForce".to_string(), "GTC".to_string());
}
OrderType::TrailingStop { callback_rate, activation_price } => {
if !is_futures {
return Err(ExchangeError::UnsupportedOperation(
"TrailingStop is only supported for BingX Swap (futures)".to_string()
));
}
params.insert("type".to_string(), "TRAILING_STOP_MARKET".to_string());
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
params.insert("priceRate".to_string(), callback_rate.to_string());
if let Some(act_price) = activation_price {
params.insert("activationPrice".to_string(), self.precision.price(&formatted_symbol, act_price));
}
}
OrderType::ReduceOnly { price } => {
if !is_futures {
return Err(ExchangeError::UnsupportedOperation(
"ReduceOnly is only supported for BingX Swap (futures)".to_string()
));
}
let (type_str, price_val) = if let Some(p) = price {
("LIMIT", Some(p))
} else {
("MARKET", None)
};
params.insert("type".to_string(), type_str.to_string());
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
params.insert("reduceOnly".to_string(), "true".to_string());
if let Some(p) = price_val {
params.insert("price".to_string(), self.precision.price(&formatted_symbol, p));
params.insert("timeInForce".to_string(), "GTC".to_string());
}
}
OrderType::Bracket { price, take_profit, stop_loss } => {
if !is_futures {
return Err(ExchangeError::UnsupportedOperation(
"Bracket orders are only supported for BingX Swap (futures)".to_string()
));
}
let type_str = if price.is_some() { "LIMIT" } else { "MARKET" };
params.insert("type".to_string(), type_str.to_string());
params.insert("quantity".to_string(), self.precision.qty(&formatted_symbol, quantity));
if let Some(p) = price {
params.insert("price".to_string(), self.precision.price(&formatted_symbol, p));
params.insert("timeInForce".to_string(), "GTC".to_string());
}
let tp_json = json!({
"type": "TAKE_PROFIT_MARKET",
"stopPrice": self.precision.price(&formatted_symbol, take_profit),
"price": "0",
"workingType": "MARK_PRICE"
});
let sl_json = json!({
"type": "STOP_MARKET",
"stopPrice": self.precision.price(&formatted_symbol, stop_loss),
"price": "0",
"workingType": "MARK_PRICE"
});
params.insert("takeProfit".to_string(), tp_json.to_string());
params.insert("stopLoss".to_string(), sl_json.to_string());
}
_ => {
return Err(ExchangeError::UnsupportedOperation(
format!("{:?} order type not supported on BingX", req.order_type)
));
}
}
let response = self.post(endpoint, params, account_type).await?;
self.check_response(&response)?;
let data = response.get("data").cloned().unwrap_or(json!({}));
let order_data = data.get("order").cloned()
.or_else(|| Some(data.clone()))
.unwrap_or(json!({}));
let order_id = order_data.get("orderId")
.and_then(|v| v.as_str().map(String::from)
.or_else(|| v.as_i64().map(|n| n.to_string())))
.unwrap_or_default();
let client_order_id = order_data.get("clientOrderId")
.and_then(|v| v.as_str())
.map(String::from);
let price_for_order = match &req.order_type {
OrderType::Limit { price } | OrderType::PostOnly { price } | OrderType::Fok { price } => Some(*price),
OrderType::Ioc { price } => *price,
OrderType::StopLimit { limit_price, .. } => Some(*limit_price),
_ => None,
};
Ok(PlaceOrderResponse::Simple(
Self::build_placed_order(order_id, client_order_id, &symbol, side, req.order_type, price_for_order, quantity)
))
}
async fn cancel_order(&self, req: CancelRequest) -> ExchangeResult<Order> {
match req.scope {
CancelScope::Single { ref order_id } => {
let symbol = req.symbol.as_ref()
.ok_or_else(|| ExchangeError::InvalidRequest("Symbol required for cancel".into()))?
.clone();
let account_type = req.account_type;
let is_futures = matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated);
let endpoint = if is_futures { BingxEndpoint::SwapOrder } else { BingxEndpoint::SpotOrder };
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&symbol.base, &symbol.quote, account_type));
params.insert("orderId".to_string(), order_id.to_string());
let response = self.delete(endpoint, params, account_type).await?;
self.check_response(&response)?;
BingxParser::parse_order(&response, &symbol.to_string())
}
_ => Err(ExchangeError::UnsupportedOperation(
format!("{:?} cancel scope — use CancelAll trait for all/bySymbol on BingX", req.scope)
)),
}
}
async fn get_order(
&self,
symbol: &str,
order_id: &str,
account_type: AccountType,
) -> ExchangeResult<Order> {
let symbol_parts: Vec<&str> = symbol.split('/').collect();
let symbol = if symbol_parts.len() == 2 {
crate::core::Symbol::new(symbol_parts[0], symbol_parts[1])
} else {
crate::core::Symbol { base: symbol.to_string(), quote: String::new(), raw: Some(symbol.to_string()) }
};
let endpoint = match account_type {
AccountType::Spot | AccountType::Margin => BingxEndpoint::SpotOrder,
_ => BingxEndpoint::SwapOrder,
};
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&symbol.base, &symbol.quote, account_type));
params.insert("orderId".to_string(), order_id.to_string());
let response = self.get(endpoint, params, account_type).await?;
BingxParser::parse_order(&response, &symbol.to_string())
}
async fn get_open_orders(
&self,
symbol: Option<&str>,
account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
let symbol: Option<crate::core::Symbol> = symbol.map(|s| {
let parts: Vec<&str> = s.split('/').collect();
if parts.len() == 2 {
crate::core::Symbol::new(parts[0], parts[1])
} else {
crate::core::Symbol { base: s.to_string(), quote: String::new(), raw: Some(s.to_string()) }
}
});
let endpoint = match account_type {
AccountType::Spot | AccountType::Margin => BingxEndpoint::SpotOpenOrders,
_ => BingxEndpoint::SwapOpenOrders,
};
let mut params = HashMap::new();
if let Some(s) = symbol {
params.insert("symbol".to_string(), format_symbol(&s.base, &s.quote, account_type));
}
let response = self.get(endpoint, params, account_type).await?;
BingxParser::parse_orders(&response)
}
async fn get_order_history(
&self,
filter: OrderHistoryFilter,
account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
let is_futures = matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated);
let mut params = HashMap::new();
if is_futures {
let sym = filter.symbol.as_ref()
.ok_or_else(|| ExchangeError::InvalidRequest(
"BingX Swap order history requires a symbol".to_string()
))?;
params.insert("symbol".to_string(), format_symbol(&sym.base, &sym.quote, account_type));
if let Some(start) = filter.start_time {
params.insert("startTime".to_string(), start.to_string());
}
if let Some(end) = filter.end_time {
params.insert("endTime".to_string(), end.to_string());
}
if let Some(limit) = filter.limit {
params.insert("limit".to_string(), limit.to_string());
}
let response = self.get(BingxEndpoint::SwapAllOrders, params, account_type).await?;
BingxParser::parse_orders(&response)
} else {
if let Some(ref sym) = filter.symbol {
params.insert("symbol".to_string(), format_symbol(&sym.base, &sym.quote, account_type));
}
if let Some(start) = filter.start_time {
params.insert("startTime".to_string(), start.to_string());
}
if let Some(end) = filter.end_time {
params.insert("endTime".to_string(), end.to_string());
}
if let Some(limit) = filter.limit {
params.insert("limit".to_string(), limit.to_string());
}
let response = self.get(BingxEndpoint::SpotHistoryOrders, params, account_type).await?;
BingxParser::parse_orders(&response)
}
}
async fn get_user_trades(
&self,
filter: UserTradeFilter,
account_type: AccountType,
) -> ExchangeResult<Vec<UserTrade>> {
let is_futures = matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated);
let mut params = HashMap::new();
if is_futures {
let sym = filter.symbol
.ok_or_else(|| ExchangeError::InvalidRequest(
"BingX swap user trades requires a symbol".to_string()
))?;
params.insert("symbol".to_string(), sym);
if let Some(oid) = filter.order_id {
params.insert("orderId".to_string(), oid);
}
if let Some(start) = filter.start_time {
params.insert("startTs".to_string(), start.to_string());
}
if let Some(end) = filter.end_time {
params.insert("endTs".to_string(), end.to_string());
}
if let Some(limit) = filter.limit {
params.insert("limit".to_string(), limit.min(100).to_string());
}
let response = self.get(BingxEndpoint::SwapFillHistory, params, account_type).await?;
BingxParser::parse_user_trades(&response, true)
} else {
let sym = filter.symbol
.ok_or_else(|| ExchangeError::InvalidRequest(
"BingX spot user trades requires a symbol".to_string()
))?;
params.insert("symbol".to_string(), sym);
if let Some(oid) = filter.order_id {
params.insert("orderId".to_string(), oid);
}
if let Some(start) = filter.start_time {
params.insert("startTime".to_string(), start.to_string());
}
if let Some(end) = filter.end_time {
params.insert("endTime".to_string(), end.to_string());
}
if let Some(limit) = filter.limit {
params.insert("limit".to_string(), limit.min(100).to_string());
}
let response = self.get(BingxEndpoint::SpotMyTrades, params, account_type).await?;
BingxParser::parse_user_trades(&response, false)
}
}
fn trading_capabilities(&self, account_type: AccountType) -> TradingCapabilities {
let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);
TradingCapabilities {
has_market_order: true,
has_limit_order: true,
has_stop_market: is_futures,
has_stop_limit: is_futures,
has_trailing_stop: is_futures,
has_bracket: is_futures,
has_oco: false,
has_amend: is_futures,
has_batch: is_futures,
max_batch_size: if is_futures { Some(5) } else { None },
has_cancel_all: true,
has_user_trades: true,
has_order_history: true,
}
}
}
#[async_trait]
impl Account for BingxConnector {
async fn get_balance(&self, query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
let _asset = query.asset.clone();
let account_type = query.account_type;
let endpoint = match account_type {
AccountType::Spot | AccountType::Margin => BingxEndpoint::SpotBalance,
_ => BingxEndpoint::SwapBalance,
};
let params = HashMap::new();
let response = self.get(endpoint, params, account_type).await?;
match account_type {
AccountType::Spot | AccountType::Margin => BingxParser::parse_balances(&response),
_ => BingxParser::parse_swap_balance(&response),
}
}
async fn get_account_info(&self, account_type: AccountType) -> ExchangeResult<AccountInfo> {
let balances = self.get_balance(BalanceQuery { asset: None, account_type }).await?;
Ok(AccountInfo {
account_type,
can_trade: true,
can_withdraw: true,
can_deposit: true,
maker_commission: 0.1, taker_commission: 0.1,
balances,
})
}
async fn get_fees(&self, _symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
let params = HashMap::new();
let response = self.get(BingxEndpoint::SpotCommissionRate, params, AccountType::Spot).await;
if let Ok(response) = response {
self.check_response(&response)?;
if let Some(data) = response.get("data") {
let maker = data.get("makerCommissionRate")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| data.get("maker").and_then(|v| v.as_f64()))
.unwrap_or(0.001);
let taker = data.get("takerCommissionRate")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| data.get("taker").and_then(|v| v.as_f64()))
.unwrap_or(0.001);
return Ok(FeeInfo {
maker_rate: maker,
taker_rate: taker,
symbol: _symbol.map(String::from),
tier: None,
});
}
}
Ok(FeeInfo {
maker_rate: 0.001,
taker_rate: 0.001,
symbol: _symbol.map(String::from),
tier: None,
})
}
fn account_capabilities(&self, _account_type: AccountType) -> AccountCapabilities {
AccountCapabilities {
has_balances: true,
has_account_info: true,
has_fees: true,
has_transfers: true,
has_sub_accounts: true,
has_deposit_withdraw: true,
has_margin: false,
has_earn_staking: false,
has_funding_history: false,
has_ledger: false,
has_convert: false,
has_positions: true,
}
}
}
#[async_trait]
impl Positions for BingxConnector {
async fn get_positions(&self, query: PositionQuery) -> ExchangeResult<Vec<Position>> {
let symbol = query.symbol.clone();
let account_type = query.account_type;
match account_type {
AccountType::Spot | AccountType::Margin => {
return Err(ExchangeError::UnsupportedOperation(
"Positions not supported for Spot/Margin".to_string()
));
}
_ => {}
}
let mut params = HashMap::new();
if let Some(s) = symbol {
params.insert("symbol".to_string(), format_symbol(&s.base, &s.quote, account_type));
}
let response = self.get(BingxEndpoint::SwapPositions, params, account_type).await?;
BingxParser::parse_positions(&response)
}
async fn get_funding_rate(
&self,
symbol: &str,
account_type: AccountType,
) -> ExchangeResult<FundingRate> {
match account_type {
AccountType::Spot | AccountType::Margin => {
return Err(ExchangeError::UnsupportedOperation(
"Funding rate not supported for Spot/Margin".to_string()
));
}
_ => {}
}
let symbol_str = symbol;
let sym = {
let parts: Vec<&str> = symbol_str.split('/').collect();
if parts.len() == 2 {
crate::core::Symbol::new(parts[0], parts[1])
} else {
crate::core::Symbol { base: symbol_str.to_string(), quote: String::new(), raw: Some(symbol_str.to_string()) }
}
};
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&sym.base, &sym.quote, account_type));
let response = self.get(BingxEndpoint::SwapFundingRate, params, account_type).await?;
let data = response.get("data").cloned().unwrap_or(json!({}));
let rate = data.get("fundingRate")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| data.get("fundingRate").and_then(|v| v.as_f64()))
.unwrap_or(0.0);
let next_time = data.get("nextFundingTime")
.and_then(|v| v.as_i64());
Ok(FundingRate {
symbol: symbol_str.to_string(),
rate,
next_funding_time: next_time,
timestamp: crate::core::timestamp_millis() as i64,
})
}
async fn modify_position(&self, req: PositionModification) -> ExchangeResult<()> {
match req {
PositionModification::SetLeverage { ref symbol, leverage, account_type } => {
let symbol = symbol.clone();
match account_type {
AccountType::Spot | AccountType::Margin => {
return Err(ExchangeError::UnsupportedOperation(
"Leverage not supported for Spot/Margin".to_string()
));
}
_ => {}
}
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&symbol.base, &symbol.quote, account_type));
params.insert("side".to_string(), "LONG".to_string()); params.insert("leverage".to_string(), leverage.to_string());
let response = self.post(BingxEndpoint::SwapLeverage, params, account_type).await?;
self.check_response(&response)?;
Ok(())
}
PositionModification::SetMarginMode { ref symbol, margin_type, account_type } => {
let symbol = symbol.clone();
match account_type {
AccountType::Spot | AccountType::Margin => {
return Err(ExchangeError::UnsupportedOperation(
"MarginMode not supported for Spot/Margin".to_string()
));
}
_ => {}
}
let margin_type_str = match margin_type {
MarginType::Cross => "CROSSED",
MarginType::Isolated => "ISOLATED",
};
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&symbol.base, &symbol.quote, account_type));
params.insert("marginType".to_string(), margin_type_str.to_string());
let response = self.post(BingxEndpoint::SwapMarginType, params, account_type).await?;
self.check_response(&response)?;
Ok(())
}
PositionModification::ClosePosition { ref symbol, account_type } => {
let symbol = symbol.clone();
match account_type {
AccountType::Spot | AccountType::Margin => {
return Err(ExchangeError::UnsupportedOperation(
"ClosePosition not supported for Spot/Margin".to_string()
));
}
_ => {}
}
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&symbol.base, &symbol.quote, account_type));
params.insert("side".to_string(), "SELL".to_string()); params.insert("type".to_string(), "MARKET".to_string());
params.insert("closePosition".to_string(), "true".to_string());
let response = self.post(BingxEndpoint::SwapOrder, params, account_type).await?;
self.check_response(&response)?;
Ok(())
}
_ => Err(ExchangeError::UnsupportedOperation(
format!("{:?} not supported on BingX", req)
)),
}
}
}
#[async_trait]
impl CancelAll for BingxConnector {
async fn cancel_all_orders(
&self,
scope: CancelScope,
account_type: AccountType,
) -> ExchangeResult<CancelAllResponse> {
let is_futures = matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated);
match scope {
CancelScope::All { ref symbol } => {
if is_futures {
let sym = symbol.as_ref()
.ok_or_else(|| ExchangeError::InvalidRequest(
"BingX Swap cancel-all requires a symbol".to_string()
))?;
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&sym.base, &sym.quote, account_type));
let response = self.delete(BingxEndpoint::SwapCancelAllOrders, params, account_type).await?;
self.check_response(&response)?;
Ok(CancelAllResponse {
cancelled_count: 0, failed_count: 0,
details: vec![],
})
} else {
let sym = symbol.as_ref()
.ok_or_else(|| ExchangeError::InvalidRequest(
"BingX Spot cancel-all requires a symbol".to_string()
))?;
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(&sym.base, &sym.quote, account_type));
let response = self.delete(BingxEndpoint::SpotCancelAllOrders, params, account_type).await?;
self.check_response(&response)?;
Ok(CancelAllResponse {
cancelled_count: 0,
failed_count: 0,
details: vec![],
})
}
}
CancelScope::BySymbol { ref symbol } => {
let formatted_symbol = format_symbol(&symbol.base, &symbol.quote, account_type);
let mut params = HashMap::new();
params.insert("symbol".to_string(), formatted_symbol);
let endpoint = if is_futures {
BingxEndpoint::SwapCancelAllOrders
} else {
BingxEndpoint::SpotCancelAllOrders
};
let response = self.delete(endpoint, params, account_type).await?;
self.check_response(&response)?;
Ok(CancelAllResponse {
cancelled_count: 0,
failed_count: 0,
details: vec![],
})
}
_ => Err(ExchangeError::UnsupportedOperation(
"cancel_all_orders only supports All and BySymbol scopes".to_string()
)),
}
}
}
#[async_trait]
impl BatchOrders for BingxConnector {
async fn place_orders_batch(
&self,
orders: Vec<OrderRequest>,
) -> ExchangeResult<Vec<OrderResult>> {
if orders.is_empty() {
return Ok(vec![]);
}
let account_type = orders.first().map(|o| o.account_type).unwrap_or(AccountType::FuturesCross);
let is_futures = matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated);
if !is_futures {
return Err(ExchangeError::UnsupportedOperation(
"BingX batch orders only supported for Swap (futures)".to_string()
));
}
let batch_orders: Vec<Value> = orders.iter().map(|req| {
let side_str = match req.side { OrderSide::Buy => "BUY", OrderSide::Sell => "SELL" };
let formatted_symbol = format_symbol(&req.symbol.base, &req.symbol.quote, req.account_type);
let mut obj = json!({
"symbol": formatted_symbol,
"side": side_str,
"quantity": req.quantity.to_string(),
});
match &req.order_type {
OrderType::Market => {
obj["type"] = json!("MARKET");
}
OrderType::Limit { price } => {
obj["type"] = json!("LIMIT");
obj["price"] = json!(price.to_string());
obj["timeInForce"] = json!("GTC");
}
OrderType::PostOnly { price } => {
obj["type"] = json!("LIMIT");
obj["price"] = json!(price.to_string());
obj["timeInForce"] = json!("PostOnly");
}
_ => {
obj["type"] = json!("MARKET");
}
}
obj
}).collect();
let batch_orders_str = serde_json::to_string(&batch_orders)
.map_err(|e| ExchangeError::Parse(format!("Failed to serialize batch orders: {}", e)))?;
let mut params = HashMap::new();
params.insert("batchOrders".to_string(), batch_orders_str);
let auth = self.auth.as_ref()
.ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
let headers = auth.sign_request(&mut params);
let base_url = self.urls.rest_url(account_type);
let path = BingxEndpoint::SwapBatchOrders.path();
let query = params.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
let url = format!("{}{}?{}", base_url, path, query);
self.rate_limit_wait(1, true).await;
let response = self.http.post(&url, &json!({}), &headers).await?;
self.check_response(&response)?;
let data = response.get("data").cloned().unwrap_or(json!([]));
let results = if let Some(arr) = data.as_array() {
arr.iter().enumerate().map(|(i, item)| {
let order_id = item.get("orderId")
.and_then(|v| v.as_str().map(String::from)
.or_else(|| v.as_i64().map(|n| n.to_string())));
let code = item.get("code").and_then(|v| v.as_i64()).unwrap_or(0);
let success = code == 0 && order_id.is_some();
let req = orders.get(i);
OrderResult {
order: order_id.map(|id| Order {
id,
client_order_id: None,
symbol: req.map(|o| o.symbol.to_string()).unwrap_or_default(),
side: req.map(|o| o.side).unwrap_or(OrderSide::Buy),
order_type: req.map(|o| o.order_type.clone()).unwrap_or(OrderType::Market),
status: crate::core::OrderStatus::New,
price: None,
stop_price: None,
quantity: req.map(|o| o.quantity).unwrap_or(0.0),
filled_quantity: 0.0,
average_price: None,
commission: None,
commission_asset: None,
created_at: crate::core::timestamp_millis() as i64,
updated_at: None,
time_in_force: TimeInForce::Gtc,
}),
client_order_id: None,
success,
error: if success { None } else {
item.get("msg").and_then(|v| v.as_str()).map(String::from)
},
error_code: if success { None } else { Some(code as i32) },
}
}).collect()
} else {
orders.iter().map(|_| OrderResult {
order: None,
client_order_id: None,
success: false,
error: Some("Unexpected response format".to_string()),
error_code: None,
}).collect()
};
Ok(results)
}
async fn cancel_orders_batch(
&self,
order_ids: Vec<String>,
symbol: Option<&str>,
account_type: AccountType,
) -> ExchangeResult<Vec<OrderResult>> {
if order_ids.is_empty() {
return Ok(vec![]);
}
let is_futures = matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated);
if !is_futures {
return Err(ExchangeError::UnsupportedOperation(
"BingX batch cancel only supported for Swap (futures)".to_string()
));
}
let sym = symbol.ok_or_else(|| ExchangeError::InvalidRequest(
"Symbol required for BingX batch cancel".to_string()
))?;
let ids_str = serde_json::to_string(&order_ids)
.map_err(|e| ExchangeError::Parse(format!("Failed to serialize order IDs: {}", e)))?;
let mut params = HashMap::new();
params.insert("symbol".to_string(), sym.to_string());
params.insert("orderIdList".to_string(), ids_str);
let response = self.delete(BingxEndpoint::SwapBatchCancelOrders, params, account_type).await?;
self.check_response(&response)?;
let data = response.get("data").cloned().unwrap_or(json!([]));
let results = if let Some(arr) = data.as_array() {
arr.iter().map(|item| {
let success = item.get("code").and_then(|v| v.as_i64()).unwrap_or(0) == 0;
OrderResult {
order: None,
client_order_id: None,
success,
error: if success { None } else {
item.get("msg").and_then(|v| v.as_str()).map(String::from)
},
error_code: None,
}
}).collect()
} else {
order_ids.iter().map(|_| OrderResult {
order: None,
client_order_id: None,
success: true,
error: None,
error_code: None,
}).collect()
};
Ok(results)
}
fn max_batch_place_size(&self) -> usize {
5
}
fn max_batch_cancel_size(&self) -> usize {
20
}
}
#[async_trait]
impl AmendOrder for BingxConnector {
async fn amend_order(&self, req: AmendRequest) -> ExchangeResult<Order> {
let symbol = &req.symbol;
let account_type = req.account_type;
let is_futures = matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated);
if !is_futures {
return Err(ExchangeError::UnsupportedOperation(
"AmendOrder is only supported for BingX Swap (futures). Spot requires cancel+replace.".to_string()
));
}
let symbol_str = format_symbol(&symbol.base, &symbol.quote, account_type);
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol_str.clone());
params.insert("orderId".to_string(), req.order_id.clone());
if let Some(new_price) = req.fields.price {
params.insert("price".to_string(), self.precision.price(&symbol_str, new_price));
}
if let Some(new_qty) = req.fields.quantity {
params.insert("quantity".to_string(), self.precision.qty(&symbol_str, new_qty));
}
if let Some(trigger) = req.fields.trigger_price {
params.insert("stopPrice".to_string(), self.precision.price(&symbol_str, trigger));
}
let response = self.post(BingxEndpoint::SwapAmend, params, account_type).await?;
self.check_response(&response)?;
self.get_order(&symbol.to_string(), &req.order_id, account_type).await
}
}
#[async_trait]
impl AccountTransfers for BingxConnector {
async fn transfer(&self, req: TransferRequest) -> ExchangeResult<TransferResponse> {
use crate::core::AccountType;
let transfer_side = match (&req.from_account, &req.to_account) {
(AccountType::Spot, AccountType::FuturesCross)
| (AccountType::Spot, AccountType::FuturesIsolated) => "STANDARD_TO_FUND",
(AccountType::FuturesCross, AccountType::Spot)
| (AccountType::FuturesIsolated, AccountType::Spot) => "FUND_TO_STANDARD",
_ => "STANDARD_TO_FUND",
};
let mut params = HashMap::new();
params.insert("asset".to_string(), req.asset.clone());
params.insert("amount".to_string(), req.amount.to_string());
params.insert("transferSide".to_string(), transfer_side.to_string());
let response = self.post(BingxEndpoint::InnerTransfer, params, AccountType::Spot).await?;
self.check_response(&response)?;
let data = response.get("data").cloned().unwrap_or(serde_json::json!({}));
let transfer_id = data.get("tranId")
.and_then(|v| v.as_str().map(String::from)
.or_else(|| v.as_i64().map(|n| n.to_string())))
.unwrap_or_else(|| "unknown".to_string());
Ok(TransferResponse {
transfer_id,
status: "Successful".to_string(),
asset: req.asset,
amount: req.amount,
timestamp: None,
})
}
async fn get_transfer_history(
&self,
filter: TransferHistoryFilter,
) -> ExchangeResult<Vec<TransferResponse>> {
use crate::core::AccountType;
let mut params = HashMap::new();
if let Some(start) = filter.start_time {
params.insert("startTime".to_string(), start.to_string());
}
if let Some(end) = filter.end_time {
params.insert("endTime".to_string(), end.to_string());
}
if let Some(limit) = filter.limit {
params.insert("limit".to_string(), limit.to_string());
}
let response = self.get(BingxEndpoint::TransferHistory, params, AccountType::Spot).await?;
let data = BingxParser::extract_data(&response)?;
let records = data.as_array()
.unwrap_or(&vec![])
.iter()
.map(|item| {
let transfer_id = item.get("tranId")
.and_then(|v| v.as_str().map(String::from)
.or_else(|| v.as_i64().map(|n| n.to_string())))
.unwrap_or_default();
let asset = item.get("asset")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let amount = BingxParser::get_f64(item, "amount").unwrap_or(0.0);
let status = item.get("status")
.and_then(|v| v.as_str())
.unwrap_or("Unknown")
.to_string();
let timestamp = item.get("timestamp").and_then(|v| v.as_i64());
TransferResponse {
transfer_id,
status,
asset,
amount,
timestamp,
}
})
.collect();
Ok(records)
}
}
#[async_trait]
impl CustodialFunds for BingxConnector {
async fn get_deposit_address(
&self,
asset: &str,
network: Option<&str>,
) -> ExchangeResult<DepositAddress> {
use crate::core::AccountType;
let mut params = HashMap::new();
params.insert("coin".to_string(), asset.to_string());
if let Some(net) = network {
params.insert("network".to_string(), net.to_string());
}
let response = self.get(BingxEndpoint::DepositAddress, params, AccountType::Spot).await?;
let data = BingxParser::extract_data(&response)?;
let address = data.get("address")
.and_then(|v| v.as_str())
.ok_or_else(|| ExchangeError::Parse("Missing 'address' field".to_string()))?
.to_string();
let tag = data.get("tag")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from);
let net = data.get("network")
.and_then(|v| v.as_str())
.map(String::from)
.or_else(|| network.map(String::from));
Ok(DepositAddress {
address,
tag,
network: net,
asset: asset.to_string(),
created_at: None,
})
}
async fn withdraw(&self, req: WithdrawRequest) -> ExchangeResult<WithdrawResponse> {
use crate::core::AccountType;
let mut params = HashMap::new();
params.insert("coin".to_string(), req.asset.clone());
params.insert("address".to_string(), req.address.clone());
params.insert("amount".to_string(), req.amount.to_string());
params.insert("walletType".to_string(), "0".to_string());
if let Some(net) = &req.network {
params.insert("network".to_string(), net.clone());
}
if let Some(tag) = &req.tag {
params.insert("addressTag".to_string(), tag.clone());
}
let response = self.post(BingxEndpoint::Withdraw, params, AccountType::Spot).await?;
self.check_response(&response)?;
let data = response.get("data").cloned().unwrap_or(serde_json::json!({}));
let withdraw_id = data.get("id")
.and_then(|v| v.as_str().map(String::from)
.or_else(|| v.as_i64().map(|n| n.to_string())))
.unwrap_or_else(|| "unknown".to_string());
Ok(WithdrawResponse {
withdraw_id,
status: "Pending".to_string(),
tx_hash: None,
})
}
async fn get_funds_history(
&self,
filter: FundsHistoryFilter,
) -> ExchangeResult<Vec<FundsRecord>> {
use crate::core::AccountType;
let mut records: Vec<FundsRecord> = Vec::new();
let build_params = |f: &FundsHistoryFilter| {
let mut p = HashMap::new();
if let Some(a) = &f.asset {
p.insert("coin".to_string(), a.clone());
}
if let Some(s) = f.start_time {
p.insert("startTime".to_string(), s.to_string());
}
if let Some(e) = f.end_time {
p.insert("endTime".to_string(), e.to_string());
}
if let Some(l) = f.limit {
p.insert("limit".to_string(), l.to_string());
}
p
};
if matches!(filter.record_type, FundsRecordType::Deposit | FundsRecordType::Both) {
let params = build_params(&filter);
let response = self.get(BingxEndpoint::DepositHistory, params, AccountType::Spot).await?;
if let Ok(data) = BingxParser::extract_data(&response) {
if let Some(arr) = data.as_array() {
for item in arr {
let id = item.get("id")
.and_then(|v| v.as_str().map(String::from)
.or_else(|| v.as_i64().map(|n| n.to_string())))
.unwrap_or_default();
let asset = item.get("coin").and_then(|v| v.as_str()).unwrap_or("").to_string();
let amount = BingxParser::get_f64(item, "amount").unwrap_or(0.0);
let tx_hash = item.get("txId").and_then(|v| v.as_str()).map(String::from);
let network = item.get("network").and_then(|v| v.as_str()).map(String::from);
let status = item.get("status").and_then(|v| v.as_str()).unwrap_or("Unknown").to_string();
let timestamp = item.get("insertTime").and_then(|v| v.as_i64()).unwrap_or(0);
records.push(FundsRecord::Deposit {
id,
asset,
amount,
tx_hash,
network,
status,
timestamp,
});
}
}
}
}
if matches!(filter.record_type, FundsRecordType::Withdrawal | FundsRecordType::Both) {
let params = build_params(&filter);
let response = self.get(BingxEndpoint::WithdrawHistory, params, AccountType::Spot).await?;
if let Ok(data) = BingxParser::extract_data(&response) {
if let Some(arr) = data.as_array() {
for item in arr {
let id = item.get("id")
.and_then(|v| v.as_str().map(String::from)
.or_else(|| v.as_i64().map(|n| n.to_string())))
.unwrap_or_default();
let asset = item.get("coin").and_then(|v| v.as_str()).unwrap_or("").to_string();
let amount = BingxParser::get_f64(item, "amount").unwrap_or(0.0);
let fee = BingxParser::get_f64(item, "transactionFee");
let address = item.get("address").and_then(|v| v.as_str()).unwrap_or("").to_string();
let tag = item.get("addressTag").and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from);
let tx_hash = item.get("txId").and_then(|v| v.as_str()).map(String::from);
let network = item.get("network").and_then(|v| v.as_str()).map(String::from);
let status = item.get("status").and_then(|v| v.as_str()).unwrap_or("Unknown").to_string();
let timestamp = item.get("applyTime").and_then(|v| v.as_i64()).unwrap_or(0);
records.push(FundsRecord::Withdrawal {
id,
asset,
amount,
fee,
address,
tag,
tx_hash,
network,
status,
timestamp,
});
}
}
}
}
Ok(records)
}
}
#[async_trait]
impl SubAccounts for BingxConnector {
async fn sub_account_operation(
&self,
op: SubAccountOperation,
) -> ExchangeResult<SubAccountResult> {
use crate::core::AccountType;
match op {
SubAccountOperation::Create { label } => {
let mut params = HashMap::new();
params.insert("subAccountString".to_string(), label.clone());
let response = self.post(BingxEndpoint::SubAccountCreate, params, AccountType::Spot).await?;
self.check_response(&response)?;
let data = response.get("data").cloned().unwrap_or(serde_json::json!({}));
let id = data.get("subUid")
.and_then(|v| v.as_str().map(String::from)
.or_else(|| v.as_i64().map(|n| n.to_string())));
Ok(SubAccountResult {
id,
name: Some(label),
accounts: vec![],
transaction_id: None,
})
}
SubAccountOperation::List => {
let response = self.get(BingxEndpoint::SubAccountList, HashMap::new(), AccountType::Spot).await?;
let data = BingxParser::extract_data(&response)?;
let accounts = data.as_array()
.unwrap_or(&vec![])
.iter()
.map(|item| {
let id = item.get("subUid")
.and_then(|v| v.as_str().map(String::from)
.or_else(|| v.as_i64().map(|n| n.to_string())))
.unwrap_or_default();
let name = item.get("note")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let status = item.get("status")
.and_then(|v| v.as_str())
.unwrap_or("Normal")
.to_string();
SubAccount { id, name, status }
})
.collect();
Ok(SubAccountResult {
id: None,
name: None,
accounts,
transaction_id: None,
})
}
SubAccountOperation::Transfer { sub_account_id, asset, amount, to_sub } => {
let transfer_type = if to_sub { "1" } else { "2" };
let mut params = HashMap::new();
params.insert("subUid".to_string(), sub_account_id);
params.insert("coin".to_string(), asset.clone());
params.insert("amount".to_string(), amount.to_string());
params.insert("type".to_string(), transfer_type.to_string());
let response = self.post(BingxEndpoint::SubAccountTransfer, params, AccountType::Spot).await?;
self.check_response(&response)?;
let data = response.get("data").cloned().unwrap_or(serde_json::json!({}));
let transaction_id = data.get("tranId")
.and_then(|v| v.as_str().map(String::from)
.or_else(|| v.as_i64().map(|n| n.to_string())));
Ok(SubAccountResult {
id: None,
name: None,
accounts: vec![],
transaction_id,
})
}
SubAccountOperation::GetBalance { sub_account_id } => {
let mut params = HashMap::new();
params.insert("subUid".to_string(), sub_account_id.clone());
let response = self.get(BingxEndpoint::SubAccountAssets, params, AccountType::Spot).await?;
self.check_response(&response)?;
Ok(SubAccountResult {
id: Some(sub_account_id),
name: None,
accounts: vec![],
transaction_id: None,
})
}
}
}
}
impl BingxConnector {
pub async fn spot_my_trades(
&self,
symbol: Option<&str>,
order_id: Option<&str>,
start_time: Option<i64>,
end_time: Option<i64>,
limit: Option<u32>,
) -> ExchangeResult<Value> {
let mut params = HashMap::new();
if let Some(s) = symbol {
params.insert("symbol".to_string(), s.to_string());
}
if let Some(oid) = order_id {
params.insert("orderId".to_string(), oid.to_string());
}
if let Some(st) = start_time {
params.insert("startTime".to_string(), st.to_string());
}
if let Some(et) = end_time {
params.insert("endTime".to_string(), et.to_string());
}
if let Some(l) = limit {
params.insert("limit".to_string(), l.min(500).to_string());
}
let response = self.get(BingxEndpoint::SpotMyTrades, params, AccountType::Spot).await?;
self.check_response(&response)?;
Ok(response)
}
pub async fn swap_all_fill_orders(
&self,
symbol: Option<&str>,
order_id: Option<&str>,
start_time: Option<i64>,
end_time: Option<i64>,
limit: Option<u32>,
) -> ExchangeResult<Value> {
let mut params = HashMap::new();
if let Some(s) = symbol {
params.insert("symbol".to_string(), s.to_string());
}
if let Some(oid) = order_id {
params.insert("orderId".to_string(), oid.to_string());
}
if let Some(st) = start_time {
params.insert("startTime".to_string(), st.to_string());
}
if let Some(et) = end_time {
params.insert("endTime".to_string(), et.to_string());
}
if let Some(l) = limit {
params.insert("pageSize".to_string(), l.min(500).to_string());
}
let response = self.get(BingxEndpoint::SwapAllFillOrders, params, AccountType::FuturesCross).await?;
self.check_response(&response)?;
Ok(response)
}
pub async fn swap_open_interest(&self, symbol: &str) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.to_string());
let response = self.get(BingxEndpoint::SwapOpenInterest, params, AccountType::FuturesCross).await?;
self.check_response(&response)?;
Ok(response)
}
pub async fn swap_funding_rate_history(
&self,
symbol: &str,
start_time: Option<i64>,
end_time: Option<i64>,
limit: Option<u32>,
) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.to_string());
if let Some(st) = start_time {
params.insert("startTime".to_string(), st.to_string());
}
if let Some(et) = end_time {
params.insert("endTime".to_string(), et.to_string());
}
if let Some(l) = limit {
params.insert("limit".to_string(), l.min(1000).to_string());
}
let response = self.get(BingxEndpoint::SwapFundingRateHistory, params, AccountType::FuturesCross).await?;
self.check_response(&response)?;
Ok(response)
}
pub async fn swap_premium_index(&self, symbol: Option<&str>) -> ExchangeResult<Value> {
let mut params = HashMap::new();
if let Some(s) = symbol {
params.insert("symbol".to_string(), s.to_string());
}
let response = self.get(BingxEndpoint::SwapPremiumIndex, params, AccountType::FuturesCross).await?;
self.check_response(&response)?;
Ok(response)
}
}