Skip to content

Commit f0e6883

Browse files
committed
feat(binance): add comprehensive account balance fetching with multiple account types
- Add BalanceFetchParams struct to support different account types (spot, margin, futures, funding) - Implement fetch_balance_with_params supporting all Binance account types and margin modes - Add dedicated methods for spot, cross margin, isolated margin, futures, delivery, funding, and option balances - Support portfolio margin API for advanced account types - Update example and test files to use AccountType enum instead of string parameters - Refactor API calls to use signed_request builder pattern for cleaner code - Add proper documentation and examples for all new balance fetching methods - Implement isolated margin balance fetching with symbol support - Add USDT-margined futures (linear) and coin-margined futures (inverse) balance support - Support funding wallet and options account balance fetching - Add comprehensive test coverage for different account types
1 parent ed8adee commit f0e6883

14 files changed

Lines changed: 1615 additions & 1292 deletions

File tree

‎ccxt-exchanges/src/binance/mod.rs‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ pub mod error;
1616
mod exchange_impl;
1717
pub mod parser;
1818
pub mod rest;
19+
pub mod signed_request;
1920
pub mod symbol;
2021
pub mod time_sync;
21-
pub mod traits;
2222
pub mod ws;
2323
mod ws_exchange_impl;
2424

2525
pub use builder::BinanceBuilder;
26+
pub use signed_request::{HttpMethod, SignedRequestBuilder};
2627
pub use time_sync::{TimeSyncConfig, TimeSyncManager};
2728

2829
/// Binance exchange structure.
@@ -315,6 +316,44 @@ impl Binance {
315316
&self.time_sync
316317
}
317318

319+
/// Creates a new signed request builder for the given endpoint.
320+
///
321+
/// This is the recommended way to make authenticated API requests.
322+
/// The builder handles credential validation, timestamp generation,
323+
/// parameter signing, and request execution.
324+
///
325+
/// # Arguments
326+
///
327+
/// * `endpoint` - Full API endpoint URL
328+
///
329+
/// # Example
330+
///
331+
/// ```no_run
332+
/// use ccxt_exchanges::binance::{Binance, HttpMethod};
333+
/// use ccxt_core::ExchangeConfig;
334+
///
335+
/// # async fn example() -> ccxt_core::Result<()> {
336+
/// let binance = Binance::new(ExchangeConfig::default())?;
337+
///
338+
/// // Simple GET request
339+
/// let response = binance.signed_request("https://api.binance.com/api/v3/account")
340+
/// .execute()
341+
/// .await?;
342+
///
343+
/// // POST request with parameters
344+
/// let response = binance.signed_request("https://api.binance.com/api/v3/order")
345+
/// .method(HttpMethod::Post)
346+
/// .param("symbol", "BTCUSDT")
347+
/// .param("side", "BUY")
348+
/// .execute()
349+
/// .await?;
350+
/// # Ok(())
351+
/// # }
352+
/// ```
353+
pub fn signed_request(&self, endpoint: impl Into<String>) -> SignedRequestBuilder<'_> {
354+
SignedRequestBuilder::new(self, endpoint)
355+
}
356+
318357
/// Returns the exchange ID.
319358
pub fn id(&self) -> &str {
320359
"binance"

‎ccxt-exchanges/src/binance/parser.rs‎

Lines changed: 159 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,66 +2221,100 @@ pub fn parse_balance_with_type(data: &Value, account_type: &str) -> Result<Balan
22212221
}
22222222
}
22232223
"linear" | "future" => {
2224-
if let Some(assets) = data["assets"].as_array() {
2225-
for item in assets {
2226-
if let Some(asset) = item["asset"].as_str() {
2227-
let available_balance =
2228-
if let Some(balance_str) = item["availableBalance"].as_str() {
2229-
balance_str.parse::<f64>().unwrap_or(0.0)
2230-
} else {
2231-
item["availableBalance"].as_f64().unwrap_or(0.0)
2232-
};
2224+
// Handle both array response (from /fapi/v2/balance) and object with assets field
2225+
let assets = if let Some(arr) = data.as_array() {
2226+
arr.clone()
2227+
} else if let Some(arr) = data["assets"].as_array() {
2228+
arr.clone()
2229+
} else {
2230+
vec![]
2231+
};
22332232

2234-
let wallet_balance =
2235-
if let Some(balance_str) = item["walletBalance"].as_str() {
2236-
balance_str.parse::<f64>().unwrap_or(0.0)
2237-
} else {
2238-
item["walletBalance"].as_f64().unwrap_or(0.0)
2239-
};
2233+
for item in &assets {
2234+
if let Some(asset) = item["asset"].as_str() {
2235+
let available_balance =
2236+
if let Some(balance_str) = item["availableBalance"].as_str() {
2237+
balance_str.parse::<f64>().unwrap_or(0.0)
2238+
} else {
2239+
item["availableBalance"].as_f64().unwrap_or(0.0)
2240+
};
22402241

2241-
let used = wallet_balance - available_balance;
2242+
let wallet_balance = if let Some(balance_str) = item["walletBalance"].as_str() {
2243+
balance_str.parse::<f64>().unwrap_or(0.0)
2244+
} else {
2245+
item["walletBalance"].as_f64().unwrap_or(0.0)
2246+
};
22422247

2243-
balances.insert(
2244-
asset.to_string(),
2245-
BalanceEntry {
2246-
free: Decimal::from_f64(available_balance).unwrap_or(Decimal::ZERO),
2247-
used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2248-
total: Decimal::from_f64(wallet_balance).unwrap_or(Decimal::ZERO),
2249-
},
2250-
);
2251-
}
2248+
// Also check balance field as fallback
2249+
let wallet_balance = if wallet_balance == 0.0 {
2250+
if let Some(balance_str) = item["balance"].as_str() {
2251+
balance_str.parse::<f64>().unwrap_or(0.0)
2252+
} else {
2253+
item["balance"].as_f64().unwrap_or(wallet_balance)
2254+
}
2255+
} else {
2256+
wallet_balance
2257+
};
2258+
2259+
let used = wallet_balance - available_balance;
2260+
2261+
balances.insert(
2262+
asset.to_string(),
2263+
BalanceEntry {
2264+
free: Decimal::from_f64(available_balance).unwrap_or(Decimal::ZERO),
2265+
used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2266+
total: Decimal::from_f64(wallet_balance).unwrap_or(Decimal::ZERO),
2267+
},
2268+
);
22522269
}
22532270
}
22542271
}
22552272
"inverse" | "delivery" => {
2256-
if let Some(assets) = data["assets"].as_array() {
2257-
for item in assets {
2258-
if let Some(asset) = item["asset"].as_str() {
2259-
let available_balance =
2260-
if let Some(balance_str) = item["availableBalance"].as_str() {
2261-
balance_str.parse::<f64>().unwrap_or(0.0)
2262-
} else {
2263-
item["availableBalance"].as_f64().unwrap_or(0.0)
2264-
};
2273+
// Handle both array response and object with assets field
2274+
let assets = if let Some(arr) = data.as_array() {
2275+
arr.clone()
2276+
} else if let Some(arr) = data["assets"].as_array() {
2277+
arr.clone()
2278+
} else {
2279+
vec![]
2280+
};
22652281

2266-
let wallet_balance =
2267-
if let Some(balance_str) = item["walletBalance"].as_str() {
2268-
balance_str.parse::<f64>().unwrap_or(0.0)
2269-
} else {
2270-
item["walletBalance"].as_f64().unwrap_or(0.0)
2271-
};
2282+
for item in &assets {
2283+
if let Some(asset) = item["asset"].as_str() {
2284+
let available_balance =
2285+
if let Some(balance_str) = item["availableBalance"].as_str() {
2286+
balance_str.parse::<f64>().unwrap_or(0.0)
2287+
} else {
2288+
item["availableBalance"].as_f64().unwrap_or(0.0)
2289+
};
22722290

2273-
let used = wallet_balance - available_balance;
2291+
let wallet_balance = if let Some(balance_str) = item["walletBalance"].as_str() {
2292+
balance_str.parse::<f64>().unwrap_or(0.0)
2293+
} else {
2294+
item["walletBalance"].as_f64().unwrap_or(0.0)
2295+
};
22742296

2275-
balances.insert(
2276-
asset.to_string(),
2277-
BalanceEntry {
2278-
free: Decimal::from_f64(available_balance).unwrap_or(Decimal::ZERO),
2279-
used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2280-
total: Decimal::from_f64(wallet_balance).unwrap_or(Decimal::ZERO),
2281-
},
2282-
);
2283-
}
2297+
// Also check balance field as fallback
2298+
let wallet_balance = if wallet_balance == 0.0 {
2299+
if let Some(balance_str) = item["balance"].as_str() {
2300+
balance_str.parse::<f64>().unwrap_or(0.0)
2301+
} else {
2302+
item["balance"].as_f64().unwrap_or(wallet_balance)
2303+
}
2304+
} else {
2305+
wallet_balance
2306+
};
2307+
2308+
let used = wallet_balance - available_balance;
2309+
2310+
balances.insert(
2311+
asset.to_string(),
2312+
BalanceEntry {
2313+
free: Decimal::from_f64(available_balance).unwrap_or(Decimal::ZERO),
2314+
used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2315+
total: Decimal::from_f64(wallet_balance).unwrap_or(Decimal::ZERO),
2316+
},
2317+
);
22842318
}
22852319
}
22862320
}
@@ -2314,6 +2348,81 @@ pub fn parse_balance_with_type(data: &Value, account_type: &str) -> Result<Balan
23142348
}
23152349
}
23162350
}
2351+
"option" => {
2352+
// Options account uses equity and available fields
2353+
if let Some(asset) = data["asset"].as_str() {
2354+
let equity = if let Some(equity_str) = data["equity"].as_str() {
2355+
equity_str.parse::<f64>().unwrap_or(0.0)
2356+
} else {
2357+
data["equity"].as_f64().unwrap_or(0.0)
2358+
};
2359+
2360+
let available = if let Some(available_str) = data["available"].as_str() {
2361+
available_str.parse::<f64>().unwrap_or(0.0)
2362+
} else {
2363+
data["available"].as_f64().unwrap_or(0.0)
2364+
};
2365+
2366+
let used = equity - available;
2367+
2368+
balances.insert(
2369+
asset.to_string(),
2370+
BalanceEntry {
2371+
free: Decimal::from_f64(available).unwrap_or(Decimal::ZERO),
2372+
used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2373+
total: Decimal::from_f64(equity).unwrap_or(Decimal::ZERO),
2374+
},
2375+
);
2376+
}
2377+
}
2378+
"portfolio" => {
2379+
// Portfolio margin balance - can be array or single object
2380+
let assets = if let Some(arr) = data.as_array() {
2381+
arr.clone()
2382+
} else if data.is_object() {
2383+
vec![data.clone()]
2384+
} else {
2385+
vec![]
2386+
};
2387+
2388+
for item in &assets {
2389+
if let Some(asset) = item["asset"].as_str() {
2390+
let total_wallet_balance =
2391+
if let Some(balance_str) = item["totalWalletBalance"].as_str() {
2392+
balance_str.parse::<f64>().unwrap_or(0.0)
2393+
} else {
2394+
item["totalWalletBalance"].as_f64().unwrap_or(0.0)
2395+
};
2396+
2397+
let available_balance =
2398+
if let Some(balance_str) = item["availableBalance"].as_str() {
2399+
balance_str.parse::<f64>().unwrap_or(0.0)
2400+
} else {
2401+
item["availableBalance"].as_f64().unwrap_or(0.0)
2402+
};
2403+
2404+
// Fallback to crossWalletBalance if availableBalance not present
2405+
let free = if available_balance > 0.0 {
2406+
available_balance
2407+
} else if let Some(cross_str) = item["crossWalletBalance"].as_str() {
2408+
cross_str.parse::<f64>().unwrap_or(0.0)
2409+
} else {
2410+
item["crossWalletBalance"].as_f64().unwrap_or(0.0)
2411+
};
2412+
2413+
let used = total_wallet_balance - free;
2414+
2415+
balances.insert(
2416+
asset.to_string(),
2417+
BalanceEntry {
2418+
free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2419+
used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2420+
total: Decimal::from_f64(total_wallet_balance).unwrap_or(Decimal::ZERO),
2421+
},
2422+
);
2423+
}
2424+
}
2425+
}
23172426
_ => {
23182427
return Err(Error::from(ParseError::invalid_value(
23192428
"account_type",

0 commit comments

Comments
 (0)