use serde_json::Value;
use crate::core::types::{
ExchangeError, ExchangeResult,
Kline, OrderBook, OrderBookLevel, Ticker, Order, Balance, Position,
OrderSide, OrderType, OrderStatus, PositionSide, SymbolInfo,
UserTrade,
OrderbookDelta as OrderbookDeltaData,
};
pub struct BingxParser;
impl BingxParser {
pub fn extract_data(response: &Value) -> ExchangeResult<&Value> {
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(),
});
}
response.get("data")
.ok_or_else(|| ExchangeError::Parse("Missing 'data' field".to_string()))
}
fn parse_f64(value: &Value) -> Option<f64> {
value.as_str()
.and_then(|s| s.parse().ok())
.or_else(|| value.as_f64())
}
pub fn get_f64(data: &Value, key: &str) -> Option<f64> {
data.get(key).and_then(Self::parse_f64)
}
fn require_f64(data: &Value, key: &str) -> ExchangeResult<f64> {
Self::get_f64(data, key)
.ok_or_else(|| ExchangeError::Parse(format!("Missing or invalid '{}'", key)))
}
fn get_str<'a>(data: &'a Value, key: &str) -> Option<&'a str> {
data.get(key).and_then(|v| v.as_str())
}
fn require_str<'a>(data: &'a Value, key: &str) -> ExchangeResult<&'a str> {
Self::get_str(data, key)
.ok_or_else(|| ExchangeError::Parse(format!("Missing '{}'", key)))
}
pub fn parse_price(response: &Value) -> ExchangeResult<f64> {
let data = Self::extract_data(response)?;
Self::get_f64(data, "price")
.or_else(|| Self::get_f64(data, "lastPrice"))
.ok_or_else(|| ExchangeError::Parse("Missing or invalid 'price'".to_string()))
}
pub fn parse_klines(response: &Value) -> ExchangeResult<Vec<Kline>> {
let data = Self::extract_data(response)?;
let arr = data.as_array()
.ok_or_else(|| ExchangeError::Parse("'data' is not an array".to_string()))?;
let mut klines = Vec::with_capacity(arr.len());
for item in arr {
if let Some(kline_arr) = item.as_array() {
if kline_arr.len() >= 6 {
let open_time = kline_arr[0].as_i64().unwrap_or(0);
klines.push(Kline {
open_time,
open: Self::parse_f64(&kline_arr[1]).unwrap_or(0.0),
high: Self::parse_f64(&kline_arr[2]).unwrap_or(0.0),
low: Self::parse_f64(&kline_arr[3]).unwrap_or(0.0),
close: Self::parse_f64(&kline_arr[4]).unwrap_or(0.0),
volume: Self::parse_f64(&kline_arr[5]).unwrap_or(0.0),
quote_volume: None,
close_time: None,
trades: None,
});
}
} else {
let open_time = item.get("time")
.or_else(|| item.get("openTime"))
.and_then(|t| t.as_i64())
.unwrap_or(0);
klines.push(Kline {
open_time,
open: Self::get_f64(item, "open").unwrap_or(0.0),
close: Self::get_f64(item, "close").unwrap_or(0.0),
high: Self::get_f64(item, "high").unwrap_or(0.0),
low: Self::get_f64(item, "low").unwrap_or(0.0),
volume: Self::get_f64(item, "volume").unwrap_or(0.0),
quote_volume: None,
close_time: None,
trades: None,
});
}
}
klines.reverse();
Ok(klines)
}
pub fn parse_orderbook(response: &Value) -> ExchangeResult<OrderBook> {
let data = Self::extract_data(response)?;
let parse_levels = |key: &str| -> Vec<OrderBookLevel> {
data.get(key)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|level| {
let pair = level.as_array()?;
if pair.len() < 2 { return None; }
let price = Self::parse_f64(&pair[0])?;
let size = Self::parse_f64(&pair[1])?;
Some(OrderBookLevel::new(price, size))
})
.collect()
})
.unwrap_or_default()
};
let mut bids = parse_levels("bids");
let mut asks = parse_levels("asks");
bids.sort_by(|a, b| b.price.partial_cmp(&a.price).unwrap_or(std::cmp::Ordering::Equal));
asks.sort_by(|a, b| a.price.partial_cmp(&b.price).unwrap_or(std::cmp::Ordering::Equal));
Ok(OrderBook {
timestamp: crate::core::timestamp_millis() as i64,
bids,
asks,
sequence: None,
last_update_id: None,
first_update_id: None,
prev_update_id: None,
event_time: None,
transaction_time: None,
checksum: None,
})
}
pub fn parse_ticker(response: &Value) -> ExchangeResult<Ticker> {
let data = Self::extract_data(response)?;
let ticker_data = if data.is_array() {
data.as_array()
.and_then(|arr| arr.first())
.ok_or_else(|| ExchangeError::Parse("Empty ticker array".to_string()))?
} else {
data
};
let bid_price = Self::get_f64(ticker_data, "bidPrice");
let ask_price = Self::get_f64(ticker_data, "askPrice");
let last_price = Self::get_f64(ticker_data, "lastPrice")
.or_else(|| Self::get_f64(ticker_data, "price"))
.or_else(|| {
match (bid_price, ask_price) {
(Some(bid), Some(ask)) => Some((bid + ask) / 2.0),
_ => None,
}
})
.unwrap_or(0.0);
Ok(Ticker {
symbol: Self::get_str(ticker_data, "symbol").unwrap_or("").to_string(),
last_price,
bid_price,
ask_price,
high_24h: Self::get_f64(ticker_data, "highPrice"),
low_24h: Self::get_f64(ticker_data, "lowPrice"),
volume_24h: Self::get_f64(ticker_data, "volume"),
quote_volume_24h: Self::get_f64(ticker_data, "quoteVolume"),
price_change_24h: Self::get_f64(ticker_data, "priceChange"),
price_change_percent_24h: Self::get_f64(ticker_data, "priceChangePercent"),
timestamp: ticker_data.get("closeTime")
.or_else(|| ticker_data.get("time"))
.and_then(|t| t.as_i64())
.unwrap_or(crate::core::timestamp_millis() as i64),
})
}
pub fn parse_order(response: &Value, symbol: &str) -> ExchangeResult<Order> {
let data = Self::extract_data(response)?;
let order_data = data.get("order").unwrap_or(data);
Self::parse_order_data(order_data, symbol)
}
pub fn parse_order_data(data: &Value, symbol: &str) -> ExchangeResult<Order> {
let side = match Self::get_str(data, "side").unwrap_or("BUY").to_uppercase().as_str() {
"SELL" => OrderSide::Sell,
_ => OrderSide::Buy,
};
let order_type = match Self::get_str(data, "type").unwrap_or("LIMIT").to_uppercase().as_str() {
"MARKET" => OrderType::Market,
_ => OrderType::Limit { price: 0.0 },
};
let status = Self::parse_order_status(data);
let id = data.get("orderId")
.and_then(|v| v.as_i64().map(|i| i.to_string()))
.or_else(|| Self::get_str(data, "orderId").map(String::from))
.unwrap_or_default();
Ok(Order {
id,
client_order_id: Self::get_str(data, "clientOrderId").map(String::from),
symbol: Self::get_str(data, "symbol").unwrap_or(symbol).to_string(),
side,
order_type,
status,
price: Self::get_f64(data, "price"),
stop_price: Self::get_f64(data, "stopPrice"),
quantity: Self::get_f64(data, "origQty")
.or_else(|| Self::get_f64(data, "quantity"))
.unwrap_or(0.0),
filled_quantity: Self::get_f64(data, "executedQty")
.or_else(|| Self::get_f64(data, "cumExecQty"))
.unwrap_or(0.0),
average_price: Self::get_f64(data, "avgPrice"),
commission: Self::get_f64(data, "commission"),
commission_asset: Self::get_str(data, "commissionAsset").map(String::from),
created_at: data.get("time")
.or_else(|| data.get("transactTime"))
.and_then(|t| t.as_i64())
.unwrap_or(0),
updated_at: data.get("updateTime").and_then(|t| t.as_i64()),
time_in_force: crate::core::TimeInForce::Gtc,
})
}
fn parse_order_status(data: &Value) -> OrderStatus {
match Self::get_str(data, "status").unwrap_or("NEW").to_uppercase().as_str() {
"FILLED" => OrderStatus::Filled,
"PARTIALLY_FILLED" => OrderStatus::PartiallyFilled,
"CANCELED" | "CANCELLED" => OrderStatus::Canceled,
"REJECTED" => OrderStatus::Rejected,
"EXPIRED" => OrderStatus::Expired,
_ => OrderStatus::New,
}
}
pub fn parse_orders(response: &Value) -> ExchangeResult<Vec<Order>> {
let data = Self::extract_data(response)?;
let items = data.get("orders")
.and_then(|v| v.as_array())
.or_else(|| data.as_array())
.ok_or_else(|| ExchangeError::Parse("Expected array of orders".to_string()))?;
items.iter()
.map(|item| Self::parse_order_data(item, ""))
.collect()
}
pub fn parse_order_id(response: &Value) -> ExchangeResult<String> {
let data = Self::extract_data(response)?;
data.get("orderId")
.and_then(|v| v.as_i64().map(|i| i.to_string()))
.or_else(|| Self::get_str(data, "orderId").map(String::from))
.ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))
}
pub fn parse_balances(response: &Value) -> ExchangeResult<Vec<Balance>> {
let data = Self::extract_data(response)?;
let balances_array = data.get("balances")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'balances' array".to_string()))?;
let mut balances = Vec::new();
for item in balances_array {
let asset = Self::get_str(item, "asset").unwrap_or("").to_string();
if asset.is_empty() { continue; }
let free = Self::get_f64(item, "free").unwrap_or(0.0);
let locked = Self::get_f64(item, "locked").unwrap_or(0.0);
balances.push(Balance {
asset,
free,
locked,
total: free + locked,
});
}
Ok(balances)
}
pub fn parse_swap_balance(response: &Value) -> ExchangeResult<Vec<Balance>> {
let data = Self::extract_data(response)?;
let balance_obj = data.get("balance")
.ok_or_else(|| ExchangeError::Parse("Missing 'balance' object".to_string()))?;
let asset = Self::get_str(balance_obj, "asset").unwrap_or("USDT").to_string();
let total_balance = Self::get_f64(balance_obj, "balance").unwrap_or(0.0);
let available = Self::get_f64(balance_obj, "availableMargin").unwrap_or(0.0);
let used = Self::get_f64(balance_obj, "usedMargin").unwrap_or(0.0);
Ok(vec![Balance {
asset,
free: available,
locked: used,
total: total_balance,
}])
}
pub fn parse_positions(response: &Value) -> ExchangeResult<Vec<Position>> {
let data = Self::extract_data(response)?;
let arr = data.as_array()
.ok_or_else(|| ExchangeError::Parse("Expected array of positions".to_string()))?;
let mut positions = Vec::new();
for item in arr {
if let Some(pos) = Self::parse_position_data(item) {
positions.push(pos);
}
}
Ok(positions)
}
pub fn parse_position(response: &Value) -> ExchangeResult<Position> {
let data = Self::extract_data(response)?;
Self::parse_position_data(data)
.ok_or_else(|| ExchangeError::Parse("Invalid position data".to_string()))
}
fn parse_position_data(data: &Value) -> Option<Position> {
let symbol = Self::get_str(data, "symbol")?.to_string();
let position_amt = Self::get_f64(data, "positionAmt")
.or_else(|| Self::get_f64(data, "positionAmount"))
.unwrap_or(0.0);
if position_amt.abs() < f64::EPSILON {
return None;
}
let side = if let Some(side_str) = Self::get_str(data, "positionSide") {
match side_str.to_uppercase().as_str() {
"LONG" => PositionSide::Long,
"SHORT" => PositionSide::Short,
_ => if position_amt > 0.0 { PositionSide::Long } else { PositionSide::Short }
}
} else if position_amt > 0.0 { PositionSide::Long } else { PositionSide::Short };
Some(Position {
symbol,
side,
quantity: position_amt.abs(),
entry_price: Self::get_f64(data, "avgPrice")
.or_else(|| Self::get_f64(data, "entryPrice"))
.unwrap_or(0.0),
mark_price: Self::get_f64(data, "markPrice"),
unrealized_pnl: Self::get_f64(data, "unrealizedProfit")
.or_else(|| Self::get_f64(data, "unrealisedPnl"))
.unwrap_or(0.0),
realized_pnl: Self::get_f64(data, "realisedProfit")
.or_else(|| Self::get_f64(data, "realisedPnl")),
leverage: Self::get_f64(data, "leverage").map(|l| l as u32).unwrap_or(1),
liquidation_price: Self::get_f64(data, "liquidationPrice"),
margin: Self::get_f64(data, "initialMargin"),
margin_type: if data.get("isolated").and_then(|v| v.as_bool()).unwrap_or(false) {
crate::core::MarginType::Isolated
} else {
crate::core::MarginType::Cross
},
take_profit: None,
stop_loss: None,
})
}
pub fn parse_ws_ticker(data: &Value, _account_type: crate::core::AccountType) -> ExchangeResult<Ticker> {
let symbol = Self::require_str(data, "s")?.to_string();
let last_price = Self::require_f64(data, "c")?;
let timestamp = data.get("C").and_then(|v| v.as_i64()).unwrap_or(0);
let price_change_24h = Self::get_f64(data, "p");
let price_change_percent_24h = Self::get_f64(data, "P").or_else(|| {
let open = Self::get_f64(data, "o")?;
if open > 0.0 {
Some((last_price - open) / open * 100.0)
} else {
None
}
});
Ok(Ticker {
symbol,
last_price,
bid_price: None, ask_price: None, high_24h: Self::get_f64(data, "h"),
low_24h: Self::get_f64(data, "l"),
volume_24h: Self::get_f64(data, "v"),
quote_volume_24h: Self::get_f64(data, "q"),
price_change_24h,
price_change_percent_24h,
timestamp,
})
}
pub fn parse_ws_trade(data: &Value) -> ExchangeResult<crate::core::PublicTrade> {
use crate::core::types::TradeSide;
let symbol = Self::require_str(data, "s")?.to_string();
let price = Self::require_f64(data, "p")?;
let quantity = Self::require_f64(data, "q")?;
let timestamp = Self::get_f64(data, "t").map(|t| t as i64).unwrap_or(0);
let is_buyer_maker = data.get("m").and_then(|v| v.as_bool()).unwrap_or(false);
let side = if is_buyer_maker {
TradeSide::Sell
} else {
TradeSide::Buy
};
Ok(crate::core::PublicTrade {
id: data.get("t").and_then(|v| v.as_i64()).map(|id| id.to_string()).unwrap_or_default(),
symbol,
price,
quantity,
side,
timestamp,
})
}
pub fn parse_ws_orderbook(data: &Value) -> ExchangeResult<crate::core::StreamEvent> {
let bids_arr = data.get("bids").and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing bids".to_string()))?;
let asks_arr = data.get("asks").and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing asks".to_string()))?;
let mut bids = Vec::new();
for bid in bids_arr {
if let Some(arr) = bid.as_array() {
if arr.len() >= 2 {
let price = Self::parse_f64(&arr[0]).unwrap_or(0.0);
let qty = Self::parse_f64(&arr[1]).unwrap_or(0.0);
bids.push(OrderBookLevel::new(price, qty));
}
}
}
let mut asks = Vec::new();
for ask in asks_arr {
if let Some(arr) = ask.as_array() {
if arr.len() >= 2 {
let price = Self::parse_f64(&arr[0]).unwrap_or(0.0);
let qty = Self::parse_f64(&arr[1]).unwrap_or(0.0);
asks.push(OrderBookLevel::new(price, qty));
}
}
}
let timestamp = crate::core::timestamp_millis() as i64;
Ok(crate::core::StreamEvent::OrderbookDelta(OrderbookDeltaData {
bids,
asks,
timestamp,
first_update_id: None,
last_update_id: None,
prev_update_id: None,
event_time: None,
checksum: None,
}))
}
pub fn parse_ws_kline(data: &Value) -> ExchangeResult<Kline> {
let kline_data = data.get("k")
.ok_or_else(|| ExchangeError::Parse("Missing 'k' field in kline data".to_string()))?;
let open_time = Self::get_f64(kline_data, "t").map(|t| t as i64)
.ok_or_else(|| ExchangeError::Parse("Missing timestamp".to_string()))?;
let open = Self::require_f64(kline_data, "o")?;
let high = Self::require_f64(kline_data, "h")?;
let low = Self::require_f64(kline_data, "l")?;
let close = Self::require_f64(kline_data, "c")?;
let volume = Self::require_f64(kline_data, "v")?;
Ok(Kline {
open_time,
open,
high,
low,
close,
volume,
close_time: Self::get_f64(kline_data, "T").map(|t| t as i64),
quote_volume: None,
trades: None,
})
}
pub fn parse_ws_order_update(
data: &Value,
_account_type: crate::core::AccountType,
) -> ExchangeResult<crate::core::OrderUpdateEvent> {
let order_id = Self::require_str(data, "i")?.to_string();
let client_order_id = Self::get_str(data, "c").map(|s| s.to_string());
let symbol = Self::require_str(data, "s")?.to_string();
let side_str = Self::require_str(data, "S")?;
let side = match side_str.to_uppercase().as_str() {
"BUY" => OrderSide::Buy,
"SELL" => OrderSide::Sell,
_ => return Err(ExchangeError::Parse(format!("Invalid side: {}", side_str))),
};
let order_type_str = Self::require_str(data, "o")?;
let order_type = match order_type_str.to_uppercase().as_str() {
"MARKET" => OrderType::Market,
"LIMIT" => OrderType::Limit { price: 0.0 },
_ => OrderType::Limit { price: 0.0 },
};
let status_str = Self::require_str(data, "X")?;
let status = match status_str.to_uppercase().as_str() {
"NEW" => OrderStatus::New,
"PARTIALLY_FILLED" => OrderStatus::PartiallyFilled,
"FILLED" => OrderStatus::Filled,
"CANCELED" | "CANCELLED" => OrderStatus::Canceled,
"REJECTED" => OrderStatus::Rejected,
"EXPIRED" => OrderStatus::Expired,
_ => OrderStatus::New,
};
let price = Self::get_f64(data, "p");
let quantity = Self::require_f64(data, "q")?;
let filled_quantity = Self::get_f64(data, "z").unwrap_or(0.0);
let average_price = Self::get_f64(data, "L");
let last_fill_price = Self::get_f64(data, "L");
let last_fill_quantity = Self::get_f64(data, "l");
let last_fill_commission = Self::get_f64(data, "n");
let commission_asset = Self::get_str(data, "N").map(|s| s.to_string());
let trade_id = data.get("t").and_then(|v| v.as_i64()).map(|id| id.to_string());
let timestamp = Self::get_f64(data, "T").map(|t| t as i64).unwrap_or(0);
Ok(crate::core::OrderUpdateEvent {
order_id,
client_order_id,
symbol,
side,
order_type,
status,
price,
quantity,
filled_quantity,
average_price,
last_fill_price,
last_fill_quantity,
last_fill_commission,
commission_asset,
trade_id,
timestamp,
})
}
pub fn parse_ws_balance_update(data: &Value) -> ExchangeResult<crate::core::BalanceUpdateEvent> {
let balances = data.get("B").and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'B' field in balance update".to_string()))?;
if balances.is_empty() {
return Err(ExchangeError::Parse("Empty balance array".to_string()));
}
let balance = &balances[0];
let asset = Self::require_str(balance, "a")?.to_string();
let free = Self::require_f64(balance, "f")?;
let locked = Self::require_f64(balance, "l")?;
let total = free + locked;
let timestamp = Self::get_f64(data, "E").map(|t| t as i64)
.unwrap_or_else(|| crate::core::timestamp_millis() as i64);
Ok(crate::core::BalanceUpdateEvent {
asset,
free,
locked,
total,
delta: None,
reason: None,
timestamp,
})
}
pub fn parse_ws_position_update(data: &Value) -> ExchangeResult<crate::core::PositionUpdateEvent> {
let symbol = Self::require_str(data, "s")?.to_string();
let position_amt = Self::require_f64(data, "pa")?;
let side = if position_amt > 0.0 {
PositionSide::Long
} else if position_amt < 0.0 {
PositionSide::Short
} else {
PositionSide::Long };
let quantity = position_amt.abs();
let entry_price = Self::require_f64(data, "ep")?;
let mark_price = Self::get_f64(data, "mp");
let unrealized_pnl = Self::require_f64(data, "up")?;
let realized_pnl = Self::get_f64(data, "rp");
let timestamp = crate::core::timestamp_millis() as i64;
Ok(crate::core::PositionUpdateEvent {
symbol,
side,
quantity,
entry_price,
mark_price,
unrealized_pnl,
realized_pnl,
liquidation_price: None,
leverage: None,
margin_type: None,
reason: None,
timestamp,
})
}
pub fn parse_swap_exchange_info(response: &Value, account_type: crate::core::AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let data = Self::extract_data(response)?;
let items = data.as_array()
.ok_or_else(|| ExchangeError::Parse("Expected array in data".to_string()))?;
let mut symbols = Vec::with_capacity(items.len());
for item in items {
let status = item.get("status").and_then(|v| v.as_i64()).unwrap_or(1);
if status != 1 {
continue;
}
let symbol = match item.get("symbol").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => continue,
};
let base_asset = item.get("asset")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let quote_asset = item.get("currency")
.and_then(|v| v.as_str())
.unwrap_or("USDT")
.to_string();
if base_asset.is_empty() {
continue;
}
let price_precision = item.get("tickSize")
.and_then(|v| v.as_f64())
.map(|ts| {
if ts <= 0.0 { 8u8 }
else { (-ts.log10().ceil()).max(0.0) as u8 }
})
.unwrap_or(2);
let quantity_precision = item.get("size")
.and_then(|v| v.as_f64())
.map(|ts| {
if ts <= 0.0 { 8u8 }
else { (-ts.log10().ceil()).max(0.0) as u8 }
})
.unwrap_or(4);
let min_quantity = item.get("tradeMinLimit")
.and_then(|v| v.as_f64());
let step_size = item.get("size").and_then(|v| v.as_f64());
let tick_size = item.get("tickSize").and_then(|v| v.as_f64());
symbols.push(SymbolInfo {
symbol,
base_asset,
quote_asset,
status: "TRADING".to_string(),
price_precision,
quantity_precision,
min_quantity,
max_quantity: None,
tick_size,
step_size,
min_notional: None,
account_type,
});
}
Ok(symbols)
}
pub fn parse_spot_exchange_info(response: &Value, account_type: crate::core::AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let data = Self::extract_data(response)?;
let symbols_arr = data.get("symbols")
.and_then(|s| s.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'symbols' array".to_string()))?;
let mut symbols = Vec::with_capacity(symbols_arr.len());
for item in symbols_arr {
let status = item.get("status").and_then(|v| v.as_i64()).unwrap_or(1);
if status != 1 {
continue;
}
let raw_symbol = match item.get("symbol").and_then(|v| v.as_str()) {
Some(s) => s,
None => continue,
};
let parts: Vec<&str> = raw_symbol.splitn(2, '-').collect();
let (base_asset, quote_asset) = if parts.len() == 2 {
(parts[0].to_string(), parts[1].to_string())
} else {
continue;
};
let price_precision = item.get("tickSize")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok())
.map(|ts| {
if ts <= 0.0 { 8u8 }
else { (-ts.log10().ceil()).max(0.0) as u8 }
})
.unwrap_or(2);
let quantity_precision = item.get("stepSize")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok())
.map(|ts| {
if ts <= 0.0 { 8u8 }
else { (-ts.log10().ceil()).max(0.0) as u8 }
})
.unwrap_or(8);
let min_quantity = item.get("minQty")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok());
let max_quantity = item.get("maxQty")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok());
let step_size = item.get("stepSize")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok());
let tick_size = item.get("tickSize")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok());
let min_notional = item.get("minNotional")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok());
symbols.push(SymbolInfo {
symbol: raw_symbol.to_string(),
base_asset,
quote_asset,
status: "TRADING".to_string(),
price_precision,
quantity_precision,
min_quantity,
max_quantity,
tick_size,
step_size,
min_notional,
account_type,
});
}
Ok(symbols)
}
pub fn parse_user_trades(response: &Value, is_futures: bool) -> ExchangeResult<Vec<UserTrade>> {
let data = Self::extract_data(response)?;
let arr = if is_futures {
data.get("fills")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'fills' array in swap trade response".to_string()))?
} else {
data.get("list")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'list' array in spot trade response".to_string()))?
};
arr.iter()
.map(|item| {
let id = item.get("tradeId")
.and_then(|v| v.as_str().map(|s| s.to_string())
.or_else(|| v.as_i64().map(|n| n.to_string())))
.ok_or_else(|| ExchangeError::Parse("Missing 'tradeId' in trade".to_string()))?;
let order_id = item.get("orderId")
.and_then(|v| v.as_str().map(|s| s.to_string())
.or_else(|| v.as_i64().map(|n| n.to_string())))
.unwrap_or_default();
let symbol = Self::get_str(item, "symbol")
.unwrap_or("")
.to_string();
let side = match Self::get_str(item, "side").unwrap_or("BUY").to_uppercase().as_str() {
"SELL" | "SHORT" => OrderSide::Sell,
_ => OrderSide::Buy,
};
let price = Self::require_f64(item, "price")?;
let quantity = Self::require_f64(item, "qty")?;
let commission = Self::get_f64(item, "commission").unwrap_or(0.0).abs();
let commission_asset = Self::get_str(item, "commissionAsset")
.unwrap_or("")
.to_string();
let is_maker = if is_futures {
Self::get_str(item, "role")
.map(|r| r.to_uppercase() == "MAKER")
.unwrap_or(false)
} else {
item.get("isMaker")
.and_then(|v| v.as_bool())
.unwrap_or(false)
};
let timestamp = if is_futures {
item.get("filledTm")
.and_then(|v| v.as_str().and_then(|s| s.parse::<i64>().ok())
.or_else(|| v.as_i64()))
.unwrap_or(0)
} else {
item.get("time")
.and_then(|v| v.as_i64())
.unwrap_or(0)
};
Ok(UserTrade {
id,
order_id,
symbol,
side,
price,
quantity,
commission,
commission_asset,
is_maker,
timestamp,
})
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_price() {
let response = json!({
"code": 0,
"msg": "",
"data": {
"price": "43302.50"
}
});
let price = BingxParser::parse_price(&response).unwrap();
assert!((price - 43302.50).abs() < f64::EPSILON);
}
#[test]
fn test_parse_ticker() {
let response = json!({
"code": 0,
"msg": "",
"data": {
"symbol": "BTC-USDT",
"lastPrice": "43302.50",
"bidPrice": "43302.00",
"askPrice": "43303.00",
"highPrice": "43500.00",
"lowPrice": "41800.00",
"volume": "12458.250000",
"quoteVolume": "536428950.25",
"priceChange": "1250.50",
"priceChangePercent": "2.98",
"closeTime": 1649404670162i64
}
});
let ticker = BingxParser::parse_ticker(&response).unwrap();
assert_eq!(ticker.symbol, "BTC-USDT");
assert!((ticker.last_price - 43302.50).abs() < f64::EPSILON);
assert!(ticker.bid_price.unwrap() < ticker.ask_price.unwrap());
}
#[test]
fn test_parse_orderbook() {
let response = json!({
"code": 0,
"msg": "",
"data": {
"bids": [
["43302.00", "0.521000"],
["43301.50", "0.234000"]
],
"asks": [
["43303.00", "0.321000"],
["43303.50", "0.892000"]
]
}
});
let orderbook = BingxParser::parse_orderbook(&response).unwrap();
assert_eq!(orderbook.bids.len(), 2);
assert_eq!(orderbook.asks.len(), 2);
assert!((orderbook.bids[0].price - 43302.0).abs() < f64::EPSILON);
}
#[test]
fn test_error_response() {
let response = json!({
"code": 100401,
"msg": "AUTHENTICATION_FAIL",
"data": null
});
let result = BingxParser::extract_data(&response);
assert!(result.is_err());
match result {
Err(ExchangeError::Api { code, message }) => {
assert_eq!(code, 100401);
assert_eq!(message, "AUTHENTICATION_FAIL");
}
_ => panic!("Expected API error"),
}
}
}