diff --git a/src/api/routes.rs b/src/api/routes.rs index d9fb1b2..87860fb 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -26,6 +26,7 @@ pub fn build(state: AppState) -> Router { .route("/wallet/spot", get(wallet::spot_balances)) .route("/wallet/earn", get(wallet::earn_balances)) .route("/wallet/futures", get(wallet::futures_balances)) + .route("/wallet/futures/coin", get(wallet::coin_futures_balances)) .route("/positions", get(positions::open_positions)) .route("/positions/closed", get(positions::closed_positions)) .route("/mode", get(mode::get_mode).post(mode::set_mode)) diff --git a/src/api/wallet.rs b/src/api/wallet.rs index 6d412f0..55b40af 100644 --- a/src/api/wallet.rs +++ b/src/api/wallet.rs @@ -82,3 +82,19 @@ pub async fn futures_balances(State(state): State) -> impl IntoRespons Err(e) => (axum::http::StatusCode::BAD_GATEWAY, e.to_string()).into_response(), } } + +pub async fn coin_futures_balances(State(state): State) -> impl IntoResponse { + let mode = state.current_mode.lock().await.clone(); + let client = make_client(&state, &mode); + + match client.get_coin_futures_balances().await { + Ok(balances) => { + let entries: Vec = balances + .into_iter() + .map(|(asset, wallet, unrealized_pnl, available)| FuturesBalance { asset, wallet, unrealized_pnl, available }) + .collect(); + Json(entries).into_response() + } + Err(e) => (axum::http::StatusCode::BAD_GATEWAY, e.to_string()).into_response(), + } +} diff --git a/src/binance/client.rs b/src/binance/client.rs index e349ede..da8fc64 100644 --- a/src/binance/client.rs +++ b/src/binance/client.rs @@ -458,6 +458,48 @@ impl BinanceClient { Ok(result) } + /// Coin-M Futures hesap bakiyesi: Vec<(asset, wallet_balance, unrealized_pnl, available)> + pub async fn get_coin_futures_balances(&self) -> Result> { + // Coin-M testnet ayrı URL, prod'da dapi.binance.com + let url = if self.base_url == BINANCE_TESTNET_URL { + "https://testnet.binancefutures.com/dapi/v1/account".to_string() + } else { + "https://dapi.binance.com/dapi/v1/account".to_string() + }; + let timestamp = chrono::Utc::now().timestamp_millis().to_string(); + let params = format!("timestamp={}", timestamp); + let signature = self.sign(¶ms); + + let response = self + .http + .get(&url) + .header("X-MBX-APIKEY", &self.api_key) + .query(&[("timestamp", timestamp.as_str()), ("signature", signature.as_str())]) + .send() + .await? + .json::() + .await?; + + if let Some(code) = response.get("code") { + return Err(anyhow!("Coin-M API hatası {}: {}", code, response["msg"].as_str().unwrap_or(""))); + } + + let assets = match response["assets"].as_array() { + Some(a) => a, + None => return Ok(vec![]), + }; + + let result = assets.iter().filter_map(|a| { + let asset = a["asset"].as_str()?.to_string(); + let wallet: f64 = a["walletBalance"].as_str()?.parse().ok()?; + let upnl: f64 = a["unrealizedProfit"].as_str()?.parse().ok()?; + let available: f64 = a["availableBalance"].as_str()?.parse().ok()?; + if wallet.abs() > 0.0 || upnl.abs() > 0.0 { Some((asset, wallet, upnl, available)) } else { None } + }).collect(); + + Ok(result) + } + /// HMAC-SHA256 imzası fn sign(&self, params: &str) -> String { use hmac::{Hmac, Mac}; diff --git a/src/web/wallet.html b/src/web/wallet.html index 40ae8cb..12cdf38 100644 --- a/src/web/wallet.html +++ b/src/web/wallet.html @@ -109,16 +109,34 @@ @@ -139,7 +157,8 @@ function fmt(n) { } let activeTab = 'spot'; -const loaded = { spot: false, earn: false, futures: false }; +let activeFuturesTab = 'usdm'; +const loaded = { spot: false, earn: false, futures_usdm: false, futures_coinm: false }; function showTab(name) { ['spot','earn','futures'].forEach(t => { @@ -147,18 +166,29 @@ function showTab(name) { document.getElementById('tab-btn-' + t).className = 'tab' + (t === name ? ' active' : ''); }); activeTab = name; - if (!loaded[name]) loadTab(name); + if (name === 'futures') { + if (!loaded['futures_' + activeFuturesTab]) loadFuturesTab(activeFuturesTab); + } else if (!loaded[name]) loadTab(name); +} + +function showFuturesTab(name) { + ['usdm','coinm'].forEach(t => { + document.getElementById('ftab-' + t).style.display = t === name ? '' : 'none'; + document.getElementById('ftab-btn-' + t).className = 'tab' + (t === name ? ' active' : ''); + }); + activeFuturesTab = name; + if (!loaded['futures_' + name]) loadFuturesTab(name); } function loadActive() { - loaded.spot = false; loaded.earn = false; loaded.futures = false; - loadTab(activeTab); + loaded.spot = false; loaded.earn = false; loaded.futures_usdm = false; loaded.futures_coinm = false; + if (activeTab === 'futures') loadFuturesTab(activeFuturesTab); + else loadTab(activeTab); } async function loadTab(name) { if (name === 'spot') await loadSpot(); else if (name === 'earn') await loadEarn(); - else if (name === 'futures') await loadFutures(); loaded[name] = true; } @@ -227,21 +257,9 @@ async function loadEarn() { `).join(''); } -async function loadFutures() { - const tbody = document.getElementById('futures-body'); - tbody.innerHTML = 'Yükleniyor...'; - document.getElementById('futures-total').textContent = '—'; - - const res = await api('/wallet/futures'); - if (!res.ok) { tbody.innerHTML = `Hata: ${await res.text()}`; return; } - const balances = await res.json(); - if (!balances.length) { tbody.innerHTML = 'Futures bakiyesi yok'; return; } - +function futuresRowsHtml(balances) { balances.sort((a, b) => b.wallet - a.wallet); - const totalWallet = balances.reduce((s, b) => s + b.wallet, 0); - document.getElementById('futures-total').textContent = `${fmt(totalWallet)} USDT toplam`; - - tbody.innerHTML = balances.map(b => { + return balances.map(b => { const pnlClass = b.unrealized_pnl >= 0 ? 'val-pnl-pos' : 'val-pnl-neg'; const pnlSign = b.unrealized_pnl >= 0 ? '+' : ''; return ` @@ -253,6 +271,34 @@ async function loadFutures() { }).join(''); } +async function loadFuturesTab(name) { + const isUsdm = name === 'usdm'; + const bodyId = isUsdm ? 'futures-usdm-body' : 'futures-coinm-body'; + const totalId = isUsdm ? 'futures-usdm-total' : 'futures-coinm-total'; + const endpoint = isUsdm ? '/wallet/futures' : '/wallet/futures/coin'; + const cols = 4; + + const tbody = document.getElementById(bodyId); + tbody.innerHTML = `Yükleniyor...`; + document.getElementById(totalId).textContent = '—'; + + const res = await api(endpoint); + if (!res.ok) { + tbody.innerHTML = `Hata: ${await res.text()}`; + loaded['futures_' + name] = true; + return; + } + const balances = await res.json(); + if (!balances.length) { + tbody.innerHTML = `Bakiye yok`; + } else { + const total = balances.reduce((s, b) => s + b.wallet, 0); + document.getElementById(totalId).textContent = `${fmt(total)} ${isUsdm ? 'USDT' : ''} toplam`; + tbody.innerHTML = futuresRowsHtml(balances); + } + loaded['futures_' + name] = true; +} + async function logout() { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); window.location.href = '/';