feat(mse): add Coin-M futures wallet tab

- BinanceClient::get_coin_futures_balances() — /dapi/v1/account
- GET /api/wallet/futures/coin endpoint
- Futures tab split into USD-M / Coin-M sub-tabs, lazy-loaded independently

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-25 12:22:52 +03:00
parent 69420ab9e4
commit 65876fb895
4 changed files with 134 additions and 29 deletions

View file

@ -26,6 +26,7 @@ pub fn build(state: AppState) -> Router {
.route("/wallet/spot", get(wallet::spot_balances)) .route("/wallet/spot", get(wallet::spot_balances))
.route("/wallet/earn", get(wallet::earn_balances)) .route("/wallet/earn", get(wallet::earn_balances))
.route("/wallet/futures", get(wallet::futures_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", get(positions::open_positions))
.route("/positions/closed", get(positions::closed_positions)) .route("/positions/closed", get(positions::closed_positions))
.route("/mode", get(mode::get_mode).post(mode::set_mode)) .route("/mode", get(mode::get_mode).post(mode::set_mode))

View file

@ -82,3 +82,19 @@ pub async fn futures_balances(State(state): State<AppState>) -> impl IntoRespons
Err(e) => (axum::http::StatusCode::BAD_GATEWAY, e.to_string()).into_response(), Err(e) => (axum::http::StatusCode::BAD_GATEWAY, e.to_string()).into_response(),
} }
} }
pub async fn coin_futures_balances(State(state): State<AppState>) -> 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<FuturesBalance> = 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(),
}
}

View file

@ -458,6 +458,48 @@ impl BinanceClient {
Ok(result) Ok(result)
} }
/// Coin-M Futures hesap bakiyesi: Vec<(asset, wallet_balance, unrealized_pnl, available)>
pub async fn get_coin_futures_balances(&self) -> Result<Vec<(String, f64, f64, f64)>> {
// 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(&params);
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::<Value>()
.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ı /// HMAC-SHA256 imzası
fn sign(&self, params: &str) -> String { fn sign(&self, params: &str) -> String {
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};

View file

@ -109,17 +109,35 @@
</div> </div>
<div id="tab-futures" style="display:none"> <div id="tab-futures" style="display:none">
<div style="display:flex;gap:6px;margin-bottom:12px">
<button class="tab active" id="ftab-btn-usdm" onclick="showFuturesTab('usdm')">USD-M</button>
<button class="tab" id="ftab-btn-coinm" onclick="showFuturesTab('coinm')">Coin-M</button>
</div>
<div id="ftab-usdm">
<section> <section>
<div class="section-header"> <div class="section-header">
<span class="section-title">USD-M Futures</span> <span class="section-title">USD-M Futures</span>
<span class="total-badge" id="futures-total"></span> <span class="total-badge" id="futures-usdm-total"></span>
</div> </div>
<table> <table>
<thead><tr><th>Varlık</th><th>Cüzdan Bakiyesi</th><th>Gerçekleşmemiş PnL</th><th>Kullanılabilir</th></tr></thead> <thead><tr><th>Varlık</th><th>Cüzdan Bakiyesi</th><th>Gerçekleşmemiş PnL</th><th>Kullanılabilir</th></tr></thead>
<tbody id="futures-body"><tr><td colspan="4" class="loading">Yükleniyor...</td></tr></tbody> <tbody id="futures-usdm-body"><tr><td colspan="4" class="loading">Yükleniyor...</td></tr></tbody>
</table> </table>
</section> </section>
</div> </div>
<div id="ftab-coinm" style="display:none">
<section>
<div class="section-header">
<span class="section-title">Coin-M Futures</span>
<span class="total-badge" id="futures-coinm-total"></span>
</div>
<table>
<thead><tr><th>Varlık</th><th>Cüzdan Bakiyesi</th><th>Gerçekleşmemiş PnL</th><th>Kullanılabilir</th></tr></thead>
<tbody id="futures-coinm-body"><tr><td colspan="4" class="loading">Yükleniyor...</td></tr></tbody>
</table>
</section>
</div>
</div>
</main> </main>
<script> <script>
@ -139,7 +157,8 @@ function fmt(n) {
} }
let activeTab = 'spot'; 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) { function showTab(name) {
['spot','earn','futures'].forEach(t => { ['spot','earn','futures'].forEach(t => {
@ -147,18 +166,29 @@ function showTab(name) {
document.getElementById('tab-btn-' + t).className = 'tab' + (t === name ? ' active' : ''); document.getElementById('tab-btn-' + t).className = 'tab' + (t === name ? ' active' : '');
}); });
activeTab = name; 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() { function loadActive() {
loaded.spot = false; loaded.earn = false; loaded.futures = false; loaded.spot = false; loaded.earn = false; loaded.futures_usdm = false; loaded.futures_coinm = false;
loadTab(activeTab); if (activeTab === 'futures') loadFuturesTab(activeFuturesTab);
else loadTab(activeTab);
} }
async function loadTab(name) { async function loadTab(name) {
if (name === 'spot') await loadSpot(); if (name === 'spot') await loadSpot();
else if (name === 'earn') await loadEarn(); else if (name === 'earn') await loadEarn();
else if (name === 'futures') await loadFutures();
loaded[name] = true; loaded[name] = true;
} }
@ -227,21 +257,9 @@ async function loadEarn() {
</tr>`).join(''); </tr>`).join('');
} }
async function loadFutures() { function futuresRowsHtml(balances) {
const tbody = document.getElementById('futures-body');
tbody.innerHTML = '<tr><td colspan="4" class="loading">Yükleniyor...</td></tr>';
document.getElementById('futures-total').textContent = '—';
const res = await api('/wallet/futures');
if (!res.ok) { tbody.innerHTML = `<tr><td colspan="4" class="empty">Hata: ${await res.text()}</td></tr>`; return; }
const balances = await res.json();
if (!balances.length) { tbody.innerHTML = '<tr><td colspan="4" class="empty">Futures bakiyesi yok</td></tr>'; return; }
balances.sort((a, b) => b.wallet - a.wallet); balances.sort((a, b) => b.wallet - a.wallet);
const totalWallet = balances.reduce((s, b) => s + b.wallet, 0); return balances.map(b => {
document.getElementById('futures-total').textContent = `${fmt(totalWallet)} USDT toplam`;
tbody.innerHTML = balances.map(b => {
const pnlClass = b.unrealized_pnl >= 0 ? 'val-pnl-pos' : 'val-pnl-neg'; const pnlClass = b.unrealized_pnl >= 0 ? 'val-pnl-pos' : 'val-pnl-neg';
const pnlSign = b.unrealized_pnl >= 0 ? '+' : ''; const pnlSign = b.unrealized_pnl >= 0 ? '+' : '';
return `<tr> return `<tr>
@ -253,6 +271,34 @@ async function loadFutures() {
}).join(''); }).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 = `<tr><td colspan="${cols}" class="loading">Yükleniyor...</td></tr>`;
document.getElementById(totalId).textContent = '—';
const res = await api(endpoint);
if (!res.ok) {
tbody.innerHTML = `<tr><td colspan="${cols}" class="empty">Hata: ${await res.text()}</td></tr>`;
loaded['futures_' + name] = true;
return;
}
const balances = await res.json();
if (!balances.length) {
tbody.innerHTML = `<tr><td colspan="${cols}" class="empty">Bakiye yok</td></tr>`;
} 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() { async function logout() {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
window.location.href = '/'; window.location.href = '/';